@topolo/sdk 0.11.9 → 0.11.11

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/dist/index.cjs CHANGED
@@ -1757,7 +1757,9 @@ var TopoloClient = class _TopoloClient {
1757
1757
  };
1758
1758
  }
1759
1759
  planResolvedAction(entry, input, options) {
1760
- const validation = validateTopoloActionInput(entry.inputSchema, input);
1760
+ const inputValidation = validateTopoloActionInput(entry.inputSchema, input);
1761
+ const resourceIssue = validateActionResource(entry, options.resource);
1762
+ const validation = resourceIssue ? { valid: false, errors: [...inputValidation.errors, resourceIssue] } : inputValidation;
1761
1763
  const agentAccess = entry.agentAccess ?? (entry.destructive ? "off" : entry.readOnly && !entry.requiresConfirmation ? "auto" : "confirm");
1762
1764
  const confirmationRequired = agentAccess === "confirm" || entry.requiresConfirmation || entry.destructive;
1763
1765
  let request = null;
@@ -1816,6 +1818,13 @@ var TopoloClient = class _TopoloClient {
1816
1818
  `Topolo action "${entry.actionId}" input is invalid: ${validation.errors.map((issue) => `${issue.path} ${issue.message}`).join("; ")}`
1817
1819
  );
1818
1820
  }
1821
+ const resourceIssue = validateActionResource(entry, options.resource);
1822
+ if (resourceIssue) {
1823
+ throw new TopoloSdkError(
1824
+ "invalid_action_resource",
1825
+ `Topolo action "${entry.actionId}" resource context is invalid: ${resourceIssue.message}`
1826
+ );
1827
+ }
1819
1828
  const mapped = mapActionInput(entry, input);
1820
1829
  const service = entry.appSlug || entry.appId;
1821
1830
  const result = await this.request({
@@ -1878,6 +1887,32 @@ var TopoloClient = class _TopoloClient {
1878
1887
  };
1879
1888
  }
1880
1889
  };
