@zapier/zapier-sdk 0.8.2 → 0.9.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 (104) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +10 -33
  3. package/dist/api/client.d.ts.map +1 -1
  4. package/dist/api/client.js +1 -2
  5. package/dist/api/polling.d.ts +36 -6
  6. package/dist/api/polling.d.ts.map +1 -1
  7. package/dist/api/polling.js +132 -28
  8. package/dist/api/polling.test.d.ts +2 -0
  9. package/dist/api/polling.test.d.ts.map +1 -0
  10. package/dist/api/polling.test.js +318 -0
  11. package/dist/api/types.d.ts +1 -2
  12. package/dist/api/types.d.ts.map +1 -1
  13. package/dist/index.cjs +489 -252
  14. package/dist/index.d.mts +182 -187
  15. package/dist/index.d.ts +1 -2
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +0 -1
  18. package/dist/index.mjs +486 -251
  19. package/dist/plugins/apps/index.d.ts +4 -0
  20. package/dist/plugins/apps/index.d.ts.map +1 -1
  21. package/dist/plugins/getApp/index.d.ts +2 -7
  22. package/dist/plugins/getApp/index.d.ts.map +1 -1
  23. package/dist/plugins/getApp/index.js +9 -9
  24. package/dist/plugins/getApp/index.test.js +1 -1
  25. package/dist/plugins/getAuthentication/index.test.js +1 -1
  26. package/dist/plugins/listActions/index.d.ts +2 -4
  27. package/dist/plugins/listActions/index.d.ts.map +1 -1
  28. package/dist/plugins/listActions/index.js +1 -1
  29. package/dist/plugins/listActions/index.test.js +4 -4
  30. package/dist/plugins/listApps/index.d.ts +4 -7
  31. package/dist/plugins/listApps/index.d.ts.map +1 -1
  32. package/dist/plugins/listApps/index.js +33 -17
  33. package/dist/plugins/listApps/index.test.js +22 -2
  34. package/dist/plugins/listAuthentications/index.d.ts +2 -4
  35. package/dist/plugins/listAuthentications/index.d.ts.map +1 -1
  36. package/dist/plugins/listAuthentications/index.js +4 -0
  37. package/dist/plugins/listAuthentications/index.test.js +39 -13
  38. package/dist/plugins/listAuthentications/schemas.d.ts +3 -0
  39. package/dist/plugins/listAuthentications/schemas.d.ts.map +1 -1
  40. package/dist/plugins/listAuthentications/schemas.js +4 -0
  41. package/dist/plugins/manifest/index.d.ts +25 -9
  42. package/dist/plugins/manifest/index.d.ts.map +1 -1
  43. package/dist/plugins/manifest/index.js +239 -67
  44. package/dist/plugins/manifest/index.test.js +426 -171
  45. package/dist/plugins/manifest/schemas.d.ts +5 -1
  46. package/dist/plugins/manifest/schemas.d.ts.map +1 -1
  47. package/dist/plugins/manifest/schemas.js +1 -0
  48. package/dist/sdk.d.ts +5 -11
  49. package/dist/sdk.d.ts.map +1 -1
  50. package/dist/sdk.js +1 -4
  51. package/dist/types/plugin.d.ts +1 -0
  52. package/dist/types/plugin.d.ts.map +1 -1
  53. package/dist/types/sdk.d.ts +6 -3
  54. package/dist/types/sdk.d.ts.map +1 -1
  55. package/dist/utils/domain-utils.d.ts +16 -0
  56. package/dist/utils/domain-utils.d.ts.map +1 -1
  57. package/dist/utils/domain-utils.js +46 -27
  58. package/dist/utils/domain-utils.test.js +157 -3
  59. package/dist/utils/file-utils.d.ts +4 -0
  60. package/dist/utils/file-utils.d.ts.map +1 -0
  61. package/dist/utils/file-utils.js +74 -0
  62. package/dist/utils/file-utils.test.d.ts +2 -0
  63. package/dist/utils/file-utils.test.d.ts.map +1 -0
  64. package/dist/utils/file-utils.test.js +51 -0
  65. package/package.json +1 -1
  66. package/src/api/client.ts +5 -4
  67. package/src/api/polling.test.ts +405 -0
  68. package/src/api/polling.ts +224 -44
  69. package/src/api/types.ts +1 -2
  70. package/src/index.ts +1 -1
  71. package/src/plugins/apps/index.ts +9 -2
  72. package/src/plugins/getApp/index.test.ts +1 -1
  73. package/src/plugins/getApp/index.ts +12 -14
  74. package/src/plugins/getAuthentication/index.test.ts +1 -1
  75. package/src/plugins/listActions/index.test.ts +8 -7
  76. package/src/plugins/listActions/index.ts +3 -3
  77. package/src/plugins/listApps/index.test.ts +23 -2
  78. package/src/plugins/listApps/index.ts +46 -25
  79. package/src/plugins/listAuthentications/index.test.ts +52 -15
  80. package/src/plugins/listAuthentications/index.ts +7 -2
  81. package/src/plugins/listAuthentications/schemas.ts +4 -0
  82. package/src/plugins/manifest/index.test.ts +503 -197
  83. package/src/plugins/manifest/index.ts +338 -82
  84. package/src/plugins/manifest/schemas.ts +9 -2
  85. package/src/sdk.ts +1 -5
  86. package/src/types/plugin.ts +3 -0
  87. package/src/types/sdk.ts +26 -21
  88. package/src/utils/domain-utils.test.ts +196 -2
  89. package/src/utils/domain-utils.ts +68 -35
  90. package/src/utils/file-utils.test.ts +73 -0
  91. package/src/utils/file-utils.ts +94 -0
  92. package/tsconfig.tsbuildinfo +1 -1
  93. package/dist/plugins/lockVersion/index.d.ts +0 -24
  94. package/dist/plugins/lockVersion/index.d.ts.map +0 -1
  95. package/dist/plugins/lockVersion/index.js +0 -72
  96. package/dist/plugins/lockVersion/index.test.d.ts +0 -2
  97. package/dist/plugins/lockVersion/index.test.d.ts.map +0 -1
  98. package/dist/plugins/lockVersion/index.test.js +0 -129
  99. package/dist/plugins/lockVersion/schemas.d.ts +0 -10
  100. package/dist/plugins/lockVersion/schemas.d.ts.map +0 -1
  101. package/dist/plugins/lockVersion/schemas.js +0 -6
  102. package/src/plugins/lockVersion/index.test.ts +0 -176
  103. package/src/plugins/lockVersion/index.ts +0 -112
  104. package/src/plugins/lockVersion/schemas.ts +0 -9
