@topolo/sdk 0.11.0 → 0.11.2

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.
@@ -571,6 +571,41 @@ async function resolveCredential(environment = selectedEnvironment()) {
571
571
  return profileForEnvironment(config, environment).credential ?? null;
572
572
  }
573
573
  var REFRESH_SKEW_SECONDS = 60;
574
+ var CONFIG_LOCK_RETRY_MS = 25;
575
+ var CONFIG_LOCK_TIMEOUT_MS = 5e3;
576
+ var CONFIG_LOCK_STALE_MS = 12e4;
577
+ async function withConfigWriteLock(operation) {
578
+ const lockPath = `${configPath()}.lock`;
579
+ await (0, import_promises.mkdir)((0, import_node_path.dirname)(lockPath), { recursive: true });
580
+ const startedAt = Date.now();
581
+ while (true) {
582
+ try {
583
+ await (0, import_promises.mkdir)(lockPath);
584
+ break;
585
+ } catch (error) {
586
+ if (error.code !== "EEXIST") throw error;
587
+ try {
588
+ const lock = await (0, import_promises.stat)(lockPath);
589
+ if (Date.now() - lock.mtimeMs > CONFIG_LOCK_STALE_MS) {
590
+ await (0, import_promises.rm)(lockPath, { recursive: true, force: true });
591
+ continue;
592
+ }
593
+ } catch (inspectionError) {
594
+ if (inspectionError.code !== "ENOENT") throw inspectionError;
595
+ continue;
596
+ }
597
+ if (Date.now() - startedAt >= CONFIG_LOCK_TIMEOUT_MS) {
598
+ throw new Error(`Timed out waiting for credential configuration lock at ${lockPath}.`);
599
+ }
600
+ await new Promise((resolve) => setTimeout(resolve, CONFIG_LOCK_RETRY_MS));
601
+ }
602
+ }
603
+ try {
604
+ return await operation();
605
+ } finally {
606
+ await (0, import_promises.rm)(lockPath, { recursive: true, force: true });
607
+ }
608
+ }
574
609
  async function refreshCredentialIfNeeded(environment = selectedEnvironment(), options = {}) {
575
610
  const prefix = envVarPrefix(environment);
576
611
  if (process.env[`${prefix}_API_KEY`] || process.env[`${prefix}_ACCESS_TOKEN`] || environment === "production" && process.env.TOPOLO_API_KEY || environment === "production" && process.env.TOPOLO_ACCESS_TOKEN) {
@@ -624,51 +659,47 @@ async function refreshCredentialIfNeeded(environment = selectedEnvironment(), op
624
659
  }
625
660
  }
626
661
  async function refreshContextCredentialIfNeeded(environment, key, options = {}) {
627
- const config = await readConfig();
628
- const profile = profileForEnvironment(config, environment);
629
- const context = profile.contexts?.[key];
630
- const credential = context?.credential;
631
- if (!credential || credential.kind !== "access_token") return credential ?? null;
632
- const oauthClientId = context.oauthClientId ?? profile.oauthClientId;
633
- if (!credential.refreshToken || !oauthClientId) return credential;
634
- const now = Math.floor(Date.now() / 1e3);
635
- if (!options.force && credential.expiresAt && credential.expiresAt - REFRESH_SKEW_SECONDS > now) return credential;
636
- const serviceUrls = resolveServiceUrls(profile.serviceUrls, environment);
637
- if (environment === "local" && !serviceUrls?.auth) {
638
- throw new TopoloSdkError(
639
- "environment_service_url_required",
640
- "Local Topolo context refresh requires an explicit local Auth service origin."
641
- );
642
- }
643
- try {
644
- const oauth = new TopoloOAuth({
645
- serviceUrls
646
- });
647
- const tokens = await oauth.refreshToken({
648
- clientId: oauthClientId,
649
- refreshToken: credential.refreshToken
650
- });
651
- const refreshed = {
652
- kind: "access_token",
653
- accessToken: tokens.access_token,
654
- ...tokens.refresh_token ? { refreshToken: tokens.refresh_token } : { refreshToken: credential.refreshToken },
655
- expiresAt: Math.floor(Date.now() / 1e3) + tokens.expires_in
656
- };
657
- await writeConfig(writeProfileConfig(config, environment, {
658
- ...profile.activeContext === key ? { credential: refreshed } : {},
659
- contexts: {
660
- ...profile.contexts ?? {},
661
- [key]: {
662
- ...context,
663
- credential: refreshed,
664
- oauthClientId
662
+ return withConfigWriteLock(async () => {
663
+ const config = await readConfig();
664
+ const profile = profileForEnvironment(config, environment);
665
+ const context = profile.contexts?.[key];
666
+ const credential = context?.credential;
667
+ if (!credential || credential.kind !== "access_token") return credential ?? null;
668
+ const oauthClientId = context.oauthClientId ?? profile.oauthClientId;
669
+ if (!credential.refreshToken || !oauthClientId) return credential;
670
+ const now = Math.floor(Date.now() / 1e3);
671
+ if (!options.force && credential.expiresAt && credential.expiresAt - REFRESH_SKEW_SECONDS > now) return credential;
672
+ const serviceUrls = resolveServiceUrls(profile.serviceUrls, environment);
673
+ if (environment === "local" && !serviceUrls?.auth) {
674
+ throw new TopoloSdkError(
675
+ "environment_service_url_required",
676
+ "Local Topolo context refresh requires an explicit local Auth service origin."
677
+ );
678
+ }
679
+ try {
680
+ const oauth = new TopoloOAuth({ serviceUrls });
681
+ const tokens = await oauth.refreshToken({
682
+ clientId: oauthClientId,
683
+ refreshToken: credential.refreshToken
684
+ });
685
+ const refreshed = {
686
+ kind: "access_token",
687
+ accessToken: tokens.access_token,
688
+ ...tokens.refresh_token ? { refreshToken: tokens.refresh_token } : { refreshToken: credential.refreshToken },
689
+ expiresAt: Math.floor(Date.now() / 1e3) + tokens.expires_in
690
+ };
691
+ await writeConfig(writeProfileConfig(config, environment, {
692
+ ...profile.activeContext === key ? { credential: refreshed } : {},
693
+ contexts: {
694
+ ...profile.contexts ?? {},
695
+ [key]: { ...context, credential: refreshed, oauthClientId }
665
696
  }
666
- }
667
- }));
668
- return refreshed;
669
- } catch {
670
- return credential;
671
- }
697
+ }));
698
+ return refreshed;
699
+ } catch {
700
+ return credential;
701
+ }
702
+ });
672
703
  }
673
704
  function resolveAgentName(configAgentName) {
674
705
  return process.env.TOPOLO_AGENT_NAME ?? configAgentName;
@@ -1,5 +1,5 @@
1
1
  // src/node/credential-store.ts
2
- import { mkdir, readFile, writeFile, chmod } from "fs/promises";
2
+ import { mkdir, readFile, writeFile, chmod, rm, stat } from "fs/promises";
3
3
  import { existsSync } from "fs";
4
4
  import { homedir, platform } from "os";
5
5
  import { dirname, join } from "path";
@@ -524,6 +524,41 @@ async function resolveCredential(environment = selectedEnvironment()) {
524
524
  return profileForEnvironment(config, environment).credential ?? null;
525
525
  }
526
526
  var REFRESH_SKEW_SECONDS = 60;
527
+ var CONFIG_LOCK_RETRY_MS = 25;
528
+ var CONFIG_LOCK_TIMEOUT_MS = 5e3;
529
+ var CONFIG_LOCK_STALE_MS = 12e4;
530
+ async function withConfigWriteLock(operation) {
531
+ const lockPath = `${configPath()}.lock`;
532
+ await mkdir(dirname(lockPath), { recursive: true });
533
+ const startedAt = Date.now();
534
+ while (true) {
535
+ try {
536
+ await mkdir(lockPath);
537
+ break;
538
+ } catch (error) {
539
+ if (error.code !== "EEXIST") throw error;
540
+ try {
541
+ const lock = await stat(lockPath);
542
+ if (Date.now() - lock.mtimeMs > CONFIG_LOCK_STALE_MS) {
543
+ await rm(lockPath, { recursive: true, force: true });
544
+ continue;
545
+ }
546
+ } catch (inspectionError) {
547
+ if (inspectionError.code !== "ENOENT") throw inspectionError;
548
+ continue;
549
+ }
550
+ if (Date.now() - startedAt >= CONFIG_LOCK_TIMEOUT_MS) {
551
+ throw new Error(`Timed out waiting for credential configuration lock at ${lockPath}.`);
552
+ }
553
+ await new Promise((resolve) => setTimeout(resolve, CONFIG_LOCK_RETRY_MS));
554
+ }
555
+ }
556
+ try {
557
+ return await operation();
558
+ } finally {
559
+ await rm(lockPath, { recursive: true, force: true });
560
+ }
561
+ }
527
562
  async function refreshCredentialIfNeeded(environment = selectedEnvironment(), options = {}) {
528
563
  const prefix = envVarPrefix(environment);
529
564
  if (process.env[`${prefix}_API_KEY`] || process.env[`${prefix}_ACCESS_TOKEN`] || environment === "production" && process.env.TOPOLO_API_KEY || environment === "production" && process.env.TOPOLO_ACCESS_TOKEN) {
@@ -577,51 +612,47 @@ async function refreshCredentialIfNeeded(environment = selectedEnvironment(), op
577
612
  }
578
613
  }
579
614
  async function refreshContextCredentialIfNeeded(environment, key, options = {}) {
580
- const config = await readConfig();
581
- const profile = profileForEnvironment(config, environment);
582
- const context = profile.contexts?.[key];
583
- const credential = context?.credential;
584
- if (!credential || credential.kind !== "access_token") return credential ?? null;
585
- const oauthClientId = context.oauthClientId ?? profile.oauthClientId;
586
- if (!credential.refreshToken || !oauthClientId) return credential;
587
- const now = Math.floor(Date.now() / 1e3);
588
- if (!options.force && credential.expiresAt && credential.expiresAt - REFRESH_SKEW_SECONDS > now) return credential;
589
- const serviceUrls = resolveServiceUrls(profile.serviceUrls, environment);
590
- if (environment === "local" && !serviceUrls?.auth) {
591
- throw new TopoloSdkError(
592
- "environment_service_url_required",
593
- "Local Topolo context refresh requires an explicit local Auth service origin."
594
- );
595
- }
596
- try {
597
- const oauth = new TopoloOAuth({
598
- serviceUrls
599
- });
600
- const tokens = await oauth.refreshToken({
601
- clientId: oauthClientId,
602
- refreshToken: credential.refreshToken
603
- });
604
- const refreshed = {
605
- kind: "access_token",
606
- accessToken: tokens.access_token,
607
- ...tokens.refresh_token ? { refreshToken: tokens.refresh_token } : { refreshToken: credential.refreshToken },
608
- expiresAt: Math.floor(Date.now() / 1e3) + tokens.expires_in
609
- };
610
- await writeConfig(writeProfileConfig(config, environment, {
611
- ...profile.activeContext === key ? { credential: refreshed } : {},
612
- contexts: {
613
- ...profile.contexts ?? {},
614
- [key]: {
615
- ...context,
616
- credential: refreshed,
617
- oauthClientId
615
+ return withConfigWriteLock(async () => {
616
+ const config = await readConfig();
617
+ const profile = profileForEnvironment(config, environment);
618
+ const context = profile.contexts?.[key];
619
+ const credential = context?.credential;
620
+ if (!credential || credential.kind !== "access_token") return credential ?? null;
621
+ const oauthClientId = context.oauthClientId ?? profile.oauthClientId;
622
+ if (!credential.refreshToken || !oauthClientId) return credential;
623
+ const now = Math.floor(Date.now() / 1e3);
624
+ if (!options.force && credential.expiresAt && credential.expiresAt - REFRESH_SKEW_SECONDS > now) return credential;
625
+ const serviceUrls = resolveServiceUrls(profile.serviceUrls, environment);
626
+ if (environment === "local" && !serviceUrls?.auth) {
627
+ throw new TopoloSdkError(
628
+ "environment_service_url_required",
629
+ "Local Topolo context refresh requires an explicit local Auth service origin."
630
+ );
631
+ }
632
+ try {
633
+ const oauth = new TopoloOAuth({ serviceUrls });
634
+ const tokens = await oauth.refreshToken({
635
+ clientId: oauthClientId,
636
+ refreshToken: credential.refreshToken
637
+ });
638
+ const refreshed = {
639
+ kind: "access_token",
640
+ accessToken: tokens.access_token,
641
+ ...tokens.refresh_token ? { refreshToken: tokens.refresh_token } : { refreshToken: credential.refreshToken },
642
+ expiresAt: Math.floor(Date.now() / 1e3) + tokens.expires_in
643
+ };
644
+ await writeConfig(writeProfileConfig(config, environment, {
645
+ ...profile.activeContext === key ? { credential: refreshed } : {},
646
+ contexts: {
647
+ ...profile.contexts ?? {},
648
+ [key]: { ...context, credential: refreshed, oauthClientId }
618
649
  }
619
- }
620
- }));
621
- return refreshed;
622
- } catch {
623
- return credential;
624
- }
650
+ }));
651
+ return refreshed;
652
+ } catch {
653
+ return credential;
654
+ }
655
+ });
625
656
  }
626
657
  function resolveAgentName(configAgentName) {
627
658
  return process.env.TOPOLO_AGENT_NAME ?? configAgentName;
package/package.json CHANGED
@@ -1,8 +1,11 @@
1
1
  {
2
2
  "name": "@topolo/sdk",
3
- "version": "0.11.0",
4
- "topoloPublishedVersion": "0.11.0",
3
+ "version": "0.11.2",
4
+ "topoloPublishedVersion": "0.11.2",
5
5
  "description": "Typed client SDK for the Topolo platform. Used by TopoloCli, TopoloMCP, and third-party agents.",
6
+ "license": "UNLICENSED",
7
+ "author": "Topolo",
8
+ "homepage": "https://docs.topolo.app",
6
9
  "type": "module",
7
10
  "main": "./dist/index.js",
8
11
  "module": "./dist/index.js",