@hasna/connectors 1.1.12 → 1.1.13

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/bin/serve.js CHANGED
@@ -19,7 +19,7 @@ var __toESM = (mod, isNodeMode, target) => {
19
19
  var __require = import.meta.require;
20
20
 
21
21
  // src/server/serve.ts
22
- import { existsSync as existsSync4, readdirSync as readdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4 } from "fs";
22
+ import { existsSync as existsSync5, readdirSync as readdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync5 } from "fs";
23
23
 
24
24
  // src/db/database.ts
25
25
  import { Database } from "bun:sqlite";
@@ -118,9 +118,9 @@ function deleteAgent(id, db) {
118
118
  }
119
119
 
120
120
  // src/server/serve.ts
121
- import { join as join5, dirname as dirname3, extname, basename } from "path";
121
+ import { join as join6, dirname as dirname3, extname, basename } from "path";
122
122
  import { fileURLToPath as fileURLToPath3 } from "url";
123
- import { homedir as homedir4 } from "os";
123
+ import { homedir as homedir5 } from "os";
124
124
 
125
125
  // src/lib/registry.ts
126
126
  import { existsSync, readFileSync } from "fs";
@@ -6212,10 +6212,80 @@ function removeConnector(name, targetDir = process.cwd()) {
6212
6212
  }
6213
6213
 
6214
6214
  // src/server/auth.ts
6215
- import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3, readdirSync as readdirSync2, rmSync as rmSync2, statSync as statSync2 } from "fs";
6215
+ import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync4, readdirSync as readdirSync2, rmSync as rmSync2, statSync as statSync2 } from "fs";
6216
6216
  import { randomBytes } from "crypto";
6217
- import { homedir as homedir3 } from "os";
6217
+ import { homedir as homedir4 } from "os";
6218
+ import { join as join5 } from "path";
6219
+
6220
+ // src/lib/lock.ts
6221
+ import { openSync, closeSync, unlinkSync, existsSync as existsSync3 } from "fs";
6218
6222
  import { join as join4 } from "path";
6223
+ import { homedir as homedir3 } from "os";
6224
+ import { mkdirSync as mkdirSync3 } from "fs";
6225
+ var LOCK_TIMEOUT_MS = 5000;
6226
+ var LOCK_RETRY_MS = 100;
6227
+ var STALE_LOCK_MS = 30000;
6228
+
6229
+ class LockTimeoutError extends Error {
6230
+ connector;
6231
+ constructor(connector) {
6232
+ super(`Could not acquire write lock for connector "${connector}" within ${LOCK_TIMEOUT_MS}ms. Another agent may be writing. Try again shortly.`);
6233
+ this.connector = connector;
6234
+ this.name = "LockTimeoutError";
6235
+ }
6236
+ }
6237
+ function lockPath(connector) {
6238
+ const dir = join4(homedir3(), ".connectors", `connect-${connector}`);
6239
+ mkdirSync3(dir, { recursive: true });
6240
+ return join4(dir, ".write.lock");
6241
+ }
6242
+ function isStale(path) {
6243
+ try {
6244
+ const { statSync: statSync2 } = __require("fs");
6245
+ const stat = statSync2(path);
6246
+ return Date.now() - stat.mtimeMs > STALE_LOCK_MS;
6247
+ } catch {
6248
+ return false;
6249
+ }
6250
+ }
6251
+ function tryAcquire(path) {
6252
+ if (existsSync3(path) && isStale(path)) {
6253
+ try {
6254
+ unlinkSync(path);
6255
+ } catch {}
6256
+ }
6257
+ try {
6258
+ const fd = openSync(path, "wx");
6259
+ closeSync(fd);
6260
+ return true;
6261
+ } catch (e) {
6262
+ if (e.code === "EEXIST")
6263
+ return false;
6264
+ throw e;
6265
+ }
6266
+ }
6267
+ function release(path) {
6268
+ try {
6269
+ unlinkSync(path);
6270
+ } catch {}
6271
+ }
6272
+ async function withWriteLock(connector, fn) {
6273
+ const path = lockPath(connector);
6274
+ const deadline = Date.now() + LOCK_TIMEOUT_MS;
6275
+ while (Date.now() < deadline) {
6276
+ if (tryAcquire(path)) {
6277
+ try {
6278
+ return await fn();
6279
+ } finally {
6280
+ release(path);
6281
+ }
6282
+ }
6283
+ await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_MS));
6284
+ }
6285
+ throw new LockTimeoutError(connector);
6286
+ }
6287
+
6288
+ // src/server/auth.ts
6219
6289
  var FETCH_TIMEOUT = 1e4;