@@ -5,69 +5,249 @@
5
5
  * with configurable retry logic and exponential backoff.
6
6
  */
7
7
 
8
- import { ZapierTimeoutError, ZapierApiError } from "../types/errors";
8
+ import {
9
+ ZapierTimeoutError,
10
+ ZapierApiError,
11
+ ZapierValidationError,
12
+ } from "../types/errors";
13
+ import { setTimeout } from "timers/promises";
9
14
 
10
- export async function pollUntilComplete(options: {
15
+ // Constants
16
+ const DEFAULT_TIMEOUT_MS = 180_000;
17
+ const DEFAULT_SUCCESS_STATUS = 200;
18
+ const DEFAULT_PENDING_STATUS = 202;
19
+ const DEFAULT_INITIAL_DELAY_MS = 50;
20
+ const DEFAULT_MAX_POLLING_INTERVAL_MS = 10_000;
21
+ const MAX_CONSECUTIVE_ERRORS = 3;
22
+ const MAX_TIMEOUT_BUFFER_MS = 10_000;
23
+ const BASE_ERROR_BACKOFF_MS = 1_000;
24
+ const JITTER_FACTOR = 0.5;
25
+
26
+ // Polling stages: [threshold_ms, interval_ms]
27
+ // Note: These are default stages, actual hard timeout is enforced separately below
28
+ const DEFAULT_POLLING_STAGES = [
29
+ [125, 125], // Up to 125ms: poll every 125ms
30
+ [375, 250], // Up to 375ms: poll every 250ms
31
+ [875, 500], // Up to 875ms: poll every 500ms
32
+ [10_000, 1_000], // Up to 10s: poll every 1s
33
+ [30_000, 2_500], // Up to 30s: poll every 2.5s
34
+ [60_000, 5_000], // Up to 60s: poll every 5s
35
+ ] as const satisfies Array<[number, number]>;
36
+
37
+ /**
38
+ * Options for the polling function
39
+ */
40
+ export interface PollOptions<TResult = unknown> {
41
+ /** Function that performs the HTTP request */
11
42
  fetchPoll: () => Promise<Response>;
12
- maxAttempts?: number;
13
- initialDelay?: number;
14
- maxDelay?: number;
43
+ /** Maximum time to wait for completion (in milliseconds) */
44
+ timeoutMs?: number;
45
+ /** HTTP status code indicating successful completion */
15
46
  successStatus?: number;
47
+ /** HTTP status code indicating the operation is still pending */
16
48
  pendingStatus?: number;
17
- resultExtractor?: (response: any) => any;
18
- }): Promise<any> {
49
+ /** Function to extract the result from the response */
50
+ resultExtractor?: (response: unknown) => TResult;
51
+ /** Initial delay before the first poll attempt (in milliseconds) */
52
+ initialDelay?: number;
53
+ }
54
+
55
+ const enum PollStatus {
56
+ Success = "success",
57
+ Continue = "continue",
58
+ }
59
+
60
+ /**
61
+ * Result of a poll operation
62
+ */
63
+ export type PollResult<TResult = unknown> = {
64
+ result?: TResult;
65
+ status: PollStatus;
66
+ errorCount: number;
67
+ };
68
+
69
+ // Helper to calculate wait time with jitter and error backoff
70
+ const calculateWaitTime = (
71
+ baseInterval: number,
72
+ errorCount: number,
73
+ ): number => {
74
+ // Jitter to avoid thundering herd
75
+ const jitter = Math.random() * JITTER_FACTOR * baseInterval;
76
+ // More backoff added if errors are seen
77
+ const errorBackoff = Math.min(
78
+ BASE_ERROR_BACKOFF_MS * (errorCount / 2),
79
+ baseInterval * 2, // Cap error backoff at 2x the base interval
80
+ );
81
+ return Math.floor(baseInterval + jitter + errorBackoff);
82
+ };
83
+
84
+ const processResponse = async <TResult = unknown>(
85
+ response: Response,
86
+ successStatus: number,
87
+ pendingStatus: number,
88
+ resultExtractor: (response: unknown) => TResult,
89
+ errorCount: number,
90
+ ): Promise<PollResult<TResult>> => {
91
+ // Handle other error responses
92
+ if (!response.ok) {
93
+ return {
94
+ status: PollStatus.Continue,
95
+ // If for some reason the status is pending, we don't want to increment the error count
96
+ errorCount:
97
+ response.status === pendingStatus ? errorCount : errorCount + 1,
98
+ };
99
+ }
100
+
101
+ // Check for successful completion
102
+ if (response.status === successStatus) {
103
+ try {
104
+ const resultJson = await response.json();
105
+ return {
106
+ result: resultExtractor(resultJson),
107
+ status: PollStatus.Success,
108
+ errorCount: 0,
109
+ };
110
+ } catch (error) {
111
+ throw new ZapierApiError(
112
+ "Result extractor failed to parse successful response as JSON",
113
+ {
114
+ statusCode: response.status,
115
+ cause: error,
116
+ },
117
+ );
118
+ }
119
+ }
120
+
121
+ // If it's not pending, it's unexpected
122
+ if (response.status !== pendingStatus) {
123
+ throw new ZapierApiError(
124
+ `Unexpected response status during polling: ${response.status}`,
125
+ {
126
+ statusCode: response.status,
127
+ },
128
+ );
129
+ }
130
+
131
+ // It's still pending, so we continue polling
132
+ return {
133
+ status: PollStatus.Continue,
134
+ errorCount: 0,
135
+ };
136
+ };
137
+
138
+ /**
139
+ * Polls an endpoint until completion, timeout, or error
140
+ * @param options Configuration options for polling
141
+ * @returns The extracted result from the successful response
142
+ * @throws {ZapierValidationError} When the input parameters are invalid
143
+ * @throws {ZapierTimeoutError} When the operation times out
144
+ * @throws {ZapierApiError} When the API returns consecutive errors
145
+ */
146
+ export async function pollUntilComplete<TResult = unknown>(
147
+ options: PollOptions<TResult>,
148
+ ): Promise<TResult> {
19
149
  const {
20
150
  fetchPoll,
21
- maxAttempts = 30,
22
- initialDelay = 50,
23
- maxDelay = 1000,
24
- successStatus = 200,
25
- pendingStatus = 202,
26
- resultExtractor = (response) => response,
151
+ timeoutMs = DEFAULT_TIMEOUT_MS,
152
+ initialDelay = DEFAULT_INITIAL_DELAY_MS,
153
+ successStatus = DEFAULT_SUCCESS_STATUS,
154
+ pendingStatus = DEFAULT_PENDING_STATUS,
155
+ resultExtractor = (response) => response as TResult,
27
156
  } = options;
28
157
 
29
- let delay = initialDelay;
158
+ // Validate input parameters
159
+ if (timeoutMs <= 0) {
160
+ throw new ZapierValidationError("Timeout must be greater than 0", {
161
+ details: { timeoutMs },
162
+ });
163
+ }
164
+
165
+ if (initialDelay < 0) {
166
+ throw new ZapierValidationError("Initial delay must be non-negative", {
167
+ details: { initialDelay },
168
+ });
169
+ }
170
+
171
+ const startTime = Date.now();
172
+ let attempts = 0;
30
173
  let errorCount = 0;
31
174
 
32
- for (let attempt = 0; attempt < maxAttempts; attempt++) {
33
- const response = await fetchPoll();
34
-
35
- if (response.status === successStatus) {
36
- // Success - reset error count and return results
37
- errorCount = 0;
38
- const result = await response.json();
39
- return resultExtractor(result);
40
- } else if (response.status === pendingStatus) {
41
- // Still processing - reset error count and wait and retry
42
- errorCount = 0;
43
- if (attempt < maxAttempts - 1) {
44
- await new Promise((resolve) => setTimeout(resolve, delay));
45
- delay = Math.min(delay * 2, maxDelay); // Exponential backoff
46
- continue;
175
+ // Build polling stages with the actual timeout appended
176
+ const pollingStages = [
177
+ ...DEFAULT_POLLING_STAGES,
178
+ [timeoutMs + MAX_TIMEOUT_BUFFER_MS, DEFAULT_MAX_POLLING_INTERVAL_MS], // Up to timeout + 10s: poll every 10s
179
+ ] as const satisfies Array<[number, number]>;
180
+
181
+ // Apply initial delay if specified
182
+ if (initialDelay > 0) {
183
+ await setTimeout(initialDelay);
184
+ }
185
+
186
+ while (true) {
187
+ attempts++;
188
+ const elapsedTime = Date.now() - startTime;
189
+
190
+ // Find the current polling stage
191
+ const pollingInterval = pollingStages.find(
192
+ ([maxTimeForStage, _interval]) => {
193
+ return elapsedTime < maxTimeForStage;
194
+ },
195
+ );
196
+
197
+ // If there isn't a current stage, throw timeout error
198
+ if (!pollingInterval) {
199
+ throw new ZapierTimeoutError(
200
+ `Operation timed out after ${Math.floor(elapsedTime / 1000)}s (${attempts} attempts)`,
201
+ {
202
+ attempts,
203
+ },
204
+ );
205
+ }
206
+
207
+ // Wait before polling (except on first attempt)
208
+ if (attempts > 1) {
209
+ const waitTime = calculateWaitTime(pollingInterval[1], errorCount);
210
+ await setTimeout(waitTime);
211
+ }
212
+
213
+ // Perform the poll request
214
+ try {
215
+ const response = await fetchPoll();
216
+ const {
217
+ result,
218
+ errorCount: newErrorCount,
219
+ status,
220
+ } = await processResponse<TResult>(
221
+ response,
222
+ successStatus,
223
+ pendingStatus,
224
+ resultExtractor,
225
+ errorCount,
226
+ );
227
+ errorCount = newErrorCount;
228
+
229
+ if (status === PollStatus.Success) {
230
+ return result as TResult;
47
231
  }
48
- } else {
49
- // Error occurred - increment error count
50
- errorCount++;
51
232
 
52
- if (errorCount >= 3) {
233
+ if (errorCount >= MAX_CONSECUTIVE_ERRORS) {
53
234
  // Too many consecutive errors, fail
54
235
  throw new ZapierApiError(
55
236
  `Poll request failed: ${response.status} ${response.statusText}`,
56
237
  { statusCode: response.status },
57
238
  );
58
239
  }
59
-
60
- // Treat as pending for up to 3 errors - wait and retry
61
- if (attempt < maxAttempts - 1) {
62
- await new Promise((resolve) => setTimeout(resolve, delay));
63
- delay = Math.min(delay * 2, maxDelay); // Exponential backoff
64
- continue;
240
+ } catch (error) {
241
+ errorCount++;
242
+ if (errorCount >= MAX_CONSECUTIVE_ERRORS) {
243
+ throw new ZapierApiError(
244
+ `Failed to poll after ${errorCount} consecutive errors: ${error instanceof Error ? error.message : String(error)}`,
245
+ {
246
+ cause: error,
247
+ },
248
+ );
65
249
  }
66
250
  }
251
+ // Continue polling if status is pending
67
252
  }
68
-
69
- throw new ZapierTimeoutError(
70
- `Operation timed out after ${maxAttempts} attempts`,
71
- { attempts: maxAttempts, maxAttempts: maxAttempts },
72
- );
73
253
  }
package/src/api/types.ts CHANGED
@@ -88,9 +88,8 @@ export interface RequestOptions {
88
88
  }
89
89
 
90
90
  export interface PollOptions extends RequestOptions {
91
- maxAttempts?: number;
92
91
  initialDelay?: number;
93
- maxDelay?: number;
92
+ timeoutMs?: number;
94
93
  successStatus?: number;
95
94
  pendingStatus?: number;
96
95
  resultExtractor?: (response: unknown) => unknown;
package/src/index.ts CHANGED
@@ -17,7 +17,6 @@ export * from "./plugins/findUniqueAuthentication";
17
17
  export * from "./plugins/runAction";
18
18
  export * from "./plugins/request";
19
19
  export * from "./plugins/manifest";
20
- export * from "./plugins/lockVersion";
21
20
  export * from "./plugins/getProfile";
22
21
  export * from "./plugins/api";
23
22
 
@@ -74,6 +73,7 @@ export type {
74
73
  PluginDependencies,
75
74
  PluginOptions,
76
75
  GetSdkType,
76
+ GetContextType,
77
77
  Sdk,
78
78
  } from "./types/plugin";
79
79
 
@@ -1,5 +1,4 @@
1
- import type { ActionExecutionOptions } from "./types";
2
- import type { ActionProxy } from "./types";
1
+ import type { ActionExecutionOptions, ActionProxy } from "./types";
3
2
  import { ZapierValidationError } from "../../types/errors";
4
3
  import type { Plugin, GetSdkType } from "../../types/plugin";
5
4
  import type { FetchPluginProvides } from "../fetch/index";
@@ -173,3 +172,11 @@ export const appsPlugin: Plugin<
173
172
  apps: createAppsProxy({ sdk }),
174
173
  };
175
174
  };
175
+
176
+ // Export types for use in generated code
177
+ export type { ActionExecutionOptions } from "./types";
178
+ export type { ActionExecutionResult } from "../../api/types";
179
+
180
+ // Interface for generated apps - will be augmented by generated .d.ts files
181
+ // This interface will contain only the specifically typed apps
182
+ export interface ZapierSdkApps {}
@@ -12,8 +12,8 @@ import { manifestPlugin } from "../manifest";
12
12
  function createTestSdk() {
13
13
  return createSdk()
14
14
  .addPlugin(apiPlugin, { fetch: global.fetch })
15
- .addPlugin(listAppsPlugin)
16
15
  .addPlugin(manifestPlugin)
16
+ .addPlugin(listAppsPlugin)
17
17
  .addPlugin(getAppPlugin);
18
18
  }
19
19
 
@@ -5,8 +5,7 @@ import type { GetAppOptions } from "./schemas";
5
5
  import type { AppItem } from "../../types/domain";
6
6
  import { ZapierAppNotFoundError } from "../../types/errors";
7
7
  import type { GetSdkType } from "../../types/plugin";
8
- import type { GetImplementation } from "../manifest/schemas";
9
- import type { ManifestPluginProvides } from "../manifest";
8
+ import type { ListAppsPluginProvides } from "../listApps";
10
9
 
11
10
  // GetApp plugin provides interface - getApp goes directly to SDK root
12
11
  export interface GetAppPluginProvides {
@@ -22,21 +21,20 @@ export interface GetAppPluginProvides {
22
21
 
23
22
  // GetApp plugin depends on listApps SDK function
24
23
  export const getAppPlugin: Plugin<
25
- GetSdkType<ManifestPluginProvides>, // depends on manifest plugin with getImplementation in context
26
- { getImplementation: GetImplementation }, //
24
+ GetSdkType<ListAppsPluginProvides>,
25
+ {},
27
26
  GetAppPluginProvides
28
- > = ({ context }) => {
27
+ > = ({ sdk }) => {
29
28
  const getApp = createFunction(async function getApp(options: GetAppOptions) {
30
- const app = await context.getImplementation(options.appKey);
31
- if (!app) {
32
- throw new ZapierAppNotFoundError("App not found", {
33
- appKey: options.appKey,
34
- });
29
+ const appsIterator = sdk.listApps({ appKeys: [options.appKey] }).items();
30
+ for await (const app of appsIterator) {
31
+ return {
32
+ data: app,
33
+ };
35
34
  }
36
-
37
- return {
38
- data: app,
39
- };
35
+ throw new ZapierAppNotFoundError("App not found", {
36
+ appKey: options.appKey,
37
+ });
40
38
  }, GetAppSchema);
41
39
 
42
40
  // Return flat structure - getApp goes directly to SDK
@@ -45,8 +45,8 @@ describe("getAuthentication plugin", () => {
45
45
  function createTestSdk() {
46
46
  return createSdk()
47
47
  .addPlugin(apiPlugin)
48
- .addPlugin(listAppsPlugin)
49
48
  .addPlugin(manifestPlugin)
49
+ .addPlugin(listAppsPlugin)
50
50
  .addPlugin(getAuthenticationPlugin);
51
51
  }
52
52
 
@@ -68,6 +68,8 @@ describe("listActions plugin", () => {
68
68
  .mockResolvedValue("SlackCLIAPI@1.21.1");
69
69
  });
70
70
 
71
+ const mockResolveAppKeys = vi.fn().mockResolvedValue([]);
72
+
71
73
  function createTestSdk() {
72
74
  // Create a proper plugin chain with context dependencies
73
75
 
@@ -76,18 +78,17 @@ describe("listActions plugin", () => {
76
78
  .addPlugin(() => ({
77
79
  context: {
78
80
  api: mockApiClient,
79
- getVersionedImplementationId: mockGetVersionedImplementationId,
80
81
  },
81
82
  }))
82
- .addPlugin(listAppsPlugin)
83
83
  .addPlugin(() => ({
84
84
  context: {
85
85
  manifest: null,
86
- getManifestEntry: () => null,
87
86
  getVersionedImplementationId: mockGetVersionedImplementationId,
88
- getImplementation: () => Promise.resolve(null),
87
+ resolveAppKeys: mockResolveAppKeys,
88
+ updateManifestEntry: vi.fn().mockResolvedValue(["test-key", {}]),
89
89
  },
90
90
  }))
91
+ .addPlugin(listAppsPlugin)
91
92
  .addPlugin(listActionsPlugin);
92
93
  }
93
94
 
@@ -166,7 +167,7 @@ describe("listActions plugin", () => {
166
167
 
167
168
  expect(result.data).toHaveLength(2); // Only write actions
168
169
  expect(
169
- result.data.every((action) => action.action_type === "write"),
170
+ result.data.every((action: any) => action.action_type === "write"),
170
171
  ).toBe(true);
171
172
  });
172
173
  });
@@ -309,9 +310,9 @@ describe("listActions plugin", () => {
309
310
 
310
311
  expect(result.data).toHaveLength(2);
311
312
  expect(
312
- result.data.every((action) => action.action_type === "write"),
313
+ result.data.every((action: any) => action.action_type === "write"),
313
314
  ).toBe(true);
314
- expect(result.data.map((action) => action.key)).toEqual([
315
+ expect(result.data.map((action: any) => action.key)).toEqual([
315
316
  "send_message",
316
317
  "create_channel",
317
318
  ]);
@@ -32,11 +32,11 @@ export interface ListActionsPluginProvides {
32
32
  }
33
33
 
34
34
  export const listActionsPlugin: Plugin<
35
- GetSdkType<ManifestPluginProvides>, // requires getApp in SDK
35
+ GetSdkType<ManifestPluginProvides>,
36
36
  {
37
37
  api: ApiClient;
38
38
  getVersionedImplementationId: GetVersionedImplementationId;
39
- }, // requires api and getVersionedImplementationId in context
39
+ },
40
40
  ListActionsPluginProvides
41
41
  > = ({ context }) => {
42
42
  const listActions = createPaginatedFunction(async function listActionsPage(
@@ -44,7 +44,7 @@ export const listActionsPlugin: Plugin<
44
44
  ): Promise<ListActionsPage> {
45
45
  const { api, getVersionedImplementationId } = context;
46
46
 
47
- // Use the getApp function from the SDK (dependency injection)
47
+ // Use getVersionedImplementationId (optimized to check manifest first)
48
48
  const selectedApi = await getVersionedImplementationId(options.appKey);
49
49
 
50
50
  if (!selectedApi) {
@@ -28,9 +28,26 @@ const mockAppsResponse = {
28
28
  },
29
29
  };
30
30
 
31
+ const mockResolveAppKeys = vi
32
+ .fn()
33
+ .mockImplementation(async ({ appKeys }: { appKeys: string[] }) => {
34
+ // Mock implementation that returns resolved locators for the app keys
35
+ return appKeys.map((appKey) => ({
36
+ lookupAppKey: appKey,
37
+ implementationName: appKey, // For testing, use appKey as implementationName
38
+ slug: appKey.toLowerCase(),
39
+ version: undefined,
40
+ }));
41
+ });
42
+
31
43
  function createTestSdk() {
32
44
  return createSdk()
33
45
  .addPlugin(apiPlugin, { fetch: global.fetch })
46
+ .addPlugin(() => ({
47
+ context: {
48
+ resolveAppKeys: mockResolveAppKeys,
49
+ },
50
+ }))
34
51
  .addPlugin(listAppsPlugin as any);
35
52
  }
36
53
 
@@ -100,7 +117,11 @@ describe("listApps plugin", () => {
100
117
  expect((context.api as any).get).toHaveBeenCalledWith(
101
118
  "/api/v4/implementations-meta/lookup/",
102
119
  {
103
- searchParams: { latest_only: "true", limit: "100" },
120
+ searchParams: {
121
+ latest_only: "true",
122
+ limit: "100",
123
+ selected_apis: "",
124
+ },
104
125
  },
105
126
  );
106
127
  });
@@ -338,7 +359,7 @@ describe("listApps plugin", () => {
338
359
  expect.stringContaining("implementations-meta"),
339
360
  expect.objectContaining({
340
361
  searchParams: expect.objectContaining({
341
- selected_apis: "SlackCLIAPI,GitHubCLIAPI",
362
+ selected_apis: "SlackCLIAPI@latest,GitHubCLIAPI@latest",
342
363
  }),
343
364
  }),
344
365
  );
@@ -1,16 +1,19 @@
1
- import type { Plugin } from "../../types/plugin";
2
- import type { ApiClient } from "../../api/types";
1
+ import type { GetContextType, Plugin } from "../../types/plugin";
3
2
  import { createPaginatedFunction } from "../../utils/function-utils";
4
3
  import { ListAppsSchema } from "./schemas";
5
4
  import type { ListAppsOptions, ListAppsPage } from "./schemas";
6
5
  import type { AppItem } from "../../types/domain";
6
+ import type { ResolvedAppLocator } from "../../utils/domain-utils";
7
7
  import {
8
- groupAppKeysByType,
9
8
  normalizeImplementationMetaToAppItem,
10
9
  splitVersionedKey,
10
+ toAppLocator,
11
+ toImplementationId,
11
12
  } from "../../utils/domain-utils";
12
13
  import { extractCursor } from "../../utils/function-utils";
13
14
  import type { ImplementationsMetaResponse } from "../../api/types";
15
+ import type { ManifestPluginProvides } from "../manifest";
16
+ import type { ApiPluginProvides } from "../api";
14
17
  // ListApps plugin provides interface - listApps goes directly to SDK root
15
18
  export interface ListAppsPluginProvides {
16
19
  listApps: (options?: ListAppsOptions) => Promise<{ data: AppItem[] }> &
@@ -26,10 +29,9 @@ export interface ListAppsPluginProvides {
26
29
  };
27
30
  }
28
31
 
29
- // Direct plugin function - takes options + sdk + context in one object
30
32
  export const listAppsPlugin: Plugin<
31
- {}, // no SDK dependencies
32
- { api: ApiClient }, // requires api in context
33
+ {},
34
+ GetContextType<ApiPluginProvides & ManifestPluginProvides>,
33
35
  ListAppsPluginProvides
34
36
  > = ({ context }) => {
35
37
  const listApps = createPaginatedFunction(async function listAppsPage(
@@ -38,9 +40,28 @@ export const listAppsPlugin: Plugin<
38
40
  const api = context.api;
39
41
  const opts = options;
40
42
 
41
- const appKeys = [...(opts.appKeys ?? [])].map(
42
- (key) => splitVersionedKey(key)[0],
43
- );
43
+ const appLocators = await context.resolveAppKeys({
44
+ appKeys: [...(opts.appKeys ?? [])],
45
+ });
46
+ const implementationNameToLocator: Record<string, ResolvedAppLocator[]> =
47
+ {};
48
+ for (const locator of appLocators) {
49
+ implementationNameToLocator[locator.implementationName] = [
50
+ ...(implementationNameToLocator[locator.implementationName] ?? []),
51
+ locator,
52
+ ];
53
+ }
54
+ const duplicatedLookupAppKeys = Object.keys(implementationNameToLocator)
55
+ .filter((key) => implementationNameToLocator[key].length > 1)
56
+ .map((key) => implementationNameToLocator[key])
57
+ .flat()
58
+ .map((locator) => locator.lookupAppKey);
59
+
60
+ if (duplicatedLookupAppKeys.length > 0) {
61
+ throw new Error(
62
+ `Duplicate lookup app keys found: ${duplicatedLookupAppKeys.join(", ")}`,
63
+ );
64
+ }
44
65
 
45
66
  if (opts.search) {
46
67
  const searchParams: Record<string, string> = {};
@@ -58,12 +79,18 @@ export const listAppsPlugin: Plugin<
58
79
  normalizeImplementationMetaToAppItem,
59
80
  );
60
81
 
61
- const appKeysSet = new Set(appKeys);
82
+ const implementationNameSet = new Set<string>(
83
+ appLocators.map((locator) => locator.implementationName),
84
+ );
62
85
 
63
86
  for (const implementation of implementations) {
64
- if (!appKeysSet.has(implementation.key)) {
65
- appKeysSet.add(implementation.key);
66
- appKeys.push(implementation.key);
87
+ const [implementationName] = splitVersionedKey(implementation.key);
88
+ if (!implementationNameSet.has(implementationName)) {
89
+ implementationNameSet.add(implementationName);
90
+ appLocators.push({
91
+ ...toAppLocator(implementation.key),
92
+ implementationName,
93
+ });
67
94
  }
68
95
  }
69
96
  }
@@ -74,23 +101,17 @@ export const listAppsPlugin: Plugin<
74
101
  searchParams.limit = opts.pageSize.toString();
75
102
  }
76
103
 
77
- searchParams.latest_only = "true";
104
+ if (appLocators.length === 0) {
105
+ searchParams.latest_only = "true";
106
+ }
78
107
 
79
108
  if (opts.cursor) {
80
109
  searchParams.offset = opts.cursor;
81
110
  }
82
111
 
83
- if (appKeys.length > 0) {
84
- const groupedAppKeys = groupAppKeysByType(appKeys);
85
-
86
- if (groupedAppKeys.selectedApi.length > 0) {
87
- searchParams.selected_apis = groupedAppKeys.selectedApi.join(",");
88
- }
89
-
90
- if (groupedAppKeys.slug.length > 0) {
91
- searchParams.slugs = groupedAppKeys.slug.join(",");
92
- }
93
- }
112
+ searchParams.selected_apis = appLocators
113
+ .map((locator) => toImplementationId(locator))
114
+ .join(",");
94
115
 
95
116
  const implementationsEnvelope: ImplementationsMetaResponse = await api.get(
96
117
  "/api/v4/implementations-meta/lookup/",