@zapier/zapier-sdk 0.22.0 → 0.23.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @zapier/zapier-sdk
2
2
 
3
+ ## 0.23.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 7cf2b12: Add debug logging to zapier-sdk-cli-login. Only allow one token refresh at a time.
8
+
9
+ ## 0.22.1
10
+
11
+ ### Patch Changes
12
+
13
+ - 8ebde4b: Add required scopes for credentials methods.
14
+
3
15
  ## 0.22.0
4
16
 
5
17
  ### Minor Changes
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EACV,SAAS,EACT,gBAAgB,EAGjB,MAAM,SAAS,CAAC;AA0fjB,eAAO,MAAM,eAAe,GAAI,SAAS,gBAAgB,KAAG,SAW3D,CAAC"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EACV,SAAS,EACT,gBAAgB,EAGjB,MAAM,SAAS,CAAC;AA2gBjB,eAAO,MAAM,eAAe,GAAI,SAAS,gBAAgB,KAAG,SAW3D,CAAC"}
@@ -69,18 +69,20 @@ class ZapierApiClient {
69
69
  }
70
70
  }
71
71
  // Helper to get a token from the different places it could be gotten
72
- async getAuthToken() {
72
+ async getAuthToken(options) {
73
73
  return resolveAuthToken({
74
74
  credentials: this.options.credentials,
75
75
  token: this.options.token,
76
76
  onEvent: this.options.onEvent,
77
77
  fetch: this.options.fetch,
78
78
  baseUrl: this.options.baseUrl,
79
+ requiredScopes: options?.requiredScopes,
80
+ debug: this.options.debug,
79
81
  });
80
82
  }
81
83
  // Helper to handle responses
82
84
  async handleResponse(params) {
83
- const { response, customErrorHandler, wasMissingAuthToken } = params;
85
+ const { response, customErrorHandler, wasMissingAuthToken, requiredScopes, } = params;
84
86
  const { data: responseData } = await this.parseResult(response);
85
87
  if (response.ok) {
86
88
  return responseData;
@@ -123,6 +125,7 @@ class ZapierApiClient {
123
125
  await invalidateCredentialsToken({
124
126
  credentials: this.options.credentials,
125
127
  token: this.options.token,
128
+ requiredScopes,
126
129
  });
127
130
  }
128
131
  throw new ZapierAuthenticationError(message, errorOptions);
@@ -237,6 +240,16 @@ class ZapierApiClient {
237
240
  // Find matching path configuration.
238
241
  const matchingPathKey = Object.keys(pathConfig).find((configPath) => path === configPath || path.startsWith(configPath + "/"));
239
242
  const config = matchingPathKey ? pathConfig[matchingPathKey] : undefined;
243
+ // Apply pathPrefix if there's a matching config with pathPrefix.
244
+ let finalPath = path;
245
+ if (config &&
246
+ "pathPrefix" in config &&
247
+ config.pathPrefix &&
248
+ matchingPathKey) {
249
+ // Strip the matching path key, and use the pathPrefix instead.
250
+ const pathWithoutPrefix = path.slice(matchingPathKey.length) || "/";
251
+ finalPath = `${config.pathPrefix}${pathWithoutPrefix}`;
252
+ }
240
253
  // Check if baseUrl is a Zapier-inferred base URL.
241
254
  const zapierBaseUrl = getZapierBaseUrl(this.options.baseUrl);
242
255
  // Let's remain compatible with a base URL that is set to a Zapier-inferred
@@ -246,26 +259,17 @@ class ZapierApiClient {
246
259
  // If baseUrl is already the Zapier base URL, use sdkapi subdomain.
247
260
  const originalBaseUrl = new URL(this.options.baseUrl);
248
261
  const finalBaseUrl = `https://sdkapi.${originalBaseUrl.hostname}`;
249
- // Only prepend pathPrefix if there's a matching config with pathPrefix.
250
- let finalPath = path;
251
- if (config &&
252
- "pathPrefix" in config &&
253
- config.pathPrefix &&
254
- matchingPathKey) {
255
- // Strip the matching path key, and use the pathPrefix instead.
256
- const pathWithoutPrefix = path.slice(matchingPathKey.length) || "/";
257
- finalPath = `${config.pathPrefix}${pathWithoutPrefix}`;
258
- }
259
262
  return {
260
263
  url: new URL(finalPath, finalBaseUrl),
261
264
  pathConfig: config,
262
265
  };
263
266
  }
264
- // For a base URL that isn't a Zapier-inferred domain, use the whole base URL.
267
+ // For a base URL that isn't a Zapier-inferred domain (e.g., localhost),
268
+ // preserve any path from the base URL and append the final path.
265
269
  const baseUrl = new URL(this.options.baseUrl);
266
- const fullPath = baseUrl.pathname.replace(/\/$/, "") + path;
270
+ const basePath = baseUrl.pathname.replace(/\/$/, "");
267
271
  return {
268
- url: new URL(fullPath, baseUrl.origin),
272
+ url: new URL(basePath + finalPath, baseUrl.origin),
269
273
  pathConfig: config,
270
274
  };
271
275
  }
@@ -286,7 +290,9 @@ class ZapierApiClient {
286
290
  // useful context to the API. The session is a good example of this. Auth
287
291
  // is not required, but if we don't add auth, then we won't get the user's
288
292
  // session!
289
- const authToken = await this.getAuthToken();
293
+ const authToken = await this.getAuthToken({
294
+ requiredScopes: options.requiredScopes,
295
+ });
290
296
  if (authToken) {
291
297
  const authHeaderName = pathConfig && pathConfig.authHeader
292
298
  ? pathConfig.authHeader
@@ -310,7 +316,9 @@ class ZapierApiClient {
310
316
  headers["Content-Type"] = "application/json";
311
317
  }
312
318
  // Check if we have an auth token available
313
- const wasMissingAuthToken = options.authRequired && (await this.getAuthToken()) == null;
319
+ const wasMissingAuthToken = options.authRequired &&
320
+ (await this.getAuthToken({ requiredScopes: options.requiredScopes })) ==
321
+ null;
314
322
  const response = await this.plainFetch(path, {
315
323
  ...options,
316
324
  method,
@@ -322,6 +330,7 @@ class ZapierApiClient {
322
330
  response,
323
331
  customErrorHandler: options.customErrorHandler,
324
332
  wasMissingAuthToken,
333
+ requiredScopes: options.requiredScopes,
325
334
  });
326
335
  // Allow empty responses for DELETE or 204 No Content
327
336
  if (typeof result === "string") {
@@ -5,14 +5,16 @@ vi.mock("../auth");
5
5
  describe("ApiClient", () => {
6
6
  const mockResolveAuthToken = vi.mocked(auth.resolveAuthToken);
7
7
  const mockInvalidateCredentialsToken = vi.mocked(auth.invalidateCredentialsToken);
8
+ let mockFetch;
8
9
  beforeEach(() => {
9
10
  vi.clearAllMocks();
10
11
  // Prevent any actual HTTP calls
11
- global.fetch = vi.fn().mockResolvedValue({
12
+ mockFetch = vi.fn().mockResolvedValue({
12
13
  ok: true,
13
14
  status: 200,
14
15
  text: () => Promise.resolve(JSON.stringify({ data: "test" })),
15
16
  });
17
+ global.fetch = mockFetch;
16
18
  });
17
19
  describe("authentication token resolution", () => {
18
20
  it("should pass credentials to resolveAuthToken when provided", async () => {
@@ -35,6 +37,8 @@ describe("ApiClient", () => {
35
37
  onEvent: undefined,
36
38
  fetch: expect.any(Function),
37
39
  baseUrl: "https://api.custom.zapier.dev",
40
+ requiredScopes: undefined,
41
+ debug: false,
38
42
  });
39
43
  });
40
44
  it("should pass undefined credentials when not provided", async () => {
@@ -51,6 +55,8 @@ describe("ApiClient", () => {
51
55
  onEvent: undefined,
52
56
  fetch: expect.any(Function),
53
57
  baseUrl: "https://api.custom.zapier.dev",
58
+ requiredScopes: undefined,
59
+ debug: false,
54
60
  });
55
61
  });
56
62
  it("should pass deprecated token option to resolveAuthToken", async () => {
@@ -69,6 +75,8 @@ describe("ApiClient", () => {
69
75
  onEvent: undefined,
70
76
  fetch: expect.any(Function),
71
77
  baseUrl: "https://api.custom.zapier.dev",
78
+ requiredScopes: undefined,
79
+ debug: false,
72
80
  });
73
81
  });
74
82
  it("should pass string credentials to resolveAuthToken", async () => {
@@ -85,6 +93,8 @@ describe("ApiClient", () => {
85
93
  onEvent: undefined,
86
94
  fetch: expect.any(Function),
87
95
  baseUrl: "https://api.custom.zapier.dev",
96
+ requiredScopes: undefined,
97
+ debug: false,
88
98
  });
89
99
  });
90
100
  it("should pass credentials function to resolveAuthToken", async () => {
@@ -102,6 +112,8 @@ describe("ApiClient", () => {
102
112
  onEvent: undefined,
103
113
  fetch: expect.any(Function),
104
114
  baseUrl: "https://api.custom.zapier.dev",
115
+ requiredScopes: undefined,
116
+ debug: false,
105
117
  });
106
118
  });
107
119
  });
@@ -148,4 +160,72 @@ describe("ApiClient", () => {
148
160
  expect(mockInvalidateCredentialsToken).not.toHaveBeenCalled();
149
161
  });
150
162
  });
163
+ describe("URL path configuration", () => {
164
+ beforeEach(() => {
165
+ mockResolveAuthToken.mockResolvedValue("test-token");
166
+ });
167
+ it("should apply pathPrefix for /relay path with Zapier base URL", async () => {
168
+ const client = createZapierApi({
169
+ baseUrl: "https://zapier.com",
170
+ credentials: "test-token",
171
+ debug: false,
172
+ });
173
+ await client.get("/relay/some/path");
174
+ expect(mockFetch).toHaveBeenCalledWith("https://sdkapi.zapier.com/api/v0/sdk/relay/some/path", expect.any(Object));
175
+ });
176
+ it("should apply pathPrefix for /zapier path with Zapier base URL", async () => {
177
+ const client = createZapierApi({
178
+ baseUrl: "https://zapier.com",
179
+ credentials: "test-token",
180
+ debug: false,
181
+ });
182
+ await client.get("/zapier/apps");
183
+ expect(mockFetch).toHaveBeenCalledWith("https://sdkapi.zapier.com/api/v0/sdk/zapier/apps", expect.any(Object));
184
+ });
185
+ it("should apply pathPrefix for /relay path with localhost base URL", async () => {
186
+ const client = createZapierApi({
187
+ baseUrl: "http://localhost:3000",
188
+ credentials: "test-token",
189
+ debug: false,
190
+ });
191
+ await client.get("/relay/some/path");
192
+ expect(mockFetch).toHaveBeenCalledWith("http://localhost:3000/api/v0/sdk/relay/some/path", expect.any(Object));
193
+ });
194
+ it("should apply pathPrefix for /zapier path with localhost base URL", async () => {
195
+ const client = createZapierApi({
196
+ baseUrl: "http://localhost:3000",
197
+ credentials: "test-token",
198
+ debug: false,
199
+ });
200
+ await client.get("/zapier/apps");
201
+ expect(mockFetch).toHaveBeenCalledWith("http://localhost:3000/api/v0/sdk/zapier/apps", expect.any(Object));
202
+ });
203
+ it("should not apply pathPrefix for non-configured paths", async () => {
204
+ const client = createZapierApi({
205
+ baseUrl: "http://localhost:3000",
206
+ credentials: "test-token",
207
+ debug: false,
208
+ });
209
+ await client.get("/other/path");
210
+ expect(mockFetch).toHaveBeenCalledWith("http://localhost:3000/other/path", expect.any(Object));
211
+ });
212
+ it("should handle exact path match for pathPrefix", async () => {
213
+ const client = createZapierApi({
214
+ baseUrl: "http://localhost:3000",
215
+ credentials: "test-token",
216
+ debug: false,
217
+ });
218
+ await client.get("/relay");
219
+ expect(mockFetch).toHaveBeenCalledWith("http://localhost:3000/api/v0/sdk/relay/", expect.any(Object));
220
+ });
221
+ it("should preserve base URL path for localhost with path component", async () => {
222
+ const client = createZapierApi({
223
+ baseUrl: "http://localhost:3000/a/b/c",
224
+ credentials: "test-token",
225
+ debug: false,
226
+ });
227
+ await client.get("/relay/some/path");
228
+ expect(mockFetch).toHaveBeenCalledWith("http://localhost:3000/a/b/c/api/v0/sdk/relay/some/path", expect.any(Object));
229
+ });
230
+ });
151
231
  });
@@ -43,6 +43,12 @@ export interface RequestOptions {
43
43
  headers?: Record<string, string>;
44
44
  searchParams?: Record<string, string>;
45
45
  authRequired?: boolean;
46
+ /**
47
+ * OAuth scopes required for this request. When using client credentials,
48
+ * these scopes will be requested during token exchange (merged with any
49
+ * scopes specified in the credentials object).
50
+ */
51
+ requiredScopes?: string[];
46
52
  customErrorHandler?: (errorInfo: {
47
53
  status: number;
48
54
  statusText: string;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/api/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/api/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,EACV,oBAAoB,EACpB,6BAA6B,EAC9B,MAAM,oDAAoD,CAAC;AAC5D,OAAO,KAAK,EACV,wBAAwB,EACxB,iCAAiC,EAClC,MAAM,oDAAoD,CAAC;AAC5D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACxD,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAC7B,OAAO,KAAK,EACV,iBAAiB,EACjB,UAAU,EACV,iBAAiB,EACjB,uBAAuB,EACvB,YAAY,EACZ,YAAY,EACZ,WAAW,EACX,2BAA2B,EAC3B,uBAAuB,EACvB,iBAAiB,EACjB,iBAAiB,EACjB,SAAS,EACT,kBAAkB,EAClB,mBAAmB,EACnB,oBAAoB,EACpB,6BAA6B,EAC7B,aAAa,EACb,sBAAsB,EACtB,wBAAwB,EACxB,yBAAyB,EACzB,6BAA6B,EAC7B,8BAA8B,EAC/B,MAAM,WAAW,CAAC;AAMnB,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB;;OAEG;IACH,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B;;OAEG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;IAChC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,QAAQ,KAAK,IAAI,CAAC;CACrC;AAED,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,CAAC,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IACzE,IAAI,EAAE,CAAC,CAAC,GAAG,OAAO,EAChB,IAAI,EAAE,MAAM,EACZ,IAAI,CAAC,EAAE,OAAO,EACd,OAAO,CAAC,EAAE,cAAc,KACrB,OAAO,CAAC,CAAC,CAAC,CAAC;IAChB,GAAG,EAAE,CAAC,CAAC,GAAG,OAAO,EACf,IAAI,EAAE,MAAM,EACZ,IAAI,CAAC,EAAE,OAAO,EACd,OAAO,CAAC,EAAE,cAAc,KACrB,OAAO,CAAC,CAAC,CAAC,CAAC;IAChB,MAAM,EAAE,CAAC,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IAC5E,IAAI,EAAE,CAAC,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IACvE,KAAK,EAAE,CACL,IAAI,EAAE,MAAM,EACZ,IAAI,CAAC,EAAE,WAAW,GAAG;QACnB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QACtC,YAAY,CAAC,EAAE,OAAO,CAAC;KACxB,KACE,OAAO,CAAC,QAAQ,CAAC,CAAC;CACxB;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,kBAAkB,CAAC,EAAE,CAAC,SAAS,EAAE;QAC/B,MAAM,EAAE,MAAM,CAAC;QACf,UAAU,EAAE,MAAM,CAAC;QACnB,IAAI,EAAE,OAAO,CAAC;KACf,KAAK,KAAK,GAAG,SAAS,CAAC;CACzB;AAED,MAAM,WAAW,WAAY,SAAQ,cAAc;IACjD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,eAAe,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,KAAK,OAAO,CAAC;CAClD;AAED,MAAM,WAAW,WAAW;IAC1B,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;CACzC;AAOD,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAC5D,MAAM,MAAM,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC;AAC9C,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAC5D,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AACxE,MAAM,MAAM,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAClD,MAAM,MAAM,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAClD,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAChD,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,2BAA2B,CAAC,CAAC;AAChF,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AACxE,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAG5D,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAClE,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAC3C,OAAO,6BAA6B,CACrC,CAAC;AACF,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAC5D,MAAM,MAAM,GAAG,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,SAAS,CAAC,CAAC;AAC5C,MAAM,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AACpD,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAGtE,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAC9D,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAGhE,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAClE,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAC3C,OAAO,6BAA6B,CACrC,CAAC;AAGF,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAC1E,MAAM,MAAM,2BAA2B,GAAG,CAAC,CAAC,KAAK,CAC/C,OAAO,iCAAiC,CACzC,CAAC;AAGF,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAC1E,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAC5E,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAC3C,OAAO,6BAA6B,CACrC,CAAC;AACF,MAAM,MAAM,wBAAwB,GAAG,CAAC,CAAC,KAAK,CAC5C,OAAO,8BAA8B,CACtC,CAAC"}
package/dist/auth.d.ts CHANGED
@@ -23,15 +23,24 @@ export interface ResolveAuthTokenOptions {
23
23
  onEvent?: EventCallback;
24
24
  fetch?: typeof globalThis.fetch;
25
25
  baseUrl?: string;
26
+ /**
27
+ * Additional OAuth scopes required for this request. When using client
28
+ * credentials, these scopes will be merged with any scopes specified in
29
+ * the credentials object during token exchange.
30
+ */
31
+ requiredScopes?: string[];
32
+ /** Enable debug logging for auth operations. */
33
+ debug?: boolean;
26
34
  }
27
35
  /**
28
36
  * Clear the token cache. Useful for testing or forcing re-authentication.
29
37
  */
30
38
  export declare function clearTokenCache(): void;
31
39
  /**
32
- * Invalidate a cached token. Called when we get a 401 response.
40
+ * Invalidate a cached token for a specific clientId and scope combination.
41
+ * Called when we get a 401 response.
33
42
  */
34
- export declare function invalidateCachedToken(clientId: string): void;
43
+ export declare function invalidateCachedToken(clientId: string, scopes: string[]): void;
35
44
  /**
36
45
  * Options for getTokenFromCliLogin.
37
46
  */
@@ -44,6 +53,7 @@ interface CliLoginOptions {
44
53
  baseUrl?: string;
45
54
  scope?: string;
46
55
  };
56
+ debug?: boolean;
47
57
  }
48
58
  /**
49
59
  * Attempts to get a token by optionally importing from CLI login package.
@@ -77,5 +87,6 @@ export declare function resolveAuthToken(options?: ResolveAuthTokenOptions): Pro
77
87
  export declare function invalidateCredentialsToken(options: {
78
88
  credentials?: Credentials;
79
89
  token?: string;
90
+ requiredScopes?: string[];
80
91
  }): Promise<void>;
81
92
  //# sourceMappingURL=auth.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AACpD,OAAO,KAAK,EAAE,WAAW,EAAuB,MAAM,qBAAqB,CAAC;AAM5E,YAAY,EACV,QAAQ,EACR,SAAS,EACT,QAAQ,EACR,YAAY,EACZ,aAAa,GACd,MAAM,gBAAgB,CAAC;AAGxB,YAAY,EACV,WAAW,EACX,mBAAmB,EACnB,iBAAiB,EACjB,uBAAuB,EACvB,qBAAqB,GACtB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,mBAAmB,EACnB,iBAAiB,EACjB,mBAAmB,EACnB,qBAAqB,GACtB,MAAM,qBAAqB,CAAC;AAE7B;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACtC,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,4CAA4C;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;IAChC,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAsBD;;GAEG;AACH,wBAAgB,eAAe,IAAI,IAAI,CAGtC;AAoCD;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAE5D;AAsFD;;GAEG;AACH,UAAU,eAAe;IACvB,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;IAChC,WAAW,CAAC,EAAE;QACZ,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,QAAQ,EAAE,MAAM,CAAC;QACjB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,CAAC;CACH;AAED;;;;;;GAMG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,eAAe,GACvB,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAgB7B;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,GAAE,uBAA4B,GACpC,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAiB7B;AAuED;;;;GAIG;AACH,wBAAsB,0BAA0B,CAAC,OAAO,EAAE;IACxD,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,GAAG,OAAO,CAAC,IAAI,CAAC,CAMhB"}
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AACpD,OAAO,KAAK,EAAE,WAAW,EAAuB,MAAM,qBAAqB,CAAC;AAM5E,YAAY,EACV,QAAQ,EACR,SAAS,EACT,QAAQ,EACR,YAAY,EACZ,aAAa,GACd,MAAM,gBAAgB,CAAC;AAGxB,YAAY,EACV,WAAW,EACX,mBAAmB,EACnB,iBAAiB,EACjB,uBAAuB,EACvB,qBAAqB,GACtB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,mBAAmB,EACnB,iBAAiB,EACjB,mBAAmB,EACnB,qBAAqB,GACtB,MAAM,qBAAqB,CAAC;AAE7B;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACtC,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,4CAA4C;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;IAChC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,gDAAgD;IAChD,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AA+BD;;GAEG;AACH,wBAAgB,eAAe,IAAI,IAAI,CAGtC;AA0CD;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EAAE,GACf,IAAI,CAIN;AA6HD;;GAEG;AACH,UAAU,eAAe;IACvB,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;IAChC,WAAW,CAAC,EAAE;QACZ,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,QAAQ,EAAE,MAAM,CAAC;QACjB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,CAAC;IACF,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED;;;;;;GAMG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,eAAe,GACvB,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAgB7B;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,GAAE,uBAA4B,GACpC,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAkB7B;AA6ED;;;;GAIG;AACH,wBAAsB,0BAA0B,CAAC,OAAO,EAAE;IACxD,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;CAC3B,GAAG,OAAO,CAAC,IAAI,CAAC,CAQhB"}
package/dist/auth.js CHANGED
@@ -14,14 +14,22 @@ import { ZAPIER_BASE_URL } from "./constants";
14
14
  export { isClientCredentials, isPkceCredentials, isCredentialsObject, isCredentialsFunction, } from "./types/credentials";
15
15
  /**
16
16
  * In-memory cache for tokens obtained via client credentials flow.
17
- * Keyed by clientId.
17
+ * Keyed by clientId + sorted scopes.
18
18
  */
19
19
  const tokenCache = new Map();
20
20
  /**
21
21
  * In-flight token exchange promises to prevent duplicate exchanges.
22
22
  * When an exchange is in progress, subsequent calls await the same promise.
23
+ * Keyed by clientId + sorted scopes.
23
24
  */
24
25
  const pendingExchanges = new Map();
26
+ /**
27
+ * Build a cache key from clientId and scopes.
28
+ */
29
+ function buildCacheKey(clientId, scopes) {
30
+ const sortedScopes = [...scopes].sort().join(",");
31
+ return `${clientId}:${sortedScopes}`;
32
+ }
25
33
  /**
26
34
  * Clear the token cache. Useful for testing or forcing re-authentication.
27
35
  */
@@ -34,8 +42,9 @@ const TOKEN_EXPIRATION_BUFFER = 5 * 60 * 1000; // 5 minutes
34
42
  * Get a cached token if it exists and is not expired.
35
43
  * Returns undefined if no valid cached token exists.
36
44
  */
37
- function getCachedToken(clientId) {
38
- const cached = tokenCache.get(clientId);
45
+ function getCachedToken(clientId, scopes) {
46
+ const cacheKey = buildCacheKey(clientId, scopes);
47
+ const cached = tokenCache.get(cacheKey);
39
48
  if (!cached)
40
49
  return undefined;
41
50
  // Check if token is still valid (with expiration buffer)
@@ -43,23 +52,27 @@ function getCachedToken(clientId) {
43
52
  return cached.accessToken;
44
53
  }
45
54
  // Token expired, remove from cache
46
- tokenCache.delete(clientId);
55
+ tokenCache.delete(cacheKey);
47
56
  return undefined;
48
57
  }
49
58
  /**
50
59
  * Cache a token obtained from client credentials flow.
51
60
  */
52
- function cacheToken(clientId, accessToken, expiresIn) {
53
- tokenCache.set(clientId, {
61
+ function cacheToken(clientId, scopes, accessToken, expiresIn) {
62
+ const cacheKey = buildCacheKey(clientId, scopes);
63
+ tokenCache.set(cacheKey, {
54
64
  accessToken,
55
65
  expiresAt: Date.now() + expiresIn * 1000,
56
66
  });
57
67
  }
58
68
  /**
59
- * Invalidate a cached token. Called when we get a 401 response.
69
+ * Invalidate a cached token for a specific clientId and scope combination.
70
+ * Called when we get a 401 response.
60
71
  */
61
- export function invalidateCachedToken(clientId) {
62
- tokenCache.delete(clientId);
72
+ export function invalidateCachedToken(clientId, scopes) {
73
+ const cacheKey = buildCacheKey(clientId, scopes);
74
+ tokenCache.delete(cacheKey);
75
+ pendingExchanges.delete(cacheKey);
63
76
  }
64
77
  /**
65
78
  * Get the token endpoint URL for client credentials exchange.
@@ -68,13 +81,43 @@ function getTokenEndpointUrl(baseUrl) {
68
81
  const base = baseUrl || ZAPIER_BASE_URL;
69
82
  return `${base}/oauth/token/`;
70
83
  }
84
+ /**
85
+ * Merge and deduplicate scopes from credentials and required scopes.
86
+ * If no credentials scope is specified, defaults to "external".
87
+ * Returns a sorted array of unique scopes.
88
+ */
89
+ function mergeScopes(credentialsScope, requiredScopes) {
90
+ const scopeSet = new Set();
91
+ // Add scopes from credentials (space-separated string)
92
+ // If no credentials scope specified, default to "external"
93
+ if (credentialsScope) {
94
+ for (const s of credentialsScope.split(" ")) {
95
+ if (s.trim()) {
96
+ scopeSet.add(s.trim());
97
+ }
98
+ }
99
+ }
100
+ else {
101
+ scopeSet.add("external");
102
+ }
103
+ // Add required scopes
104
+ if (requiredScopes) {
105
+ for (const s of requiredScopes) {
106
+ scopeSet.add(s);
107
+ }
108
+ }
109
+ return [...scopeSet].sort();
110
+ }
71
111
  /**
72
112
  * Exchange client credentials for an access token.
73
113
  */
74
114
  async function exchangeClientCredentials(options) {
75
- const { clientId, clientSecret, baseUrl, scope, onEvent } = options;
115
+ const { clientId, clientSecret, baseUrl, scope, requiredScopes, onEvent } = options;
76
116
  const fetchFn = options.fetch || globalThis.fetch;
77
117
  const tokenUrl = getTokenEndpointUrl(baseUrl);
118
+ // Merge credentials scope with required scopes
119
+ const mergedScopes = mergeScopes(scope, requiredScopes);
120
+ const scopeString = mergedScopes.join(" ");
78
121
  onEvent?.({
79
122
  type: "auth_exchanging",
80
123
  payload: {
@@ -92,7 +135,7 @@ async function exchangeClientCredentials(options) {
92
135
  grant_type: "client_credentials",
93
136
  client_id: clientId,
94
137
  client_secret: clientSecret,
95
- scope: scope || "external",
138
+ scope: scopeString,
96
139
  audience: "zapier.com",
97
140
  }),
98
141
  });
@@ -113,9 +156,9 @@ async function exchangeClientCredentials(options) {
113
156
  if (!data.access_token) {
114
157
  throw new Error("Client credentials response missing access_token");
115
158
  }
116
- // Cache the token
159
+ // Cache the token with the scopes used
117
160
  const expiresIn = data.expires_in || 3600; // Default to 1 hour
118
- cacheToken(clientId, data.access_token, expiresIn);
161
+ cacheToken(clientId, mergedScopes, data.access_token, expiresIn);
119
162
  onEvent?.({
120
163
  type: "auth_success",
121
164
  payload: {
@@ -177,6 +220,7 @@ export async function resolveAuthToken(options = {}) {
177
220
  return getTokenFromCliLogin({
178
221
  onEvent: options.onEvent,
179
222
  fetch: options.fetch,
223
+ debug: options.debug,
180
224
  });
181
225
  }
182
226
  /**
@@ -190,13 +234,16 @@ async function resolveAuthTokenFromCredentials(credentials, options) {
190
234
  // Client credentials: exchange for token
191
235
  if (isClientCredentials(credentials)) {
192
236
  const { clientId } = credentials;
237
+ // Compute merged scopes for cache lookup
238
+ const mergedScopes = mergeScopes(credentials.scope, options.requiredScopes);
239
+ const cacheKey = buildCacheKey(clientId, mergedScopes);
193
240
  // Check cache first
194
- const cached = getCachedToken(clientId);
241
+ const cached = getCachedToken(clientId, mergedScopes);
195
242
  if (cached) {
196
243
  return cached;
197
244
  }
198
- // Check if there's already an exchange in progress for this clientId
199
- const pending = pendingExchanges.get(clientId);
245
+ // Check if there's already an exchange in progress for this clientId + scopes
246
+ const pending = pendingExchanges.get(cacheKey);
200
247
  if (pending) {
201
248
  return pending;
202
249
  }
@@ -206,13 +253,14 @@ async function resolveAuthTokenFromCredentials(credentials, options) {
206
253
  clientSecret: credentials.clientSecret,
207
254
  baseUrl: credentials.baseUrl || options.baseUrl,
208
255
  scope: credentials.scope,
256
+ requiredScopes: options.requiredScopes,
209
257
  fetch: options.fetch,
210
258
  onEvent: options.onEvent,
211
259
  }).finally(() => {
212
260
  // Remove from pending when done (success or failure)
213
- pendingExchanges.delete(clientId);
261
+ pendingExchanges.delete(cacheKey);
214
262
  });
215
- pendingExchanges.set(clientId, exchangePromise);
263
+ pendingExchanges.set(cacheKey, exchangePromise);
216
264
  return exchangePromise;
217
265
  }
218
266
  // PKCE credentials: delegate to CLI login
@@ -222,6 +270,7 @@ async function resolveAuthTokenFromCredentials(credentials, options) {
222
270
  onEvent: options.onEvent,
223
271
  fetch: options.fetch,
224
272
  credentials,
273
+ debug: options.debug,
225
274
  });
226
275
  if (storedToken) {
227
276
  return storedToken;
@@ -240,8 +289,11 @@ async function resolveAuthTokenFromCredentials(credentials, options) {
240
289
  */
241
290
  export async function invalidateCredentialsToken(options) {
242
291
  const resolved = await resolveCredentials(options);
292
+ if (!resolved)
293
+ return;
243
294
  const clientId = getClientIdFromCredentials(resolved);
244
- if (clientId) {
245
- invalidateCachedToken(clientId);
295
+ if (clientId && isClientCredentials(resolved)) {
296
+ const scopes = mergeScopes(resolved.scope, options.requiredScopes);
297
+ invalidateCachedToken(clientId, scopes);
246
298
  }
247
299
  }
package/dist/auth.test.js CHANGED
@@ -218,6 +218,102 @@ describe("auth", () => {
218
218
  fetch: mockFetch,
219
219
  })).rejects.toThrow("Client credentials exchange failed");
220
220
  });
221
+ it("should merge requiredScopes with credentials scope", async () => {
222
+ mockFetch.mockResolvedValue({
223
+ ok: true,
224
+ json: () => Promise.resolve({
225
+ access_token: "merged-scope-token",
226
+ expires_in: 3600,
227
+ }),
228
+ });
229
+ await auth.resolveAuthToken({
230
+ credentials: {
231
+ type: "client_credentials",
232
+ clientId: "scope-test-client",
233
+ clientSecret: "secret",
234
+ scope: "external",
235
+ },
236
+ requiredScopes: ["credentials"],
237
+ fetch: mockFetch,
238
+ });
239
+ const callArgs = mockFetch.mock.calls[0];
240
+ const body = callArgs[1].body;
241
+ // Scopes should be merged and sorted alphabetically
242
+ expect(body.get("scope")).toBe("credentials external");
243
+ });
244
+ it("should always include external scope with requiredScopes", async () => {
245
+ mockFetch.mockResolvedValue({
246
+ ok: true,
247
+ json: () => Promise.resolve({
248
+ access_token: "required-scope-token",
249
+ expires_in: 3600,
250
+ }),
251
+ });
252
+ await auth.resolveAuthToken({
253
+ credentials: {
254
+ type: "client_credentials",
255
+ clientId: "required-scope-client",
256
+ clientSecret: "secret",
257
+ },
258
+ requiredScopes: ["credentials"],
259
+ fetch: mockFetch,
260
+ });
261
+ const callArgs = mockFetch.mock.calls[0];
262
+ const body = callArgs[1].body;
263
+ // Should include both external (always present) and credentials (required)
264
+ expect(body.get("scope")).toBe("credentials external");
265
+ });
266
+ it("should cache tokens separately for different scope combinations", async () => {
267
+ mockFetch.mockResolvedValue({
268
+ ok: true,
269
+ json: () => Promise.resolve({
270
+ access_token: "first-scope-token",
271
+ expires_in: 3600,
272
+ }),
273
+ });
274
+ // First call with no requiredScopes
275
+ const result1 = await auth.resolveAuthToken({
276
+ credentials: {
277
+ type: "client_credentials",
278
+ clientId: "scope-cache-client",
279
+ clientSecret: "secret",
280
+ },
281
+ fetch: mockFetch,
282
+ });
283
+ expect(result1).toBe("first-scope-token");
284
+ expect(mockFetch).toHaveBeenCalledTimes(1);
285
+ // Second call with different requiredScopes - should NOT use cache
286
+ mockFetch.mockResolvedValue({
287
+ ok: true,
288
+ json: () => Promise.resolve({
289
+ access_token: "second-scope-token",
290
+ expires_in: 3600,
291
+ }),
292
+ });
293
+ const result2 = await auth.resolveAuthToken({
294
+ credentials: {
295
+ type: "client_credentials",
296
+ clientId: "scope-cache-client",
297
+ clientSecret: "secret",
298
+ },
299
+ requiredScopes: ["credentials"],
300
+ fetch: mockFetch,
301
+ });
302
+ expect(result2).toBe("second-scope-token");
303
+ expect(mockFetch).toHaveBeenCalledTimes(2); // Should have made a new request
304
+ // Third call with same requiredScopes as second - should use cache
305
+ const result3 = await auth.resolveAuthToken({
306
+ credentials: {
307
+ type: "client_credentials",
308
+ clientId: "scope-cache-client",
309
+ clientSecret: "secret",
310
+ },
311
+ requiredScopes: ["credentials"],
312
+ fetch: mockFetch,
313
+ });
314
+ expect(result3).toBe("second-scope-token");
315
+ expect(mockFetch).toHaveBeenCalledTimes(2); // No new request
316
+ });
221
317
  });
222
318
  describe("PKCE credentials", () => {
223
319
  it("should delegate PKCE credentials to CLI login", async () => {
@@ -260,8 +356,8 @@ describe("auth", () => {
260
356
  fetch: mockFetch,
261
357
  });
262
358
  expect(mockFetch).toHaveBeenCalledTimes(1);
263
- // Invalidate the cache
264
- auth.invalidateCachedToken("invalidate-test");
359
+ // Invalidate the cache (default scope is "external")
360
+ auth.invalidateCachedToken("invalidate-test", ["external"]);
265
361
  // Next call should fetch again
266
362
  mockFetch.mockResolvedValue({
267
363
  ok: true,