6220
6290
  var oauthStateStore = new Map;
6221
6291
  var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
@@ -6271,12 +6341,12 @@ function getAuthType(name) {
6271
6341
  }
6272
6342
  function getConnectorConfigDir(name) {
6273
6343
  const connectorName = name.startsWith("connect-") ? name : `connect-${name}`;
6274
- return join4(homedir3(), ".connectors", connectorName);
6344
+ return join5(homedir4(), ".connectors", connectorName);
6275
6345
  }
6276
6346
  function getCurrentProfile(name) {
6277
6347
  const configDir = getConnectorConfigDir(name);
6278
- const currentProfileFile = join4(configDir, "current_profile");
6279
- if (existsSync3(currentProfileFile)) {
6348
+ const currentProfileFile = join5(configDir, "current_profile");
6349
+ if (existsSync4(currentProfileFile)) {
6280
6350
  try {
6281
6351
  return readFileSync3(currentProfileFile, "utf-8").trim() || "default";
6282
6352
  } catch {
@@ -6290,14 +6360,14 @@ function loadProfileConfig(name) {
6290
6360
  const profile = getCurrentProfile(name);
6291
6361
  let flatConfig = {};
6292
6362
  let dirConfig = {};
6293
- const profileFile = join4(configDir, "profiles", `${profile}.json`);
6294
- if (existsSync3(profileFile)) {
6363
+ const profileFile = join5(configDir, "profiles", `${profile}.json`);
6364
+ if (existsSync4(profileFile)) {
6295
6365
  try {
6296
6366
  flatConfig = JSON.parse(readFileSync3(profileFile, "utf-8"));
6297
6367
  } catch {}
6298
6368
  }
6299
- const profileDirConfig = join4(configDir, "profiles", profile, "config.json");
6300
- if (existsSync3(profileDirConfig)) {
6369
+ const profileDirConfig = join5(configDir, "profiles", profile, "config.json");
6370
+ if (existsSync4(profileDirConfig)) {
6301
6371
  try {
6302
6372
  dirConfig = JSON.parse(readFileSync3(profileDirConfig, "utf-8"));
6303
6373
  } catch {}
@@ -6310,8 +6380,8 @@ function loadProfileConfig(name) {
6310
6380
  function loadTokens(name) {
6311
6381
  const configDir = getConnectorConfigDir(name);
6312
6382
  const profile = getCurrentProfile(name);
6313
- const tokensFile = join4(configDir, "profiles", profile, "tokens.json");
6314
- if (existsSync3(tokensFile)) {
6383
+ const tokensFile = join5(configDir, "profiles", profile, "tokens.json");
6384
+ if (existsSync4(tokensFile)) {
6315
6385
  try {
6316
6386
  return JSON.parse(readFileSync3(tokensFile, "utf-8"));
6317
6387
  } catch {
@@ -6358,15 +6428,18 @@ function getAuthStatus(name) {
6358
6428
  envVarTotalCount
6359
6429
  };
6360
6430
  }
6361
- function saveApiKey(name, key, field) {
6431
+ async function saveApiKey(name, key, field) {
6432
+ return withWriteLock(name, () => _saveApiKey(name, key, field));
6433
+ }
6434
+ function _saveApiKey(name, key, field) {
6362
6435
  const configDir = getConnectorConfigDir(name);
6363
6436
  const profile = getCurrentProfile(name);
6364
6437
  const keyField = field || guessKeyField(name);
6365
6438
  if (keyField === "clientId" || keyField === "clientSecret") {
6366
- const credentialsFile = join4(configDir, "credentials.json");
6367
- mkdirSync3(configDir, { recursive: true });
6439
+ const credentialsFile = join5(configDir, "credentials.json");
6440
+ mkdirSync4(configDir, { recursive: true });
6368
6441
  let creds = {};
6369
- if (existsSync3(credentialsFile)) {
6442
+ if (existsSync4(credentialsFile)) {
6370
6443
  try {
6371
6444
  creds = JSON.parse(readFileSync3(credentialsFile, "utf-8"));
6372
6445
  } catch {}
@@ -6375,9 +6448,9 @@ function saveApiKey(name, key, field) {
6375
6448
  writeFileSync2(credentialsFile, JSON.stringify(creds, null, 2));
6376
6449
  return;
6377
6450
  }
6378
- const profileFile = join4(configDir, "profiles", `${profile}.json`);
6379
- const profileDir = join4(configDir, "profiles", profile);
6380
- if (existsSync3(profileFile)) {
6451
+ const profileFile = join5(configDir, "profiles", `${profile}.json`);
6452
+ const profileDir = join5(configDir, "profiles", profile);
6453
+ if (existsSync4(profileFile)) {
6381
6454
  let config = {};
6382
6455
  try {
6383
6456
  config = JSON.parse(readFileSync3(profileFile, "utf-8"));
@@ -6386,10 +6459,10 @@ function saveApiKey(name, key, field) {
6386
6459
  writeFileSync2(profileFile, JSON.stringify(config, null, 2));
6387
6460
  return;
6388
6461
  }
6389
- if (existsSync3(profileDir)) {
6390
- const configFile = join4(profileDir, "config.json");
6462
+ if (existsSync4(profileDir)) {
6463
+ const configFile = join5(profileDir, "config.json");
6391
6464
  let config = {};
6392
- if (existsSync3(configFile)) {
6465
+ if (existsSync4(configFile)) {
6393
6466
  try {
6394
6467
  config = JSON.parse(readFileSync3(configFile, "utf-8"));
6395
6468
  } catch {}
@@ -6398,8 +6471,8 @@ function saveApiKey(name, key, field) {
6398
6471
  writeFileSync2(configFile, JSON.stringify(config, null, 2));
6399
6472
  return;
6400
6473
  }
6401
- mkdirSync3(profileDir, { recursive: true });
6402
- writeFileSync2(join4(profileDir, "config.json"), JSON.stringify({ [keyField]: key }, null, 2));
6474
+ mkdirSync4(profileDir, { recursive: true });
6475
+ writeFileSync2(join5(profileDir, "config.json"), JSON.stringify({ [keyField]: key }, null, 2));
6403
6476
  }
6404
6477
  function guessKeyField(name) {
6405
6478
  const docs = getConnectorDocs(name);
@@ -6417,8 +6490,8 @@ function guessKeyField(name) {
6417
6490
  }
6418
6491
  function getOAuthConfig(name) {
6419
6492
  const configDir = getConnectorConfigDir(name);
6420
- const credentialsFile = join4(configDir, "credentials.json");
6421
- if (existsSync3(credentialsFile)) {
6493
+ const credentialsFile = join5(configDir, "credentials.json");
6494
+ if (existsSync4(credentialsFile)) {
6422
6495
  try {
6423
6496
  const creds = JSON.parse(readFileSync3(credentialsFile, "utf-8"));
6424
6497
  return { clientId: creds.clientId, clientSecret: creds.clientSecret };
@@ -6499,12 +6572,15 @@ async function exchangeOAuthCode(name, code, redirectUri) {
6499
6572
  function saveOAuthTokens(name, tokens) {
6500
6573
  const configDir = getConnectorConfigDir(name);
6501
6574
  const profile = getCurrentProfile(name);
6502
- const profileDir = join4(configDir, "profiles", profile);
6503
- mkdirSync3(profileDir, { recursive: true });
6504
- const tokensFile = join4(profileDir, "tokens.json");
6575
+ const profileDir = join5(configDir, "profiles", profile);
6576
+ mkdirSync4(profileDir, { recursive: true });
6577
+ const tokensFile = join5(profileDir, "tokens.json");
6505
6578
  writeFileSync2(tokensFile, JSON.stringify(tokens, null, 2), { mode: 384 });
6506
6579
  }
6507
6580
  async function refreshOAuthToken(name) {
6581
+ return withWriteLock(name, () => _refreshOAuthToken(name));
6582
+ }
6583
+ async function _refreshOAuthToken(name) {
6508
6584
  const oauthConfig = getOAuthConfig(name);
6509
6585
  const currentTokens = loadTokens(name);
6510
6586
  if (!oauthConfig.clientId || !oauthConfig.clientSecret) {
@@ -6541,14 +6617,14 @@ async function refreshOAuthToken(name) {
6541
6617
  }
6542
6618
  function listProfiles(name) {
6543
6619
  const configDir = getConnectorConfigDir(name);
6544
- const profilesDir = join4(configDir, "profiles");
6545
- if (!existsSync3(profilesDir))
6620
+ const profilesDir = join5(configDir, "profiles");
6621
+ if (!existsSync4(profilesDir))
6546
6622
  return ["default"];
6547
6623
  const seen = new Set;
6548
6624
  try {
6549
6625
  const entries = readdirSync2(profilesDir);
6550
6626
  for (const entry of entries) {
6551
- const fullPath = join4(profilesDir, entry);
6627
+ const fullPath = join5(profilesDir, entry);
6552
6628
  const stat = statSync2(fullPath);
6553
6629
  if (stat.isDirectory()) {
6554
6630
  seen.add(entry);
@@ -6562,24 +6638,24 @@ function listProfiles(name) {
6562
6638
  }
6563
6639
  function switchProfile(name, profile) {
6564
6640
  const configDir = getConnectorConfigDir(name);
6565
- mkdirSync3(configDir, { recursive: true });
6566
- writeFileSync2(join4(configDir, "current_profile"), profile);
6641
+ mkdirSync4(configDir, { recursive: true });
6642
+ writeFileSync2(join5(configDir, "current_profile"), profile);
6567
6643
  }
6568
6644
  function deleteProfile(name, profile) {
6569
6645
  if (profile === "default")
6570
6646
  return false;
6571
6647
  const configDir = getConnectorConfigDir(name);
6572
- const profilesDir = join4(configDir, "profiles");
6573
- const profileFile = join4(profilesDir, `${profile}.json`);
6574
- if (existsSync3(profileFile)) {
6648
+ const profilesDir = join5(configDir, "profiles");
6649
+ const profileFile = join5(profilesDir, `${profile}.json`);
6650
+ if (existsSync4(profileFile)) {
6575
6651
  rmSync2(profileFile);
6576
6652
  if (getCurrentProfile(name) === profile) {
6577
6653
  switchProfile(name, "default");
6578
6654
  }
6579
6655
  return true;
6580
6656
  }
6581
- const profileDir = join4(profilesDir, profile);
6582
- if (existsSync3(profileDir)) {
6657
+ const profileDir = join5(profilesDir, profile);
6658
+ if (existsSync4(profileDir)) {
6583
6659
  rmSync2(profileDir, { recursive: true });
6584
6660
  if (getCurrentProfile(name) === profile) {
6585
6661
  switchProfile(name, "default");
@@ -6602,20 +6678,20 @@ function resolveDashboardDir() {
6602
6678
  const candidates = [];
6603
6679
  try {
6604
6680
  const scriptDir = dirname3(fileURLToPath3(import.meta.url));
6605
- candidates.push(join5(scriptDir, "..", "dashboard", "dist"));
6606
- candidates.push(join5(scriptDir, "..", "..", "dashboard", "dist"));
6681
+ candidates.push(join6(scriptDir, "..", "dashboard", "dist"));
6682
+ candidates.push(join6(scriptDir, "..", "..", "dashboard", "dist"));
6607
6683
  } catch {}
6608
6684
  if (process.argv[1]) {
6609
6685
  const mainDir = dirname3(process.argv[1]);
6610
- candidates.push(join5(mainDir, "..", "dashboard", "dist"));
6611
- candidates.push(join5(mainDir, "..", "..", "dashboard", "dist"));
6686
+ candidates.push(join6(mainDir, "..", "dashboard", "dist"));
6687
+ candidates.push(join6(mainDir, "..", "..", "dashboard", "dist"));
6612
6688
  }
6613
- candidates.push(join5(process.cwd(), "dashboard", "dist"));
6689
+ candidates.push(join6(process.cwd(), "dashboard", "dist"));
6614
6690
  for (const candidate of candidates) {
6615
- if (existsSync4(candidate))
6691
+ if (existsSync5(candidate))
6616
6692
  return candidate;
6617
6693
  }
6618
- return join5(process.cwd(), "dashboard", "dist");
6694
+ return join6(process.cwd(), "dashboard", "dist");
6619
6695
  }
6620
6696
  var MIME_TYPES = {
6621
6697
  ".html": "text/html; charset=utf-8",
@@ -6705,7 +6781,7 @@ function oauthPage(type, title, message, hint, extra) {
6705
6781
  </body></html>`;
6706
6782
  }
6707
6783
  function serveStaticFile(filePath) {
6708
- if (!existsSync4(filePath))
6784
+ if (!existsSync5(filePath))
6709
6785
  return null;
6710
6786
  const ext = extname(filePath);
6711
6787
  const contentType = MIME_TYPES[ext] || "application/octet-stream";
@@ -6729,7 +6805,7 @@ async function startServer(requestedPort, options) {
6729
6805
  const shouldOpen = options?.open ?? true;
6730
6806
  loadConnectorVersions();
6731
6807
  const dashboardDir = resolveDashboardDir();
6732
- const dashboardExists = existsSync4(dashboardDir);
6808
+ const dashboardExists = existsSync5(dashboardDir);
6733
6809
  if (!dashboardExists) {
6734
6810
  console.error(`
6735
6811
  Dashboard not found at: ${dashboardDir}`);
@@ -6804,7 +6880,7 @@ Dashboard not found at: ${dashboardDir}`);
6804
6880
  const body = await req.json();
6805
6881
  if (!body.key)
6806
6882
  return json({ error: "Missing 'key' in request body" }, 400, port);
6807
- saveApiKey(name, body.key, body.field);
6883
+ await saveApiKey(name, body.key, body.field);
6808
6884
  logActivity("key_saved", name, body.field ? `Field: ${body.field}` : undefined);
6809
6885
  return json({ success: true }, 200, port);
6810
6886
  } catch (e) {
@@ -6910,10 +6986,10 @@ Dashboard not found at: ${dashboardDir}`);
6910
6986
  return json({ error: "Invalid connector name" }, 400, port);
6911
6987
  try {
6912
6988
  const profiles = listProfiles(name);
6913
- const configDir = join5(homedir4(), ".connectors", name.startsWith("connect-") ? name : `connect-${name}`);
6914
- const currentProfileFile = join5(configDir, "current_profile");
6989
+ const configDir = join6(homedir5(), ".connectors", name.startsWith("connect-") ? name : `connect-${name}`);
6990
+ const currentProfileFile = join6(configDir, "current_profile");
6915
6991
  let current = "default";
6916
- if (existsSync4(currentProfileFile)) {
6992
+ if (existsSync5(currentProfileFile)) {
6917
6993
  try {
6918
6994
  current = readFileSync4(currentProfileFile, "utf-8").trim() || "default";
6919
6995
  } catch {}
@@ -6962,16 +7038,16 @@ Dashboard not found at: ${dashboardDir}`);
6962
7038
  }
6963
7039
  if (path === "/api/export" && method === "GET") {
6964
7040
  try {
6965
- const connectDir = join5(homedir4(), ".connectors");
7041
+ const connectDir = join6(homedir5(), ".connectors");
6966
7042
  const result = {};
6967
- if (existsSync4(connectDir)) {
7043
+ if (existsSync5(connectDir)) {
6968
7044
  const entries = readdirSync3(connectDir, { withFileTypes: true });
6969
7045
  for (const entry of entries) {
6970
7046
  if (!entry.isDirectory() || !entry.name.startsWith("connect-"))
6971
7047
  continue;
6972
7048
  const connectorName = entry.name.replace(/^connect-/, "");
6973
- const profilesDir = join5(connectDir, entry.name, "profiles");
6974
- if (!existsSync4(profilesDir))
7049
+ const profilesDir = join6(connectDir, entry.name, "profiles");
7050
+ if (!existsSync5(profilesDir))
6975
7051
  continue;
6976
7052
  const profiles = {};
6977
7053
  const profileEntries = readdirSync3(profilesDir, { withFileTypes: true });
@@ -6979,13 +7055,13 @@ Dashboard not found at: ${dashboardDir}`);
6979
7055
  if (pEntry.isFile() && pEntry.name.endsWith(".json")) {
6980
7056
  const profileName = basename(pEntry.name, ".json");
6981
7057
  try {
6982
- const config = JSON.parse(readFileSync4(join5(profilesDir, pEntry.name), "utf-8"));
7058
+ const config = JSON.parse(readFileSync4(join6(profilesDir, pEntry.name), "utf-8"));
6983
7059
  profiles[profileName] = config;
6984
7060
  } catch {}
6985
7061
  }
6986
7062
  if (pEntry.isDirectory()) {
6987
- const configPath = join5(profilesDir, pEntry.name, "config.json");
6988
- if (existsSync4(configPath)) {
7063
+ const configPath = join6(profilesDir, pEntry.name, "config.json");
7064
+ if (existsSync5(configPath)) {
6989
7065
  try {
6990
7066
  const config = JSON.parse(readFileSync4(configPath, "utf-8"));
6991
7067
  profiles[pEntry.name] = config;
@@ -7022,19 +7098,19 @@ Dashboard not found at: ${dashboardDir}`);
7022
7098
  return json({ error: "Invalid import format: missing 'connectors' object" }, 400, port);
7023
7099
  }
7024
7100
  let imported = 0;
7025
- const connectDir = join5(homedir4(), ".connectors");
7101
+ const connectDir = join6(homedir5(), ".connectors");
7026
7102
  for (const [connectorName, data] of Object.entries(body.connectors)) {
7027
7103
  if (!isValidConnectorName(connectorName))
7028
7104
  continue;
7029
7105
  if (!data.profiles || typeof data.profiles !== "object")
7030
7106
  continue;
7031
- const connectorDir = join5(connectDir, `connect-${connectorName}`);
7032
- const profilesDir = join5(connectorDir, "profiles");
7107
+ const connectorDir = join6(connectDir, `connect-${connectorName}`);
7108
+ const profilesDir = join6(connectorDir, "profiles");
7033
7109
  for (const [profileName, config] of Object.entries(data.profiles)) {
7034
7110
  if (!config || typeof config !== "object")
7035
7111
  continue;
7036
- mkdirSync4(profilesDir, { recursive: true });
7037
- const profileFile = join5(profilesDir, `${profileName}.json`);
7112
+ mkdirSync5(profilesDir, { recursive: true });
7113
+ const profileFile = join6(profilesDir, `${profileName}.json`);
7038
7114
  writeFileSync3(profileFile, JSON.stringify(config, null, 2));
7039
7115
  imported++;
7040
7116
  }
@@ -7090,12 +7166,12 @@ Dashboard not found at: ${dashboardDir}`);
7090
7166
  }
7091
7167
  if (dashboardExists && (method === "GET" || method === "HEAD")) {
7092
7168
  if (path !== "/") {
7093
- const filePath = join5(dashboardDir, path);
7169
+ const filePath = join6(dashboardDir, path);
7094
7170
  const res2 = serveStaticFile(filePath);
7095
7171
  if (res2)
7096
7172
  return res2;
7097
7173
  }
7098
- const indexPath = join5(dashboardDir, "index.html");
7174
+ const indexPath = join6(dashboardDir, "index.html");
7099
7175
  const res = serveStaticFile(indexPath);
7100
7176
  if (res)
7101
7177
  return res;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Advisory file-based write lock for per-connector operations.
3
+ *
4
+ * Prevents concurrent agents from racing on token refresh and config writes.
5
+ * Reads remain unaffected — only write operations (saveApiKey, refreshOAuthToken,
6
+ * switchProfile) acquire the lock.
7
+ *
8
+ * Strategy: atomic O_EXCL file creation as the lock primitive (works on all
9
+ * platforms including macOS). Lock files live at:
10
+ * ~/.connectors/connect-{name}/.write.lock
11
+ *
12
+ * Callers that cannot acquire the lock within the timeout receive a LockTimeoutError.
13
+ */
14
+ export declare class LockTimeoutError extends Error {
15
+ readonly connector: string;
16
+ constructor(connector: string);
17
+ }
18
+ /**
19
+ * Acquire a write lock for a connector, run the callback, then release.
20
+ * Throws LockTimeoutError if the lock cannot be acquired within LOCK_TIMEOUT_MS.
21
+ */
22
+ export declare function withWriteLock<T>(connector: string, fn: () => T | Promise<T>): Promise<T>;
@@ -0,0 +1 @@
1
+ export {};
@@ -42,7 +42,7 @@ export declare function getEnvVars(name: string): {
42
42
  /**
43
43
  * Save an API key to a connector's profile
44
44
  */
45
- export declare function saveApiKey(name: string, key: string, field?: string): void;
45
+ export declare function saveApiKey(name: string, key: string, field?: string): Promise<void>;
46
46
  /**
47
47
  * Get OAuth client credentials for a connector
48
48
  */
@@ -63,7 +63,9 @@ export declare function validateOAuthState(state: string | null, expectedConnect
63
63
  */
64
64
  export declare function exchangeOAuthCode(name: string, code: string, redirectUri: string): Promise<OAuthTokens>;
65
65
  /**
66
- * Refresh an OAuth token using the stored refresh token
66
+ * Refresh an OAuth token using the stored refresh token.
67
+ * Serialized with a per-connector write lock to prevent concurrent agents
68
+ * from racing on token refresh (double-refresh race condition).
67
69
  */
68
70
  export declare function refreshOAuthToken(name: string): Promise<OAuthTokens>;
69
71
  /**
package/package.json CHANGED
@@ -1,7 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/connectors",
3
-
4
- "version": "1.1.12",
3
+ "version": "1.1.13",
5
4
  "description": "Open source connector library - Install API connectors with a single command",
6
5
  "type": "module",
7
6
  "bin": {