@hasna/connectors 1.3.28 → 1.3.30

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/README.md CHANGED
@@ -8,7 +8,7 @@ Open-source connector platform for enabling, authenticating, and running API con
8
8
  ## Install
9
9
 
10
10
  ```bash
11
- npm install -g @hasna/connectors
11
+ bun install -g @hasna/connectors
12
12
  ```
13
13
 
14
14
  ## What It Is
@@ -56,6 +56,16 @@ connectors-mcp
56
56
  connectors-serve
57
57
  ```
58
58
 
59
+ The local REST API is served by the one-product runtime at
60
+ `http://localhost:9876`. Use `@hasna/connectors-sdk` with
61
+ `ConnectorsClient` or `LocalConnectorsClient` for this local
62
+ `connectors-serve` API.
63
+
64
+ Hosted SaaS products should use `HostedConnectorsClient` from
65
+ `@hasna/connectors-sdk`. The hosted client talks to a platform
66
+ `/api/v1` endpoint with bearer API keys and does not require local connector
67
+ installs or individual connector packages.
68
+
59
69
  ## Project Layout
60
70
 
61
71
  Project-local enablement is lightweight:
package/bin/index.js CHANGED
@@ -1909,7 +1909,7 @@ var package_default;
1909
1909
  var init_package = __esm(() => {
1910
1910
  package_default = {
1911
1911
  name: "@hasna/connectors",
1912
- version: "1.3.28",
1912
+ version: "1.3.30",
1913
1913
  description: "Open source connector library - Install API connectors with a single command",
1914
1914
  type: "module",
1915
1915
  bin: {
@@ -7017,22 +7017,58 @@ async function requestJson(profile, path, params, options = {}) {
7017
7017
  if (value !== undefined && value !== null && value !== "")
7018
7018
  url.searchParams.append(key, String(value));
7019
7019
  }
7020
- const response = await fetch(url, {
7021
- method: options.method ?? "GET",
7022
- headers: {
7023
- Authorization: `Bearer ${token}`,
7024
- Accept: "application/json",
7025
- ...options.body ? { "Content-Type": "application/json" } : {}
7026
- },
7027
- body: options.body ? JSON.stringify(options.body) : undefined
7028
- });
7029
- const text = await response.text();
7030
- const data = text ? JSON.parse(text) : {};
7031
- if (!response.ok) {
7020
+ let lastError;
7021
+ for (let attempt = 0;attempt <= MAX_GMAIL_RETRIES; attempt++) {
7022
+ let response;
7023
+ try {
7024
+ response = await fetch(url, {
7025
+ method: options.method ?? "GET",
7026
+ headers: {
7027
+ Authorization: `Bearer ${token}`,
7028
+ Accept: "application/json",
7029
+ ...options.body ? { "Content-Type": "application/json" } : {}
7030
+ },
7031
+ body: options.body ? JSON.stringify(options.body) : undefined
7032
+ });
7033
+ } catch (error2) {
7034
+ lastError = error2 instanceof Error ? error2.message : String(error2);
7035
+ if (attempt >= MAX_GMAIL_RETRIES)
7036
+ throw error2;
7037
+ await sleep(gmailBackoffDelayMs(attempt));
7038
+ continue;
7039
+ }
7040
+ const text = await response.text();
7041
+ const data = text ? JSON.parse(text) : {};
7042
+ if (response.ok)
7043
+ return data;
7032
7044
  const error = data;
7033
- throw new Error(`Gmail request failed (${response.status}): ${error.error?.message ?? response.statusText}`);
7045
+ lastError = error.error?.message ?? response.statusText;
7046
+ if (!isRetryableGmailResponse(response.status, error) || attempt >= MAX_GMAIL_RETRIES) {
7047
+ throw new Error(`Gmail request failed (${response.status}): ${lastError}`);
7048
+ }
7049
+ await sleep(gmailBackoffDelayMs(attempt, response.headers.get("retry-after")));
7034
7050
  }
7035
- return data;
7051
+ throw new Error(`Gmail request failed: ${lastError ?? "unknown error"}`);
7052
+ }
7053
+ function isRetryableGmailResponse(status, error) {
7054
+ if ([429, 500, 502, 503, 504].includes(status))
7055
+ return true;
7056
+ const reasons = error.error?.errors?.map((entry) => entry.reason) ?? [];
7057
+ return reasons.some((reason) => reason === "rateLimitExceeded" || reason === "userRateLimitExceeded");
7058
+ }
7059
+ function gmailBackoffDelayMs(attempt, retryAfter = null) {
7060
+ if (retryAfter) {
7061
+ const seconds = Number(retryAfter);
7062
+ if (Number.isFinite(seconds) && seconds >= 0)
7063
+ return seconds * 1000;
7064
+ }
7065
+ const base = Number(process.env.CONNECTORS_GMAIL_RETRY_BASE_MS ?? "1000");
7066
+ const baseMs = Number.isFinite(base) && base >= 0 ? base : 1000;
7067
+ const jitterMs = Math.floor(Math.random() * 1000);
7068
+ return Math.min(2 ** attempt * baseMs + jitterMs, 64000);
7069
+ }
7070
+ function sleep(ms) {
7071
+ return new Promise((resolve) => setTimeout(resolve, ms));
7036
7072
  }
7037
7073
  async function getValidAccessToken(profile) {
7038
7074
  if (process.env.GMAIL_ACCESS_TOKEN)
@@ -7210,7 +7246,7 @@ ${input.body}`;
7210
7246
  function safeFilename(filename) {
7211
7247
  return basename(filename.replace(/[\u00A0\u2000-\u200B\u202F\u205F\u3000]/g, " ")).replace(/[\/\\]/g, "_");
7212
7248
  }
7213
- var GMAIL_API_BASE = "https://gmail.googleapis.com/gmail/v1", TOKEN_URL = "https://oauth2.googleapis.com/token", REFRESH_BUFFER_MS, listMessagesSchema, messageIdSchema, readMessageSchema, attachmentListSchema, attachmentDownloadSchema, historyListSchema, replySchema, gmailConnector;
7249
+ var GMAIL_API_BASE = "https://gmail.googleapis.com/gmail/v1", TOKEN_URL = "https://oauth2.googleapis.com/token", REFRESH_BUFFER_MS, MAX_GMAIL_RETRIES = 5, listMessagesSchema, messageIdSchema, readMessageSchema, attachmentListSchema, attachmentDownloadSchema, historyListSchema, replySchema, gmailConnector;
7214
7250
  var init_gmail = __esm(() => {
7215
7251
  init_zod();
7216
7252
  init_connector();
@@ -34282,6 +34318,27 @@ function getOAuthTokenState(name) {
34282
34318
  })();
34283
34319
  return { hasTokens: true, expired: isExpired, expiresIn };
34284
34320
  }
34321
+ function getCurrentOAuthProfile(name, connectorsHome = getConnectorsHome()) {
34322
+ for (const dir of getConnectorConfigReadDirs(name, connectorsHome)) {
34323
+ const currentProfilePath = join20(dir, "current_profile");
34324
+ if (!existsSync20(currentProfilePath))
34325
+ continue;
34326
+ const profile = readFileSync11(currentProfilePath, "utf8").trim();
34327
+ if (profile)
34328
+ return profile;
34329
+ }
34330
+ return "default";
34331
+ }
34332
+ function getOAuthTokenPathsForProfile(name, connectorsHome = getConnectorsHome(), profile = getCurrentOAuthProfile(name, connectorsHome)) {
34333
+ return getConnectorConfigReadDirs(name, connectorsHome).map((dir) => join20(dir, "profiles", profile, "tokens.json"));
34334
+ }
34335
+ function hasOAuthTokenFileUpdatedSince(tokenPaths, sinceMs) {
34336
+ return tokenPaths.some((tokensPath) => {
34337
+ if (!existsSync20(tokensPath))
34338
+ return false;
34339
+ return statSync8(tokensPath).mtimeMs >= sinceMs;
34340
+ });
34341
+ }
34285
34342
  function registerCommands4(program2) {
34286
34343
  program2.command("auth").argument("<connector>", "Connector name to configure auth for").option("-k, --key <value>", "API key or bearer token value (non-interactive)").option("-f, --field <field>", "Which field to set (for multi-field connectors)").option("--json", "Output as JSON", false).option("--no-browser", "Print OAuth URL without opening a browser (agent-friendly)", false).option("--refresh", "Refresh expired OAuth tokens", false).option("--port <port>", "OAuth server port (default: 9876)", "9876").description("Configure authentication for a connector").action(async (connector, options) => {
34287
34344
  const meta = getConnector(connector);
@@ -34407,10 +34464,11 @@ ${meta.displayName} \u2014 Auth Configuration
34407
34464
  console.log(chalk5.dim(`Starting OAuth server on port ${port}...`));
34408
34465
  const { spawn: spawn3 } = await import("child_process");
34409
34466
  const scriptPath = process.argv[1];
34410
- const serverProc = spawn3("node", [scriptPath, "serve", "--port", String(port)], {
34467
+ const serverProc = spawn3(process.execPath, [scriptPath, "serve", "--port", String(port)], {
34411
34468
  detached: true,
34412
34469
  stdio: "ignore"
34413
34470
  });
34471
+ const startedAt = Date.now();
34414
34472
  serverProc.unref();
34415
34473
  await new Promise((resolve) => setTimeout(resolve, 2000));
34416
34474
  try {
@@ -34427,12 +34485,13 @@ ${meta.displayName} \u2014 Auth Configuration
34427
34485
  `);
34428
34486
  console.log(chalk5.dim("Waiting for authentication to complete..."));
34429
34487
  const connectorsHome = getConnectorsHome();
34430
- const tokenPaths = getConnectorConfigReadDirs(connector, connectorsHome).map((dir) => join20(dir, "profiles", "default", "tokens.json"));
34488
+ const activeProfile = getCurrentOAuthProfile(connector, connectorsHome);
34489
+ const tokenPaths = getOAuthTokenPathsForProfile(connector, connectorsHome, activeProfile);
34431
34490
  let attempts = 0;
34432
34491
  const maxAttempts = 360;
34433
34492
  while (attempts < maxAttempts) {
34434
34493
  await new Promise((resolve) => setTimeout(resolve, 500));
34435
- if (tokenPaths.some((tokensPath) => existsSync20(tokensPath))) {
34494
+ if (hasOAuthTokenFileUpdatedSince(tokenPaths, startedAt)) {
34436
34495
  break;
34437
34496
  }
34438
34497
  attempts++;
@@ -35586,7 +35645,6 @@ Testing connector credentials...
35586
35645
  init_registry2();
35587
35646
  init_auth();
35588
35647
  init_runner();
35589
- init_connector_resolver();
35590
35648
  import chalk7 from "chalk";
35591
35649
  function registerCommands6(program2) {
35592
35650
  program2.command("ops").description("List available API operations for a connector").argument("<name>", "Connector name (e.g. stripe, gmail)").argument("[command]", "Get detailed help for a specific subcommand").option("--json", "Output as JSON").action(async (name, command, options) => {
@@ -35750,13 +35808,12 @@ Setting up ${meta.displayName}...
35750
35808
  `);
35751
35809
  const { spawn: spawn3 } = await import("child_process");
35752
35810
  const { getConnectorsHome: getConnectorsHome2 } = await Promise.resolve().then(() => (init_database(), exports_database));
35753
- const { existsSync: existsSync8 } = await import("fs");
35754
- const { join: join8 } = await import("path");
35755
35811
  const scriptPath = process.argv[1];
35756
- const serverProc = spawn3("node", [scriptPath, "serve", "--port", String(port)], {
35812
+ const serverProc = spawn3(process.execPath, [scriptPath, "serve", "--port", String(port)], {
35757
35813
  detached: true,
35758
35814
  stdio: "ignore"
35759
35815
  });
35816
+ const startedAt = Date.now();
35760
35817
  serverProc.unref();
35761
35818
  await new Promise((resolve) => setTimeout(resolve, 2000));
35762
35819
  try {
@@ -35768,12 +35825,13 @@ Setting up ${meta.displayName}...
35768
35825
  }
35769
35826
  console.log(chalk7.dim(" Waiting for authentication to complete..."));
35770
35827
  const connectorsHome = getConnectorsHome2();
35771
- const tokenPaths = getConnectorConfigReadDirs(name, connectorsHome).map((dir) => join8(dir, "profiles", "default", "tokens.json"));
35828
+ const activeProfile = getCurrentOAuthProfile(name, connectorsHome);
35829
+ const tokenPaths = getOAuthTokenPathsForProfile(name, connectorsHome, activeProfile);
35772
35830
  let attempts = 0;
35773
35831
  const maxAttempts = 360;
35774
35832
  while (attempts < maxAttempts) {
35775
35833
  await new Promise((resolve) => setTimeout(resolve, 500));
35776
- if (tokenPaths.some((tokensPath) => existsSync8(tokensPath)))
35834
+ if (hasOAuthTokenFileUpdatedSince(tokenPaths, startedAt))
35777
35835
  break;
35778
35836
  attempts++;
35779
35837
  if (attempts % 6 === 0)
package/bin/mcp.js CHANGED
@@ -11542,22 +11542,58 @@ async function requestJson(profile, path, params, options = {}) {
11542
11542
  if (value !== undefined && value !== null && value !== "")
11543
11543
  url.searchParams.append(key, String(value));
11544
11544
  }
11545
- const response = await fetch(url, {
11546
- method: options.method ?? "GET",
11547
- headers: {
11548
- Authorization: `Bearer ${token}`,
11549
- Accept: "application/json",
11550
- ...options.body ? { "Content-Type": "application/json" } : {}
11551
- },
11552
- body: options.body ? JSON.stringify(options.body) : undefined
11553
- });
11554
- const text = await response.text();
11555
- const data = text ? JSON.parse(text) : {};
11556
- if (!response.ok) {
11545
+ let lastError;
11546
+ for (let attempt = 0;attempt <= MAX_GMAIL_RETRIES; attempt++) {
11547
+ let response;
11548
+ try {
11549
+ response = await fetch(url, {
11550
+ method: options.method ?? "GET",
11551
+ headers: {
11552
+ Authorization: `Bearer ${token}`,
11553
+ Accept: "application/json",
11554
+ ...options.body ? { "Content-Type": "application/json" } : {}
11555
+ },
11556
+ body: options.body ? JSON.stringify(options.body) : undefined
11557
+ });
11558
+ } catch (error3) {
11559
+ lastError = error3 instanceof Error ? error3.message : String(error3);
11560
+ if (attempt >= MAX_GMAIL_RETRIES)
11561
+ throw error3;
11562
+ await sleep(gmailBackoffDelayMs(attempt));
11563
+ continue;
11564
+ }
11565
+ const text = await response.text();
11566
+ const data = text ? JSON.parse(text) : {};
11567
+ if (response.ok)
11568
+ return data;
11557
11569
  const error2 = data;
11558
- throw new Error(`Gmail request failed (${response.status}): ${error2.error?.message ?? response.statusText}`);
11570
+ lastError = error2.error?.message ?? response.statusText;
11571
+ if (!isRetryableGmailResponse(response.status, error2) || attempt >= MAX_GMAIL_RETRIES) {
11572
+ throw new Error(`Gmail request failed (${response.status}): ${lastError}`);
11573
+ }
11574
+ await sleep(gmailBackoffDelayMs(attempt, response.headers.get("retry-after")));
11575
+ }
11576
+ throw new Error(`Gmail request failed: ${lastError ?? "unknown error"}`);
11577
+ }
11578
+ function isRetryableGmailResponse(status, error2) {
11579
+ if ([429, 500, 502, 503, 504].includes(status))
11580
+ return true;
11581
+ const reasons = error2.error?.errors?.map((entry) => entry.reason) ?? [];
11582
+ return reasons.some((reason) => reason === "rateLimitExceeded" || reason === "userRateLimitExceeded");
11583
+ }
11584
+ function gmailBackoffDelayMs(attempt, retryAfter = null) {
11585
+ if (retryAfter) {
11586
+ const seconds = Number(retryAfter);
11587
+ if (Number.isFinite(seconds) && seconds >= 0)
11588
+ return seconds * 1000;
11559
11589
  }
11560
- return data;
11590
+ const base = Number(process.env.CONNECTORS_GMAIL_RETRY_BASE_MS ?? "1000");
11591
+ const baseMs = Number.isFinite(base) && base >= 0 ? base : 1000;
11592
+ const jitterMs = Math.floor(Math.random() * 1000);
11593
+ return Math.min(2 ** attempt * baseMs + jitterMs, 64000);
11594
+ }
11595
+ function sleep(ms) {
11596
+ return new Promise((resolve) => setTimeout(resolve, ms));
11561
11597
  }
11562
11598
  async function getValidAccessToken(profile) {
11563
11599
  if (process.env.GMAIL_ACCESS_TOKEN)
@@ -11735,7 +11771,7 @@ ${input.body}`;
11735
11771
  function safeFilename(filename) {
11736
11772
  return basename(filename.replace(/[\u00A0\u2000-\u200B\u202F\u205F\u3000]/g, " ")).replace(/[\/\\]/g, "_");
11737
11773
  }
11738
- var GMAIL_API_BASE = "https://gmail.googleapis.com/gmail/v1", TOKEN_URL = "https://oauth2.googleapis.com/token", REFRESH_BUFFER_MS, listMessagesSchema, messageIdSchema, readMessageSchema, attachmentListSchema, attachmentDownloadSchema, historyListSchema, replySchema, gmailConnector;
11774
+ var GMAIL_API_BASE = "https://gmail.googleapis.com/gmail/v1", TOKEN_URL = "https://oauth2.googleapis.com/token", REFRESH_BUFFER_MS, MAX_GMAIL_RETRIES = 5, listMessagesSchema, messageIdSchema, readMessageSchema, attachmentListSchema, attachmentDownloadSchema, historyListSchema, replySchema, gmailConnector;
11739
11775
  var init_gmail = __esm(() => {
11740
11776
  init_zod();
11741
11777
  init_connector();
@@ -12240,7 +12276,7 @@ var package_default;
12240
12276
  var init_package = __esm(() => {
12241
12277
  package_default = {
12242
12278
  name: "@hasna/connectors",
12243
- version: "1.3.28",
12279
+ version: "1.3.30",
12244
12280
  description: "Open source connector library - Install API connectors with a single command",
12245
12281
  type: "module",
12246
12282
  bin: {
package/bin/serve.js CHANGED
@@ -15200,6 +15200,7 @@ import { basename, join as join9 } from "path";
15200
15200
  var GMAIL_API_BASE = "https://gmail.googleapis.com/gmail/v1";
15201
15201
  var TOKEN_URL = "https://oauth2.googleapis.com/token";
15202
15202
  var REFRESH_BUFFER_MS = 5 * 60 * 1000;
15203
+ var MAX_GMAIL_RETRIES = 5;
15203
15204
  var listMessagesSchema = exports_external2.object({
15204
15205
  max: exports_external2.coerce.number().int().positive().max(500).optional(),
15205
15206
  maxResults: exports_external2.coerce.number().int().positive().max(500).optional(),
@@ -15430,22 +15431,58 @@ async function requestJson(profile, path, params, options = {}) {
15430
15431
  if (value !== undefined && value !== null && value !== "")
15431
15432
  url.searchParams.append(key, String(value));
15432
15433
  }
15433
- const response = await fetch(url, {
15434
- method: options.method ?? "GET",
15435
- headers: {
15436
- Authorization: `Bearer ${token}`,
15437
- Accept: "application/json",
15438
- ...options.body ? { "Content-Type": "application/json" } : {}
15439
- },
15440
- body: options.body ? JSON.stringify(options.body) : undefined
15441
- });
15442
- const text = await response.text();
15443
- const data = text ? JSON.parse(text) : {};
15444
- if (!response.ok) {
15434
+ let lastError;
15435
+ for (let attempt = 0;attempt <= MAX_GMAIL_RETRIES; attempt++) {
15436
+ let response;
15437
+ try {
15438
+ response = await fetch(url, {
15439
+ method: options.method ?? "GET",
15440
+ headers: {
15441
+ Authorization: `Bearer ${token}`,
15442
+ Accept: "application/json",
15443
+ ...options.body ? { "Content-Type": "application/json" } : {}
15444
+ },
15445
+ body: options.body ? JSON.stringify(options.body) : undefined
15446
+ });
15447
+ } catch (error2) {
15448
+ lastError = error2 instanceof Error ? error2.message : String(error2);
15449
+ if (attempt >= MAX_GMAIL_RETRIES)
15450
+ throw error2;
15451
+ await sleep(gmailBackoffDelayMs(attempt));
15452
+ continue;
15453
+ }
15454
+ const text = await response.text();
15455
+ const data = text ? JSON.parse(text) : {};
15456
+ if (response.ok)
15457
+ return data;
15445
15458
  const error = data;
15446
- throw new Error(`Gmail request failed (${response.status}): ${error.error?.message ?? response.statusText}`);
15459
+ lastError = error.error?.message ?? response.statusText;
15460
+ if (!isRetryableGmailResponse(response.status, error) || attempt >= MAX_GMAIL_RETRIES) {
15461
+ throw new Error(`Gmail request failed (${response.status}): ${lastError}`);
15462
+ }
15463
+ await sleep(gmailBackoffDelayMs(attempt, response.headers.get("retry-after")));
15447
15464
  }
15448
- return data;
15465
+ throw new Error(`Gmail request failed: ${lastError ?? "unknown error"}`);
15466
+ }
15467
+ function isRetryableGmailResponse(status, error) {
15468
+ if ([429, 500, 502, 503, 504].includes(status))
15469
+ return true;
15470
+ const reasons = error.error?.errors?.map((entry) => entry.reason) ?? [];
15471
+ return reasons.some((reason) => reason === "rateLimitExceeded" || reason === "userRateLimitExceeded");
15472
+ }
15473
+ function gmailBackoffDelayMs(attempt, retryAfter = null) {
15474
+ if (retryAfter) {
15475
+ const seconds = Number(retryAfter);
15476
+ if (Number.isFinite(seconds) && seconds >= 0)
15477
+ return seconds * 1000;
15478
+ }
15479
+ const base = Number(process.env.CONNECTORS_GMAIL_RETRY_BASE_MS ?? "1000");
15480
+ const baseMs = Number.isFinite(base) && base >= 0 ? base : 1000;
15481
+ const jitterMs = Math.floor(Math.random() * 1000);
15482
+ return Math.min(2 ** attempt * baseMs + jitterMs, 64000);
15483
+ }
15484
+ function sleep(ms) {
15485
+ return new Promise((resolve) => setTimeout(resolve, ms));
15449
15486
  }
15450
15487
  async function getValidAccessToken(profile) {
15451
15488
  if (process.env.GMAIL_ACCESS_TOKEN)
@@ -15953,7 +15990,7 @@ function extractGoogleError(body) {
15953
15990
  // package.json
15954
15991
  var package_default = {
15955
15992
  name: "@hasna/connectors",
15956
- version: "1.3.28",
15993
+ version: "1.3.30",
15957
15994
  description: "Open source connector library - Install API connectors with a single command",
15958
15995
  type: "module",
15959
15996
  bin: {
@@ -0,0 +1 @@
1
+ export {};
@@ -1,2 +1,5 @@
1
1
  import { Command } from "commander";
2
+ export declare function getCurrentOAuthProfile(name: string, connectorsHome?: string): string;
3
+ export declare function getOAuthTokenPathsForProfile(name: string, connectorsHome?: string, profile?: string): string[];
4
+ export declare function hasOAuthTokenFileUpdatedSince(tokenPaths: string[], sinceMs: number): boolean;
2
5
  export declare function registerCommands(program: Command): void;
package/dist/index.js CHANGED
@@ -4974,6 +4974,7 @@ import { basename, join as join2 } from "path";
4974
4974
  var GMAIL_API_BASE = "https://gmail.googleapis.com/gmail/v1";
4975
4975
  var TOKEN_URL = "https://oauth2.googleapis.com/token";
4976
4976
  var REFRESH_BUFFER_MS = 5 * 60 * 1000;
4977
+ var MAX_GMAIL_RETRIES = 5;
4977
4978
  var listMessagesSchema = exports_external.object({
4978
4979
  max: exports_external.coerce.number().int().positive().max(500).optional(),
4979
4980
  maxResults: exports_external.coerce.number().int().positive().max(500).optional(),
@@ -5204,22 +5205,58 @@ async function requestJson(profile, path, params, options = {}) {
5204
5205
  if (value !== undefined && value !== null && value !== "")
5205
5206
  url.searchParams.append(key, String(value));
5206
5207
  }
5207
- const response = await fetch(url, {
5208
- method: options.method ?? "GET",
5209
- headers: {
5210
- Authorization: `Bearer ${token}`,
5211
- Accept: "application/json",
5212
- ...options.body ? { "Content-Type": "application/json" } : {}
5213
- },
5214
- body: options.body ? JSON.stringify(options.body) : undefined
5215
- });
5216
- const text = await response.text();
5217
- const data = text ? JSON.parse(text) : {};
5218
- if (!response.ok) {
5208
+ let lastError;
5209
+ for (let attempt = 0;attempt <= MAX_GMAIL_RETRIES; attempt++) {
5210
+ let response;
5211
+ try {
5212
+ response = await fetch(url, {
5213
+ method: options.method ?? "GET",
5214
+ headers: {
5215
+ Authorization: `Bearer ${token}`,
5216
+ Accept: "application/json",
5217
+ ...options.body ? { "Content-Type": "application/json" } : {}
5218
+ },
5219
+ body: options.body ? JSON.stringify(options.body) : undefined
5220
+ });
5221
+ } catch (error2) {
5222
+ lastError = error2 instanceof Error ? error2.message : String(error2);
5223
+ if (attempt >= MAX_GMAIL_RETRIES)
5224
+ throw error2;
5225
+ await sleep(gmailBackoffDelayMs(attempt));
5226
+ continue;
5227
+ }
5228
+ const text = await response.text();
5229
+ const data = text ? JSON.parse(text) : {};
5230
+ if (response.ok)
5231
+ return data;
5219
5232
  const error = data;
5220
- throw new Error(`Gmail request failed (${response.status}): ${error.error?.message ?? response.statusText}`);
5233
+ lastError = error.error?.message ?? response.statusText;
5234
+ if (!isRetryableGmailResponse(response.status, error) || attempt >= MAX_GMAIL_RETRIES) {
5235
+ throw new Error(`Gmail request failed (${response.status}): ${lastError}`);
5236
+ }
5237
+ await sleep(gmailBackoffDelayMs(attempt, response.headers.get("retry-after")));
5221
5238
  }
5222
- return data;
5239
+ throw new Error(`Gmail request failed: ${lastError ?? "unknown error"}`);
5240
+ }
5241
+ function isRetryableGmailResponse(status, error) {
5242
+ if ([429, 500, 502, 503, 504].includes(status))
5243
+ return true;
5244
+ const reasons = error.error?.errors?.map((entry) => entry.reason) ?? [];
5245
+ return reasons.some((reason) => reason === "rateLimitExceeded" || reason === "userRateLimitExceeded");
5246
+ }
5247
+ function gmailBackoffDelayMs(attempt, retryAfter = null) {
5248
+ if (retryAfter) {
5249
+ const seconds = Number(retryAfter);
5250
+ if (Number.isFinite(seconds) && seconds >= 0)
5251
+ return seconds * 1000;
5252
+ }
5253
+ const base = Number(process.env.CONNECTORS_GMAIL_RETRY_BASE_MS ?? "1000");
5254
+ const baseMs = Number.isFinite(base) && base >= 0 ? base : 1000;
5255
+ const jitterMs = Math.floor(Math.random() * 1000);
5256
+ return Math.min(2 ** attempt * baseMs + jitterMs, 64000);
5257
+ }
5258
+ function sleep(ms) {
5259
+ return new Promise((resolve) => setTimeout(resolve, ms));
5223
5260
  }
5224
5261
  async function getValidAccessToken(profile) {
5225
5262
  if (process.env.GMAIL_ACCESS_TOKEN)
@@ -5727,7 +5764,7 @@ function extractGoogleError(body) {
5727
5764
  // package.json
5728
5765
  var package_default = {
5729
5766
  name: "@hasna/connectors",
5730
- version: "1.3.28",
5767
+ version: "1.3.30",
5731
5768
  description: "Open source connector library - Install API connectors with a single command",
5732
5769
  type: "module",
5733
5770
  bin: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/connectors",
3
- "version": "1.3.28",
3
+ "version": "1.3.30",
4
4
  "description": "Open source connector library - Install API connectors with a single command",
5
5
  "type": "module",
6
6
  "bin": {