1890
+ function validateActionResource(entry, resource) {
1891
+ const permissionName = entry.permissionName || entry.requiredPermission.split(".").at(-1) || "";
1892
+ if (!permissionName.startsWith("workspace:")) return null;
1893
+ if (!resource) {
1894
+ return {
1895
+ path: "$.resource",
1896
+ code: "required",
1897
+ message: "Select a workspace with the application resource catalog before planning or calling this action."
1898
+ };
1899
+ }
1900
+ if (resource.resourceType !== "workspace") {
1901
+ return {
1902
+ path: "$.resource.resourceType",
1903
+ code: "const",
1904
+ message: 'This action requires resourceType "workspace".'
1905
+ };
1906
+ }
1907
+ if (!resource.resourceId.trim()) {
1908
+ return {
1909
+ path: "$.resource.resourceId",
1910
+ code: "required",
1911
+ message: "This action requires a workspace resourceId."
1912
+ };
1913
+ }
1914
+ return null;
1915
+ }
1881
1916
  function resolveConfiguredAppId(slug, appIds) {
1882
1917
  for (const serviceKey of serviceKeyCandidates2(slug)) {
1883
1918
  const configured = appIds?.[serviceKey];
package/dist/index.js CHANGED
@@ -1674,7 +1674,9 @@ var TopoloClient = class _TopoloClient {
1674
1674
  };
1675
1675
  }
1676
1676
  planResolvedAction(entry, input, options) {
1677
- const validation = validateTopoloActionInput(entry.inputSchema, input);
1677
+ const inputValidation = validateTopoloActionInput(entry.inputSchema, input);
1678
+ const resourceIssue = validateActionResource(entry, options.resource);
1679
+ const validation = resourceIssue ? { valid: false, errors: [...inputValidation.errors, resourceIssue] } : inputValidation;
1678
1680
  const agentAccess = entry.agentAccess ?? (entry.destructive ? "off" : entry.readOnly && !entry.requiresConfirmation ? "auto" : "confirm");
1679
1681
  const confirmationRequired = agentAccess === "confirm" || entry.requiresConfirmation || entry.destructive;
1680
1682
  let request = null;
@@ -1733,6 +1735,13 @@ var TopoloClient = class _TopoloClient {
1733
1735
  `Topolo action "${entry.actionId}" input is invalid: ${validation.errors.map((issue) => `${issue.path} ${issue.message}`).join("; ")}`
1734
1736
  );
1735
1737
  }
1738
+ const resourceIssue = validateActionResource(entry, options.resource);
1739
+ if (resourceIssue) {
1740
+ throw new TopoloSdkError(
1741
+ "invalid_action_resource",
1742
+ `Topolo action "${entry.actionId}" resource context is invalid: ${resourceIssue.message}`
1743
+ );
1744
+ }
1736
1745
  const mapped = mapActionInput(entry, input);
1737
1746
  const service = entry.appSlug || entry.appId;
1738
1747
  const result = await this.request({
@@ -1795,6 +1804,32 @@ var TopoloClient = class _TopoloClient {
1795
1804
  };
1796
1805
  }
1797
1806
  };
1807
+ function validateActionResource(entry, resource) {
1808
+ const permissionName = entry.permissionName || entry.requiredPermission.split(".").at(-1) || "";
1809
+ if (!permissionName.startsWith("workspace:")) return null;
1810
+ if (!resource) {
1811
+ return {
1812
+ path: "$.resource",
1813
+ code: "required",
1814
+ message: "Select a workspace with the application resource catalog before planning or calling this action."
1815
+ };
1816
+ }
1817
+ if (resource.resourceType !== "workspace") {
1818
+ return {
1819
+ path: "$.resource.resourceType",
1820
+ code: "const",
1821
+ message: 'This action requires resourceType "workspace".'
1822
+ };
1823
+ }
1824
+ if (!resource.resourceId.trim()) {
1825
+ return {
1826
+ path: "$.resource.resourceId",
1827
+ code: "required",
1828
+ message: "This action requires a workspace resourceId."
1829
+ };
1830
+ }
1831
+ return null;
1832
+ }
1798
1833
  function resolveConfiguredAppId(slug, appIds) {
1799
1834
  for (const serviceKey of serviceKeyCandidates2(slug)) {
1800
1835
  const configured = appIds?.[serviceKey];
@@ -612,52 +612,58 @@ async function refreshCredentialIfNeeded(environment = selectedEnvironment(), op
612
612
  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) {
613
613
  return resolveCredential(environment);
614
614
  }
615
- const config = await readConfig();
616
- const profile = profileForEnvironment(config, environment);
617
- const cred = profile.credential;
618
- if (!cred || cred.kind !== "access_token") return cred ?? null;
619
- if (!cred.refreshToken || !profile.oauthClientId) return cred;
620
- const now = Math.floor(Date.now() / 1e3);
621
- if (!options.force && cred.expiresAt && cred.expiresAt - REFRESH_SKEW_SECONDS > now) return cred;
622
- const serviceUrls = resolveServiceUrls(profile.serviceUrls, environment);
623
- if (environment === "local" && !serviceUrls?.auth) {
624
- throw new TopoloSdkError(
625
- "environment_service_url_required",
626
- "Local Topolo credential refresh requires an explicit local Auth service origin."
627
- );
628
- }
629
- try {
630
- const oauth = new TopoloOAuth({
631
- serviceUrls
632
- });
633
- const tokens = await oauth.refreshToken({
634
- clientId: profile.oauthClientId,
635
- refreshToken: cred.refreshToken
636
- });
637
- const refreshed = {
638
- kind: "access_token",
639
- accessToken: tokens.access_token,
640
- ...tokens.refresh_token ? { refreshToken: tokens.refresh_token } : { refreshToken: cred.refreshToken },
641
- expiresAt: Math.floor(Date.now() / 1e3) + tokens.expires_in
642
- };
643
- const activeContext = profile.activeContext;
644
- const activeEntry = activeContext ? profile.contexts?.[activeContext] : void 0;
645
- const updated = activeContext && activeEntry ? writeProfileConfig(config, environment, {
646
- credential: refreshed,
647
- contexts: {
648
- ...profile.contexts ?? {},
649
- [activeContext]: {
650
- ...activeEntry,
651
- credential: refreshed,
652
- oauthClientId: activeEntry.oauthClientId ?? profile.oauthClientId
615
+ const observed = profileForEnvironment(await readConfig(), environment).credential;
616
+ return withConfigWriteLock(async () => {
617
+ const config = await readConfig();
618
+ const profile = profileForEnvironment(config, environment);
619
+ const cred = profile.credential;
620
+ if (!cred || cred.kind !== "access_token") return cred ?? null;
621
+ if (!cred.refreshToken || !profile.oauthClientId) return cred;
622
+ if (options.force && observed?.kind === "access_token" && observed.accessToken !== cred.accessToken) {
623
+ return cred;
624
+ }
625
+ const now = Math.floor(Date.now() / 1e3);
626
+ if (!options.force && cred.expiresAt && cred.expiresAt - REFRESH_SKEW_SECONDS > now) return cred;
627
+ const serviceUrls = resolveServiceUrls(profile.serviceUrls, environment);
628
+ if (environment === "local" && !serviceUrls?.auth) {
629
+ throw new TopoloSdkError(
630
+ "environment_service_url_required",
631
+ "Local Topolo credential refresh requires an explicit local Auth service origin."
632
+ );
633
+ }
634
+ try {
635
+ const oauth = new TopoloOAuth({
636
+ serviceUrls
637
+ });
638
+ const tokens = await oauth.refreshToken({
639
+ clientId: profile.oauthClientId,
640
+ refreshToken: cred.refreshToken
641
+ });
642
+ const refreshed = {
643
+ kind: "access_token",
644
+ accessToken: tokens.access_token,
645
+ ...tokens.refresh_token ? { refreshToken: tokens.refresh_token } : { refreshToken: cred.refreshToken },
646
+ expiresAt: Math.floor(Date.now() / 1e3) + tokens.expires_in
647
+ };
648
+ const activeContext = profile.activeContext;
649
+ const activeEntry = activeContext ? profile.contexts?.[activeContext] : void 0;
650
+ const updated = activeContext && activeEntry ? writeProfileConfig(config, environment, {
651
+ credential: refreshed,
652
+ contexts: {
653
+ ...profile.contexts ?? {},
654
+ [activeContext]: {
655
+ ...activeEntry,
656
+ credential: refreshed,
657
+ oauthClientId: activeEntry.oauthClientId ?? profile.oauthClientId
658
+ }
653
659
  }
654
- }
655
- }) : environment === "production" && !config.profiles?.production ? { ...config, credential: refreshed } : writeProfileConfig(config, environment, { credential: refreshed });
656
- await writeConfig(updated);
657
- return refreshed;
658
- } catch {
659
- return cred;
660
- }
660
+ }) : environment === "production" && !config.profiles?.production ? { ...config, credential: refreshed } : writeProfileConfig(config, environment, { credential: refreshed });
661
+ await writeConfig(updated);
662
+ return refreshed;
663
+ } catch {
664
+ return cred;
665
+ }
666
+ });
661
667
  }
662
668
  async function refreshContextCredentialIfNeeded(environment, key, options = {}) {
663
669
  return withConfigWriteLock(async () => {
@@ -565,52 +565,58 @@ async function refreshCredentialIfNeeded(environment = selectedEnvironment(), op
565
565
  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) {
566
566
  return resolveCredential(environment);
567
567
  }
568
- const config = await readConfig();
569
- const profile = profileForEnvironment(config, environment);
570
- const cred = profile.credential;
571
- if (!cred || cred.kind !== "access_token") return cred ?? null;
572
- if (!cred.refreshToken || !profile.oauthClientId) return cred;
573
- const now = Math.floor(Date.now() / 1e3);
574
- if (!options.force && cred.expiresAt && cred.expiresAt - REFRESH_SKEW_SECONDS > now) return cred;
575
- const serviceUrls = resolveServiceUrls(profile.serviceUrls, environment);
576
- if (environment === "local" && !serviceUrls?.auth) {
577
- throw new TopoloSdkError(
578
- "environment_service_url_required",
579
- "Local Topolo credential refresh requires an explicit local Auth service origin."
580
- );
581
- }
582
- try {
583
- const oauth = new TopoloOAuth({
584
- serviceUrls
585
- });
586
- const tokens = await oauth.refreshToken({
587
- clientId: profile.oauthClientId,
588
- refreshToken: cred.refreshToken
589
- });
590
- const refreshed = {
591
- kind: "access_token",
592
- accessToken: tokens.access_token,
593
- ...tokens.refresh_token ? { refreshToken: tokens.refresh_token } : { refreshToken: cred.refreshToken },
594
- expiresAt: Math.floor(Date.now() / 1e3) + tokens.expires_in
595
- };
596
- const activeContext = profile.activeContext;
597
- const activeEntry = activeContext ? profile.contexts?.[activeContext] : void 0;
598
- const updated = activeContext && activeEntry ? writeProfileConfig(config, environment, {
599
- credential: refreshed,
600
- contexts: {
601
- ...profile.contexts ?? {},
602
- [activeContext]: {
603
- ...activeEntry,
604
- credential: refreshed,
605
- oauthClientId: activeEntry.oauthClientId ?? profile.oauthClientId
568
+ const observed = profileForEnvironment(await readConfig(), environment).credential;
569
+ return withConfigWriteLock(async () => {
570
+ const config = await readConfig();
571
+ const profile = profileForEnvironment(config, environment);
572
+ const cred = profile.credential;
573
+ if (!cred || cred.kind !== "access_token") return cred ?? null;
574
+ if (!cred.refreshToken || !profile.oauthClientId) return cred;
575
+ if (options.force && observed?.kind === "access_token" && observed.accessToken !== cred.accessToken) {
576
+ return cred;
577
+ }
578
+ const now = Math.floor(Date.now() / 1e3);
579
+ if (!options.force && cred.expiresAt && cred.expiresAt - REFRESH_SKEW_SECONDS > now) return cred;
580
+ const serviceUrls = resolveServiceUrls(profile.serviceUrls, environment);
581
+ if (environment === "local" && !serviceUrls?.auth) {
582
+ throw new TopoloSdkError(
583
+ "environment_service_url_required",
584
+ "Local Topolo credential refresh requires an explicit local Auth service origin."
585
+ );
586
+ }
587
+ try {
588
+ const oauth = new TopoloOAuth({
589
+ serviceUrls
590
+ });
591
+ const tokens = await oauth.refreshToken({
592
+ clientId: profile.oauthClientId,
593
+ refreshToken: cred.refreshToken
594
+ });
595
+ const refreshed = {
596
+ kind: "access_token",
597
+ accessToken: tokens.access_token,
598
+ ...tokens.refresh_token ? { refreshToken: tokens.refresh_token } : { refreshToken: cred.refreshToken },
599
+ expiresAt: Math.floor(Date.now() / 1e3) + tokens.expires_in
600
+ };
601
+ const activeContext = profile.activeContext;
602
+ const activeEntry = activeContext ? profile.contexts?.[activeContext] : void 0;
603
+ const updated = activeContext && activeEntry ? writeProfileConfig(config, environment, {
604
+ credential: refreshed,
605
+ contexts: {
606
+ ...profile.contexts ?? {},
607
+ [activeContext]: {
608
+ ...activeEntry,
609
+ credential: refreshed,
610
+ oauthClientId: activeEntry.oauthClientId ?? profile.oauthClientId
611
+ }
606
612
  }
607
- }
608
- }) : environment === "production" && !config.profiles?.production ? { ...config, credential: refreshed } : writeProfileConfig(config, environment, { credential: refreshed });
609
- await writeConfig(updated);
610
- return refreshed;
611
- } catch {
612
- return cred;
613
- }
613
+ }) : environment === "production" && !config.profiles?.production ? { ...config, credential: refreshed } : writeProfileConfig(config, environment, { credential: refreshed });
614
+ await writeConfig(updated);
615
+ return refreshed;
616
+ } catch {
617
+ return cred;
618
+ }
619
+ });
614
620
  }
615
621
  async function refreshContextCredentialIfNeeded(environment, key, options = {}) {
616
622
  return withConfigWriteLock(async () => {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@topolo/sdk",
3
- "version": "0.11.9",
4
- "topoloPublishedVersion": "0.11.9",
3
+ "version": "0.11.11",
4
+ "topoloPublishedVersion": "0.11.11",
5
5
  "description": "Typed client SDK for the Topolo platform. Used by TopoloCli, TopoloMCP, and third-party agents.",
6
6
  "license": "UNLICENSED",
7
7
  "author": "Topolo",