@topolo/sdk 0.4.3 → 0.4.5

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
@@ -47,6 +47,7 @@ __export(index_exports, {
47
47
  defineTopoloRecentActivityWidget: () => defineTopoloRecentActivityWidget,
48
48
  defineTopoloStatsWidget: () => defineTopoloStatsWidget,
49
49
  requirementsForApplication: () => requirementsForApplication,
50
+ resolveCatalogServiceUrl: () => resolveCatalogServiceUrl,
50
51
  resolveServiceUrl: () => resolveServiceUrl,
51
52
  validateTopoloWidgetResponse: () => validateTopoloWidgetResponse
52
53
  });
@@ -2099,17 +2100,90 @@ function statusRank(status) {
2099
2100
 
2100
2101
  // src/services.ts
2101
2102
  var DEFAULT_SERVICE_URLS = SERVICES;
2103
+ function resolveCatalogServiceUrl(entry, options = {}) {
2104
+ const keys = catalogServiceKeyCandidates(entry, options.serviceKey);
2105
+ const override = resolveServiceUrlOverride(keys, options.overrides);
2106
+ if (override) return override;
2107
+ const apiBaseUrl = firstString(entry.apiBaseUrl, entry.api_base_url);
2108
+ if (apiBaseUrl) return apiBaseUrl;
2109
+ const bundledUrl = firstBundledServiceUrl(keys);
2110
+ const liveUrl = firstString(entry.baseUrl);
2111
+ if (bundledUrl && (!liveUrl || sameUrlEnvironment(bundledUrl, liveUrl))) return bundledUrl;
2112
+ return liveUrl ?? bundledUrl ?? null;
2113
+ }
2102
2114
  function resolveServiceUrl(service, overrides) {
2103
- const override = overrides?.[service];
2115
+ const override = resolveServiceUrlOverride([service], overrides);
2104
2116
  if (override) return override;
2105
- const envKey = `TOPOLO_SERVICE_URL_${service.replace(/\./g, "_").toUpperCase()}`;
2106
- const envValue = typeof process !== "undefined" ? process.env?.[envKey] : void 0;
2107
- if (envValue) return envValue;
2108
2117
  return DEFAULT_SERVICE_URLS[service];
2109
2118
  }
2119
+ function catalogServiceKeyCandidates(entry, preferred) {
2120
+ const values = [preferred, entry.slug, entry.name, entry.serviceId].filter(
2121
+ (value) => typeof value === "string" && value.trim().length > 0
2122
+ );
2123
+ const keys = /* @__PURE__ */ new Set();
2124
+ for (const value of values) {
2125
+ for (const candidate of serviceKeyCandidates(value)) keys.add(candidate);
2126
+ }
2127
+ return [...keys];
2128
+ }
2129
+ function serviceKeyCandidates(value) {
2130
+ const normalized = value.trim().toLowerCase().replace(/\s+/g, "-");
2131
+ const withoutTopolo = normalized.startsWith("topolo-") ? normalized.slice("topolo-".length) : normalized;
2132
+ return [.../* @__PURE__ */ new Set([
2133
+ normalized,
2134
+ normalized.replace(/-/g, "_"),
2135
+ withoutTopolo,
2136
+ withoutTopolo.replace(/-/g, "_")
2137
+ ])].filter(Boolean);
2138
+ }
2139
+ function resolveServiceUrlOverride(keys, overrides) {
2140
+ for (const key of keys) {
2141
+ const direct = overrides?.[key];
2142
+ if (direct) return direct;
2143
+ const envStyleKey = key.replace(/\./g, "_");
2144
+ const envStyle = overrides?.[envStyleKey];
2145
+ if (envStyle) return envStyle;
2146
+ }
2147
+ for (const key of keys) {
2148
+ const envKey = `TOPOLO_SERVICE_URL_${key.replace(/\./g, "_").toUpperCase()}`;
2149
+ const envValue = typeof process !== "undefined" ? process.env?.[envKey] : void 0;
2150
+ if (envValue) return envValue;
2151
+ }
2152
+ return null;
2153
+ }
2154
+ function firstBundledServiceUrl(keys) {
2155
+ for (const key of keys) {
2156
+ const url = DEFAULT_SERVICE_URLS[key];
2157
+ if (url) return url;
2158
+ }
2159
+ return null;
2160
+ }
2161
+ function firstString(...values) {
2162
+ for (const value of values) {
2163
+ if (typeof value === "string" && value.trim()) return value.trim();
2164
+ }
2165
+ return null;
2166
+ }
2167
+ function sameUrlEnvironment(left, right) {
2168
+ return urlEnvironment(left) === urlEnvironment(right);
2169
+ }
2170
+ function urlEnvironment(value) {
2171
+ try {
2172
+ const url = new URL(value);
2173
+ const host = url.hostname.toLowerCase();
2174
+ if (host === "localhost" || host === "127.0.0.1") return "local";
2175
+ if (host.endsWith(".stg.topolo.us") || host.includes("topolo-staging")) return "staging";
2176
+ if (host.endsWith(".topolo.app") || host.endsWith(".topolo.us")) return "production";
2177
+ return "external";
2178
+ } catch {
2179
+ return "external";
2180
+ }
2181
+ }
2110
2182
 
2111
2183
  // src/client.ts
2112
2184
  var WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
2185
+ var TOPOLO_ACTION_ID_HEADER = "X-Topolo-Action-Id";
2186
+ var TOPOLO_ACTION_NAME_HEADER = "X-Topolo-Action-Name";
2113
2187
  var POSITIVE_CACHE_TTL_MS = 5 * 60 * 1e3;
2114
2188
  var NEGATIVE_CACHE_TTL_MS = 60 * 1e3;
2115
2189
  var CATALOG_CACHE_TTL_MS = 60 * 1e3;
@@ -2277,8 +2351,12 @@ var TopoloClient = class _TopoloClient {
2277
2351
  async resolveLiveCallableService(serviceKey) {
2278
2352
  const catalog = await this.listServices();
2279
2353
  const match = catalog.find((entry) => catalogEntryMatches(entry, serviceKey));
2280
- if (!match?.baseUrl || !isCallableCatalogEntry(match)) return null;
2281
- return { serviceKey, baseUrl: match.baseUrl };
2354
+ if (!match || !isCallableCatalogEntry(match)) return null;
2355
+ const baseUrl = resolveCatalogServiceUrl(match, {
2356
+ serviceKey,
2357
+ overrides: this.serviceUrls
2358
+ });
2359
+ return baseUrl ? { serviceKey, baseUrl } : null;
2282
2360
  }
2283
2361
  /**
2284
2362
  * Resolve a service-slug → opaque service-id via caller config or TopoloAuth.
@@ -2372,6 +2450,8 @@ var TopoloClient = class _TopoloClient {
2372
2450
  name: typeof s.name === "string" ? s.name : null,
2373
2451
  description: typeof s.description === "string" ? s.description : null,
2374
2452
  baseUrl: typeof s.base_url === "string" ? s.base_url : null,
2453
+ apiBaseUrl: typeof s.api_base_url === "string" ? s.api_base_url : typeof s.apiBaseUrl === "string" ? s.apiBaseUrl : null,
2454
+ launchUrl: typeof s.launch_url === "string" ? s.launch_url : typeof s.launchUrl === "string" ? s.launchUrl : null,
2375
2455
  status: typeof s.status === "string" ? s.status : null,
2376
2456
  surfaceKind: typeof s.surface_kind === "string" ? s.surface_kind : null,
2377
2457
  launchable: s.launchable === true,
@@ -2438,6 +2518,10 @@ var TopoloClient = class _TopoloClient {
2438
2518
  path: mapped.path,
2439
2519
  ...mapped.query ? { query: mapped.query } : {},
2440
2520
  ...mapped.body !== void 0 ? { body: mapped.body } : {},
2521
+ headers: {
2522
+ [TOPOLO_ACTION_ID_HEADER]: entry.actionId,
2523
+ [TOPOLO_ACTION_NAME_HEADER]: entry.name
2524
+ },
2441
2525
  confirm: options.confirm === true
2442
2526
  });
2443
2527
  }
@@ -2453,6 +2537,8 @@ var TopoloClient = class _TopoloClient {
2453
2537
  name: app.name ?? app.id,
2454
2538
  description: null,
2455
2539
  baseUrl: app.productionUrl ?? null,
2540
+ apiBaseUrl: null,
2541
+ launchUrl: app.productionUrl ?? null,
2456
2542
  status: "bundled",
2457
2543
  surfaceKind: "application",
2458
2544
  launchable: Boolean(app.productionUrl),
@@ -2496,7 +2582,7 @@ var TopoloClient = class _TopoloClient {
2496
2582
  }
2497
2583
  };
2498
2584
  function resolveConfiguredServiceId(slug, serviceIds) {
2499
- for (const serviceKey of serviceKeyCandidates(slug)) {
2585
+ for (const serviceKey of serviceKeyCandidates2(slug)) {
2500
2586
  const configured = serviceIds?.[serviceKey];
2501
2587
  if (typeof configured === "string" && configured.trim()) return configured.trim();
2502
2588
  const envKey = `TOPOLO_SERVICE_ID_${serviceKey.replace(/\./g, "_").toUpperCase()}`;
@@ -2506,7 +2592,7 @@ function resolveConfiguredServiceId(slug, serviceIds) {
2506
2592
  return null;
2507
2593
  }
2508
2594
  function resolveConfiguredServiceUrl(slug, serviceUrls) {
2509
- for (const serviceKey of serviceKeyCandidates(slug)) {
2595
+ for (const serviceKey of serviceKeyCandidates2(slug)) {
2510
2596
  const configured = serviceUrls?.[serviceKey];
2511
2597
  if (typeof configured === "string" && configured.trim()) return configured.trim();
2512
2598
  const envKey = `TOPOLO_SERVICE_URL_${serviceKey.replace(/\./g, "_").toUpperCase()}`;
@@ -2515,7 +2601,7 @@ function resolveConfiguredServiceUrl(slug, serviceUrls) {
2515
2601
  }
2516
2602
  return null;
2517
2603
  }
2518
- function serviceKeyCandidates(slug) {
2604
+ function serviceKeyCandidates2(slug) {
2519
2605
  const normalized = normalizeLookupKey(slug);
2520
2606
  const withoutTopolo = normalized.startsWith("topolo-") ? normalized.slice("topolo-".length) : normalized;
2521
2607
  return [.../* @__PURE__ */ new Set([
@@ -2537,7 +2623,7 @@ function catalogEntryLookupKeys(entry) {
2537
2623
  for (const value of values) {
2538
2624
  const normalized = normalizeLookupKey(value);
2539
2625
  if (!normalized) continue;
2540
- for (const candidate of serviceKeyCandidates(normalized)) {
2626
+ for (const candidate of serviceKeyCandidates2(normalized)) {
2541
2627
  keys.add(candidate);
2542
2628
  }
2543
2629
  }
@@ -2547,7 +2633,7 @@ function isCallableCatalogEntry(entry) {
2547
2633
  const kind = normalizeLookupKey(entry.surfaceKind || "");
2548
2634
  if (kind === "api" || kind === "technical_service" || kind === "service") return true;
2549
2635
  return Boolean(
2550
- entry.baseUrl && catalogEntryLookupKeys(entry).some((key) => Object.prototype.hasOwnProperty.call(DEFAULT_SERVICE_URLS, key))
2636
+ (entry.baseUrl || entry.apiBaseUrl) && catalogEntryLookupKeys(entry).some((key) => Object.prototype.hasOwnProperty.call(DEFAULT_SERVICE_URLS, key))
2551
2637
  );
2552
2638
  }
2553
2639
  function normalizeActionCatalogEntry(item) {
@@ -3228,6 +3314,7 @@ function createTopolo(options) {
3228
3314
  defineTopoloRecentActivityWidget,
3229
3315
  defineTopoloStatsWidget,
3230
3316
  requirementsForApplication,
3317
+ resolveCatalogServiceUrl,
3231
3318
  resolveServiceUrl,
3232
3319
  validateTopoloWidgetResponse
3233
3320
  });
package/dist/index.d.cts CHANGED
@@ -105,6 +105,8 @@ interface TopoloServiceCatalogEntry {
105
105
  name: string | null;
106
106
  description: string | null;
107
107
  baseUrl: string | null;
108
+ apiBaseUrl?: string | null;
109
+ launchUrl?: string | null;
108
110
  status: string | null;
109
111
  surfaceKind: string | null;
110
112
  launchable: boolean;
@@ -1579,6 +1581,20 @@ declare function auditAllApplicationRequirements(applicationIds?: readonly Appli
1579
1581
  */
1580
1582
 
1581
1583
  declare const DEFAULT_SERVICE_URLS: Record<ServiceId, string>;
1584
+ interface ServiceCatalogUrlEntry {
1585
+ serviceId?: string | null;
1586
+ slug?: string | null;
1587
+ name?: string | null;
1588
+ baseUrl?: string | null;
1589
+ apiBaseUrl?: string | null;
1590
+ launchUrl?: string | null;
1591
+ api_base_url?: string | null;
1592
+ launch_url?: string | null;
1593
+ }
1594
+ declare function resolveCatalogServiceUrl(entry: ServiceCatalogUrlEntry, options?: {
1595
+ serviceKey?: string;
1596
+ overrides?: Record<string, string> | Partial<Record<ServiceId, string>>;
1597
+ }): string | null;
1582
1598
  declare function resolveServiceUrl(service: ServiceId, overrides?: Partial<Record<ServiceId, string>>): string;
1583
1599
 
1584
1600
  declare class TopoloSdkError extends Error {
@@ -2085,4 +2101,4 @@ declare function createTopolo(options: TopoloClientOptions): {
2085
2101
  };
2086
2102
  type Topolo = ReturnType<typeof createTopolo>;
2087
2103
 
2088
- export { APPLICATIONS, APPLICATION_REQUIREMENTS, APPLICATION_REQUIREMENTS_VERSION, type AgentIdentity, type ApplicationCatalogEntry, type ApplicationDeployTarget, type ApplicationId, type ApplicationRequirement, type ApplicationRequirementAudit, type ApplicationRequirementFinding, type ApplicationRequirementMigrationItem, type ApplicationRequirementScope, type ApplicationRequirementScore, type ApplicationRequirementStatus, type ApplicationRequirementsAuditReport, type CredentialIntrospection, type CrmContactSummary, CrmModule, DEFAULT_SERVICE_URLS, type DeviceAuthorizationResponse, IdentityModule, type ListContactsOptions, type ListContactsResult, type OAuthHelperOptions, type P2PActionDetail, type P2PActionRequestInput, type P2PActionRequestResult, type P2PActionRow, type P2PActionRun, type P2PCapability, type P2PConnection, type P2PHold, type P2PLedgerEntry, P2PModule, type P2PPolicy, type P2PPricingModel, type P2PSettlementBatch, type P2PSettlementGroup, type PublishCapabilityInput, type RequestOptions, type ServiceId, TOPOLO_WIDGET_ENDPOINT, TOPOLO_WIDGET_REFRESH_INTERVAL_MS, type TokenResponse, type Topolo, type TopoloActionCatalogEntry, TopoloAuthError, type TopoloChartWidget, TopoloClient, type TopoloClientOptions, type TopoloCredential, type TopoloDebugEvent, TopoloHttpError, type TopoloNotificationsWidget, TopoloOAuth, TopoloPermissionError, type TopoloQuickActionsWidget, type TopoloRecentActivityWidget, TopoloSdkError, type TopoloServiceCatalogEntry, type TopoloStatsWidget, type TopoloWidget, type TopoloWidgetActionItem, type TopoloWidgetActivityItem, type TopoloWidgetApiResponse, type TopoloWidgetBase, type TopoloWidgetConfig, type TopoloWidgetDraft, type TopoloWidgetMetric, type TopoloWidgetNotificationItem, type TopoloWidgetResponseOptions, type TopoloWidgetSnapshotMetadata, type TopoloWidgetSnapshotMetric, type TopoloWidgetType, type TopoloWidgetValidationResult, applicationRequirementScopes, auditAllApplicationRequirements, auditApplicationRequirements, createTopolo, createTopoloWidgetError, createTopoloWidgetResponse, defineTopoloChartWidget, defineTopoloNotificationsWidget, defineTopoloQuickActionsWidget, defineTopoloRecentActivityWidget, defineTopoloStatsWidget, requirementsForApplication, resolveServiceUrl, validateTopoloWidgetResponse };
2104
+ export { APPLICATIONS, APPLICATION_REQUIREMENTS, APPLICATION_REQUIREMENTS_VERSION, type AgentIdentity, type ApplicationCatalogEntry, type ApplicationDeployTarget, type ApplicationId, type ApplicationRequirement, type ApplicationRequirementAudit, type ApplicationRequirementFinding, type ApplicationRequirementMigrationItem, type ApplicationRequirementScope, type ApplicationRequirementScore, type ApplicationRequirementStatus, type ApplicationRequirementsAuditReport, type CredentialIntrospection, type CrmContactSummary, CrmModule, DEFAULT_SERVICE_URLS, type DeviceAuthorizationResponse, IdentityModule, type ListContactsOptions, type ListContactsResult, type OAuthHelperOptions, type P2PActionDetail, type P2PActionRequestInput, type P2PActionRequestResult, type P2PActionRow, type P2PActionRun, type P2PCapability, type P2PConnection, type P2PHold, type P2PLedgerEntry, P2PModule, type P2PPolicy, type P2PPricingModel, type P2PSettlementBatch, type P2PSettlementGroup, type PublishCapabilityInput, type RequestOptions, type ServiceCatalogUrlEntry, type ServiceId, TOPOLO_WIDGET_ENDPOINT, TOPOLO_WIDGET_REFRESH_INTERVAL_MS, type TokenResponse, type Topolo, type TopoloActionCatalogEntry, TopoloAuthError, type TopoloChartWidget, TopoloClient, type TopoloClientOptions, type TopoloCredential, type TopoloDebugEvent, TopoloHttpError, type TopoloNotificationsWidget, TopoloOAuth, TopoloPermissionError, type TopoloQuickActionsWidget, type TopoloRecentActivityWidget, TopoloSdkError, type TopoloServiceCatalogEntry, type TopoloStatsWidget, type TopoloWidget, type TopoloWidgetActionItem, type TopoloWidgetActivityItem, type TopoloWidgetApiResponse, type TopoloWidgetBase, type TopoloWidgetConfig, type TopoloWidgetDraft, type TopoloWidgetMetric, type TopoloWidgetNotificationItem, type TopoloWidgetResponseOptions, type TopoloWidgetSnapshotMetadata, type TopoloWidgetSnapshotMetric, type TopoloWidgetType, type TopoloWidgetValidationResult, applicationRequirementScopes, auditAllApplicationRequirements, auditApplicationRequirements, createTopolo, createTopoloWidgetError, createTopoloWidgetResponse, defineTopoloChartWidget, defineTopoloNotificationsWidget, defineTopoloQuickActionsWidget, defineTopoloRecentActivityWidget, defineTopoloStatsWidget, requirementsForApplication, resolveCatalogServiceUrl, resolveServiceUrl, validateTopoloWidgetResponse };
package/dist/index.d.ts CHANGED
@@ -105,6 +105,8 @@ interface TopoloServiceCatalogEntry {
105
105
  name: string | null;
106
106
  description: string | null;
107
107
  baseUrl: string | null;
108
+ apiBaseUrl?: string | null;
109
+ launchUrl?: string | null;
108
110
  status: string | null;
109
111
  surfaceKind: string | null;
110
112
  launchable: boolean;
@@ -1579,6 +1581,20 @@ declare function auditAllApplicationRequirements(applicationIds?: readonly Appli
1579
1581
  */
1580
1582
 
1581
1583
  declare const DEFAULT_SERVICE_URLS: Record<ServiceId, string>;
1584
+ interface ServiceCatalogUrlEntry {
1585
+ serviceId?: string | null;
1586
+ slug?: string | null;
1587
+ name?: string | null;
1588
+ baseUrl?: string | null;
1589
+ apiBaseUrl?: string | null;
1590
+ launchUrl?: string | null;
1591
+ api_base_url?: string | null;
1592
+ launch_url?: string | null;
1593
+ }
1594
+ declare function resolveCatalogServiceUrl(entry: ServiceCatalogUrlEntry, options?: {
1595
+ serviceKey?: string;
1596
+ overrides?: Record<string, string> | Partial<Record<ServiceId, string>>;
1597
+ }): string | null;
1582
1598
  declare function resolveServiceUrl(service: ServiceId, overrides?: Partial<Record<ServiceId, string>>): string;
1583
1599
 
1584
1600
  declare class TopoloSdkError extends Error {
@@ -2085,4 +2101,4 @@ declare function createTopolo(options: TopoloClientOptions): {
2085
2101
  };
2086
2102
  type Topolo = ReturnType<typeof createTopolo>;
2087
2103
 
2088
- export { APPLICATIONS, APPLICATION_REQUIREMENTS, APPLICATION_REQUIREMENTS_VERSION, type AgentIdentity, type ApplicationCatalogEntry, type ApplicationDeployTarget, type ApplicationId, type ApplicationRequirement, type ApplicationRequirementAudit, type ApplicationRequirementFinding, type ApplicationRequirementMigrationItem, type ApplicationRequirementScope, type ApplicationRequirementScore, type ApplicationRequirementStatus, type ApplicationRequirementsAuditReport, type CredentialIntrospection, type CrmContactSummary, CrmModule, DEFAULT_SERVICE_URLS, type DeviceAuthorizationResponse, IdentityModule, type ListContactsOptions, type ListContactsResult, type OAuthHelperOptions, type P2PActionDetail, type P2PActionRequestInput, type P2PActionRequestResult, type P2PActionRow, type P2PActionRun, type P2PCapability, type P2PConnection, type P2PHold, type P2PLedgerEntry, P2PModule, type P2PPolicy, type P2PPricingModel, type P2PSettlementBatch, type P2PSettlementGroup, type PublishCapabilityInput, type RequestOptions, type ServiceId, TOPOLO_WIDGET_ENDPOINT, TOPOLO_WIDGET_REFRESH_INTERVAL_MS, type TokenResponse, type Topolo, type TopoloActionCatalogEntry, TopoloAuthError, type TopoloChartWidget, TopoloClient, type TopoloClientOptions, type TopoloCredential, type TopoloDebugEvent, TopoloHttpError, type TopoloNotificationsWidget, TopoloOAuth, TopoloPermissionError, type TopoloQuickActionsWidget, type TopoloRecentActivityWidget, TopoloSdkError, type TopoloServiceCatalogEntry, type TopoloStatsWidget, type TopoloWidget, type TopoloWidgetActionItem, type TopoloWidgetActivityItem, type TopoloWidgetApiResponse, type TopoloWidgetBase, type TopoloWidgetConfig, type TopoloWidgetDraft, type TopoloWidgetMetric, type TopoloWidgetNotificationItem, type TopoloWidgetResponseOptions, type TopoloWidgetSnapshotMetadata, type TopoloWidgetSnapshotMetric, type TopoloWidgetType, type TopoloWidgetValidationResult, applicationRequirementScopes, auditAllApplicationRequirements, auditApplicationRequirements, createTopolo, createTopoloWidgetError, createTopoloWidgetResponse, defineTopoloChartWidget, defineTopoloNotificationsWidget, defineTopoloQuickActionsWidget, defineTopoloRecentActivityWidget, defineTopoloStatsWidget, requirementsForApplication, resolveServiceUrl, validateTopoloWidgetResponse };
2104
+ export { APPLICATIONS, APPLICATION_REQUIREMENTS, APPLICATION_REQUIREMENTS_VERSION, type AgentIdentity, type ApplicationCatalogEntry, type ApplicationDeployTarget, type ApplicationId, type ApplicationRequirement, type ApplicationRequirementAudit, type ApplicationRequirementFinding, type ApplicationRequirementMigrationItem, type ApplicationRequirementScope, type ApplicationRequirementScore, type ApplicationRequirementStatus, type ApplicationRequirementsAuditReport, type CredentialIntrospection, type CrmContactSummary, CrmModule, DEFAULT_SERVICE_URLS, type DeviceAuthorizationResponse, IdentityModule, type ListContactsOptions, type ListContactsResult, type OAuthHelperOptions, type P2PActionDetail, type P2PActionRequestInput, type P2PActionRequestResult, type P2PActionRow, type P2PActionRun, type P2PCapability, type P2PConnection, type P2PHold, type P2PLedgerEntry, P2PModule, type P2PPolicy, type P2PPricingModel, type P2PSettlementBatch, type P2PSettlementGroup, type PublishCapabilityInput, type RequestOptions, type ServiceCatalogUrlEntry, type ServiceId, TOPOLO_WIDGET_ENDPOINT, TOPOLO_WIDGET_REFRESH_INTERVAL_MS, type TokenResponse, type Topolo, type TopoloActionCatalogEntry, TopoloAuthError, type TopoloChartWidget, TopoloClient, type TopoloClientOptions, type TopoloCredential, type TopoloDebugEvent, TopoloHttpError, type TopoloNotificationsWidget, TopoloOAuth, TopoloPermissionError, type TopoloQuickActionsWidget, type TopoloRecentActivityWidget, TopoloSdkError, type TopoloServiceCatalogEntry, type TopoloStatsWidget, type TopoloWidget, type TopoloWidgetActionItem, type TopoloWidgetActivityItem, type TopoloWidgetApiResponse, type TopoloWidgetBase, type TopoloWidgetConfig, type TopoloWidgetDraft, type TopoloWidgetMetric, type TopoloWidgetNotificationItem, type TopoloWidgetResponseOptions, type TopoloWidgetSnapshotMetadata, type TopoloWidgetSnapshotMetric, type TopoloWidgetType, type TopoloWidgetValidationResult, applicationRequirementScopes, auditAllApplicationRequirements, auditApplicationRequirements, createTopolo, createTopoloWidgetError, createTopoloWidgetResponse, defineTopoloChartWidget, defineTopoloNotificationsWidget, defineTopoloQuickActionsWidget, defineTopoloRecentActivityWidget, defineTopoloStatsWidget, requirementsForApplication, resolveCatalogServiceUrl, resolveServiceUrl, validateTopoloWidgetResponse };
package/dist/index.js CHANGED
@@ -2045,17 +2045,90 @@ function statusRank(status) {
2045
2045
 
2046
2046
  // src/services.ts
2047
2047
  var DEFAULT_SERVICE_URLS = SERVICES;
2048
+ function resolveCatalogServiceUrl(entry, options = {}) {
2049
+ const keys = catalogServiceKeyCandidates(entry, options.serviceKey);
2050
+ const override = resolveServiceUrlOverride(keys, options.overrides);
2051
+ if (override) return override;
2052
+ const apiBaseUrl = firstString(entry.apiBaseUrl, entry.api_base_url);
2053
+ if (apiBaseUrl) return apiBaseUrl;
2054
+ const bundledUrl = firstBundledServiceUrl(keys);
2055
+ const liveUrl = firstString(entry.baseUrl);
2056
+ if (bundledUrl && (!liveUrl || sameUrlEnvironment(bundledUrl, liveUrl))) return bundledUrl;
2057
+ return liveUrl ?? bundledUrl ?? null;
2058
+ }
2048
2059
  function resolveServiceUrl(service, overrides) {
2049
- const override = overrides?.[service];
2060
+ const override = resolveServiceUrlOverride([service], overrides);
2050
2061
  if (override) return override;
2051
- const envKey = `TOPOLO_SERVICE_URL_${service.replace(/\./g, "_").toUpperCase()}`;
2052
- const envValue = typeof process !== "undefined" ? process.env?.[envKey] : void 0;
2053
- if (envValue) return envValue;
2054
2062
  return DEFAULT_SERVICE_URLS[service];
2055
2063
  }
2064
+ function catalogServiceKeyCandidates(entry, preferred) {
2065
+ const values = [preferred, entry.slug, entry.name, entry.serviceId].filter(
2066
+ (value) => typeof value === "string" && value.trim().length > 0
2067
+ );
2068
+ const keys = /* @__PURE__ */ new Set();
2069
+ for (const value of values) {
2070
+ for (const candidate of serviceKeyCandidates(value)) keys.add(candidate);
2071
+ }
2072
+ return [...keys];
2073
+ }
2074
+ function serviceKeyCandidates(value) {
2075
+ const normalized = value.trim().toLowerCase().replace(/\s+/g, "-");
2076
+ const withoutTopolo = normalized.startsWith("topolo-") ? normalized.slice("topolo-".length) : normalized;
2077
+ return [.../* @__PURE__ */ new Set([
2078
+ normalized,
2079
+ normalized.replace(/-/g, "_"),
2080
+ withoutTopolo,
2081
+ withoutTopolo.replace(/-/g, "_")
2082
+ ])].filter(Boolean);
2083
+ }
2084
+ function resolveServiceUrlOverride(keys, overrides) {
2085
+ for (const key of keys) {
2086
+ const direct = overrides?.[key];
2087
+ if (direct) return direct;
2088
+ const envStyleKey = key.replace(/\./g, "_");
2089
+ const envStyle = overrides?.[envStyleKey];
2090
+ if (envStyle) return envStyle;
2091
+ }
2092
+ for (const key of keys) {
2093
+ const envKey = `TOPOLO_SERVICE_URL_${key.replace(/\./g, "_").toUpperCase()}`;
2094
+ const envValue = typeof process !== "undefined" ? process.env?.[envKey] : void 0;
2095
+ if (envValue) return envValue;
2096
+ }
2097
+ return null;
2098
+ }
2099
+ function firstBundledServiceUrl(keys) {
2100
+ for (const key of keys) {
2101
+ const url = DEFAULT_SERVICE_URLS[key];
2102
+ if (url) return url;
2103
+ }
2104
+ return null;
2105
+ }
2106
+ function firstString(...values) {
2107
+ for (const value of values) {
2108
+ if (typeof value === "string" && value.trim()) return value.trim();
2109
+ }
2110
+ return null;
2111
+ }
2112
+ function sameUrlEnvironment(left, right) {
2113
+ return urlEnvironment(left) === urlEnvironment(right);
2114
+ }
2115
+ function urlEnvironment(value) {
2116
+ try {
2117
+ const url = new URL(value);
2118
+ const host = url.hostname.toLowerCase();
2119
+ if (host === "localhost" || host === "127.0.0.1") return "local";
2120
+ if (host.endsWith(".stg.topolo.us") || host.includes("topolo-staging")) return "staging";
2121
+ if (host.endsWith(".topolo.app") || host.endsWith(".topolo.us")) return "production";
2122
+ return "external";
2123
+ } catch {
2124
+ return "external";
2125
+ }
2126
+ }
2056
2127
 
2057
2128
  // src/client.ts
2058
2129
  var WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
2130
+ var TOPOLO_ACTION_ID_HEADER = "X-Topolo-Action-Id";
2131
+ var TOPOLO_ACTION_NAME_HEADER = "X-Topolo-Action-Name";
2059
2132
  var POSITIVE_CACHE_TTL_MS = 5 * 60 * 1e3;
2060
2133
  var NEGATIVE_CACHE_TTL_MS = 60 * 1e3;
2061
2134
  var CATALOG_CACHE_TTL_MS = 60 * 1e3;
@@ -2223,8 +2296,12 @@ var TopoloClient = class _TopoloClient {
2223
2296
  async resolveLiveCallableService(serviceKey) {
2224
2297
  const catalog = await this.listServices();
2225
2298
  const match = catalog.find((entry) => catalogEntryMatches(entry, serviceKey));
2226
- if (!match?.baseUrl || !isCallableCatalogEntry(match)) return null;
2227
- return { serviceKey, baseUrl: match.baseUrl };
2299
+ if (!match || !isCallableCatalogEntry(match)) return null;
2300
+ const baseUrl = resolveCatalogServiceUrl(match, {
2301
+ serviceKey,
2302
+ overrides: this.serviceUrls
2303
+ });
2304
+ return baseUrl ? { serviceKey, baseUrl } : null;
2228
2305
  }
2229
2306
  /**
2230
2307
  * Resolve a service-slug → opaque service-id via caller config or TopoloAuth.
@@ -2318,6 +2395,8 @@ var TopoloClient = class _TopoloClient {
2318
2395
  name: typeof s.name === "string" ? s.name : null,
2319
2396
  description: typeof s.description === "string" ? s.description : null,
2320
2397
  baseUrl: typeof s.base_url === "string" ? s.base_url : null,
2398
+ apiBaseUrl: typeof s.api_base_url === "string" ? s.api_base_url : typeof s.apiBaseUrl === "string" ? s.apiBaseUrl : null,
2399
+ launchUrl: typeof s.launch_url === "string" ? s.launch_url : typeof s.launchUrl === "string" ? s.launchUrl : null,
2321
2400
  status: typeof s.status === "string" ? s.status : null,
2322
2401
  surfaceKind: typeof s.surface_kind === "string" ? s.surface_kind : null,
2323
2402
  launchable: s.launchable === true,
@@ -2384,6 +2463,10 @@ var TopoloClient = class _TopoloClient {
2384
2463
  path: mapped.path,
2385
2464
  ...mapped.query ? { query: mapped.query } : {},
2386
2465
  ...mapped.body !== void 0 ? { body: mapped.body } : {},
2466
+ headers: {
2467
+ [TOPOLO_ACTION_ID_HEADER]: entry.actionId,
2468
+ [TOPOLO_ACTION_NAME_HEADER]: entry.name
2469
+ },
2387
2470
  confirm: options.confirm === true
2388
2471
  });
2389
2472
  }
@@ -2399,6 +2482,8 @@ var TopoloClient = class _TopoloClient {
2399
2482
  name: app.name ?? app.id,
2400
2483
  description: null,
2401
2484
  baseUrl: app.productionUrl ?? null,
2485
+ apiBaseUrl: null,
2486
+ launchUrl: app.productionUrl ?? null,
2402
2487
  status: "bundled",
2403
2488
  surfaceKind: "application",
2404
2489
  launchable: Boolean(app.productionUrl),
@@ -2442,7 +2527,7 @@ var TopoloClient = class _TopoloClient {
2442
2527
  }
2443
2528
  };
2444
2529
  function resolveConfiguredServiceId(slug, serviceIds) {
2445
- for (const serviceKey of serviceKeyCandidates(slug)) {
2530
+ for (const serviceKey of serviceKeyCandidates2(slug)) {
2446
2531
  const configured = serviceIds?.[serviceKey];
2447
2532
  if (typeof configured === "string" && configured.trim()) return configured.trim();
2448
2533
  const envKey = `TOPOLO_SERVICE_ID_${serviceKey.replace(/\./g, "_").toUpperCase()}`;
@@ -2452,7 +2537,7 @@ function resolveConfiguredServiceId(slug, serviceIds) {
2452
2537
  return null;
2453
2538
  }
2454
2539
  function resolveConfiguredServiceUrl(slug, serviceUrls) {
2455
- for (const serviceKey of serviceKeyCandidates(slug)) {
2540
+ for (const serviceKey of serviceKeyCandidates2(slug)) {
2456
2541
  const configured = serviceUrls?.[serviceKey];
2457
2542
  if (typeof configured === "string" && configured.trim()) return configured.trim();
2458
2543
  const envKey = `TOPOLO_SERVICE_URL_${serviceKey.replace(/\./g, "_").toUpperCase()}`;
@@ -2461,7 +2546,7 @@ function resolveConfiguredServiceUrl(slug, serviceUrls) {
2461
2546
  }
2462
2547
  return null;
2463
2548
  }
2464
- function serviceKeyCandidates(slug) {
2549
+ function serviceKeyCandidates2(slug) {
2465
2550
  const normalized = normalizeLookupKey(slug);
2466
2551
  const withoutTopolo = normalized.startsWith("topolo-") ? normalized.slice("topolo-".length) : normalized;
2467
2552
  return [.../* @__PURE__ */ new Set([
@@ -2483,7 +2568,7 @@ function catalogEntryLookupKeys(entry) {
2483
2568
  for (const value of values) {
2484
2569
  const normalized = normalizeLookupKey(value);
2485
2570
  if (!normalized) continue;
2486
- for (const candidate of serviceKeyCandidates(normalized)) {
2571
+ for (const candidate of serviceKeyCandidates2(normalized)) {
2487
2572
  keys.add(candidate);
2488
2573
  }
2489
2574
  }
@@ -2493,7 +2578,7 @@ function isCallableCatalogEntry(entry) {
2493
2578
  const kind = normalizeLookupKey(entry.surfaceKind || "");
2494
2579
  if (kind === "api" || kind === "technical_service" || kind === "service") return true;
2495
2580
  return Boolean(
2496
- entry.baseUrl && catalogEntryLookupKeys(entry).some((key) => Object.prototype.hasOwnProperty.call(DEFAULT_SERVICE_URLS, key))
2581
+ (entry.baseUrl || entry.apiBaseUrl) && catalogEntryLookupKeys(entry).some((key) => Object.prototype.hasOwnProperty.call(DEFAULT_SERVICE_URLS, key))
2497
2582
  );
2498
2583
  }
2499
2584
  function normalizeActionCatalogEntry(item) {
@@ -3173,6 +3258,7 @@ export {
3173
3258
  defineTopoloRecentActivityWidget,
3174
3259
  defineTopoloStatsWidget,
3175
3260
  requirementsForApplication,
3261
+ resolveCatalogServiceUrl,
3176
3262
  resolveServiceUrl,
3177
3263
  validateTopoloWidgetResponse
3178
3264
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@topolo/sdk",
3
- "version": "0.4.3",
3
+ "version": "0.4.5",
4
4
  "description": "Typed client SDK for the Topolo platform. Used by TopoloCli, TopoloMCP, and third-party agents.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -477,6 +477,8 @@ describe('TopoloClient.listServices', () => {
477
477
  name: 'Topolo Director',
478
478
  description: 'Demo control plane',
479
479
  baseUrl: 'https://director.topolo.app',
480
+ apiBaseUrl: null,
481
+ launchUrl: null,
480
482
  status: 'active',
481
483
  surfaceKind: 'application',
482
484
  launchable: true,
@@ -536,6 +538,74 @@ describe('TopoloClient.listServices', () => {
536
538
  expect(calls[1]!.url).toBe('https://mail.topolo.app/api/health');
537
539
  });
538
540
 
541
+ it('routes known split-surface services to the callable API URL', async () => {
542
+ const { impl, calls } = stubFetch([
543
+ {
544
+ body: {
545
+ data: {
546
+ services: [
547
+ {
548
+ service_id: 'srv_crm',
549
+ app_slug: 'topolo-crm',
550
+ name: 'Topolo CRM',
551
+ base_url: 'https://crm.topolo.app',
552
+ surface_kind: 'application',
553
+ status: 'active',
554
+ granted_permissions: ['srv_crm.contacts:read'],
555
+ },
556
+ ],
557
+ },
558
+ },
559
+ },
560
+ { body: { ok: true } },
561
+ ]);
562
+ const client = new TopoloClient({
563
+ credential: { kind: 'api_key', apiKey: 'dak_xyz' },
564
+ agent: AGENT,
565
+ fetch: impl,
566
+ serviceUrls: { auth: 'https://auth.example.com' },
567
+ });
568
+
569
+ await client.request({ service: 'crm', path: '/health' });
570
+
571
+ expect(calls[0]!.url).toBe('https://auth.example.com/api/services/catalog');
572
+ expect(calls[1]!.url).toBe('https://crm-api.topolo.app/health');
573
+ });
574
+
575
+ it('prefers live catalog api_base_url for dynamic services', async () => {
576
+ const { impl, calls } = stubFetch([
577
+ {
578
+ body: {
579
+ data: {
580
+ services: [
581
+ {
582
+ service_id: 'srv_future',
583
+ app_slug: 'topolo-future',
584
+ name: 'Future',
585
+ base_url: 'https://future.topolo.app',
586
+ api_base_url: 'https://future-api.topolo.app',
587
+ surface_kind: 'api',
588
+ status: 'active',
589
+ granted_permissions: ['srv_future.read:all'],
590
+ },
591
+ ],
592
+ },
593
+ },
594
+ },
595
+ { body: { ok: true } },
596
+ ]);
597
+ const client = new TopoloClient({
598
+ credential: { kind: 'api_key', apiKey: 'dak_xyz' },
599
+ agent: AGENT,
600
+ fetch: impl,
601
+ serviceUrls: { auth: 'https://auth.example.com' },
602
+ });
603
+
604
+ await client.request({ service: 'future', path: '/health' });
605
+
606
+ expect(calls[1]!.url).toBe('https://future-api.topolo.app/health');
607
+ });
608
+
539
609
  it('refuses a bundled static service that is absent from the scoped live catalog', async () => {
540
610
  const { impl, calls } = stubFetch([
541
611
  {
@@ -671,8 +741,11 @@ describe('TopoloClient action catalog', () => {
671
741
  await client.callAction('messages.list', { mailboxId: 'primary', q: 'Ada', page: 2 });
672
742
  const call = requestCalls(calls)[0]!;
673
743
  const url = new URL(call.url);
674
- expect(url.toString()).toBe('https://mail-api.topolo.app/api/mail/mailboxes/primary/messages?q=Ada&page=2');
744
+ expect(url.toString()).toBe('https://mail.topolo.app/api/mail/mailboxes/primary/messages?q=Ada&page=2');
675
745
  expect(call.init.method).toBe('GET');
746
+ const headers = new Headers(call.init.headers);
747
+ expect(headers.get('X-Topolo-Action-Id')).toBe('srv_mail.messages.list');
748
+ expect(headers.get('X-Topolo-Action-Name')).toBe('messages.list');
676
749
  });
677
750
 
678
751
  it('substitutes action path params and removes them from the query', async () => {
package/src/client.ts CHANGED
@@ -9,6 +9,7 @@ import { TopoloAuthError, TopoloHttpError, TopoloPermissionError, TopoloSdkError
9
9
  import {
10
10
  DEFAULT_SERVICE_URLS,
11
11
  APPLICATIONS,
12
+ resolveCatalogServiceUrl,
12
13
  resolveServiceUrl,
13
14
  type ServiceId,
14
15
  } from './services.js';
@@ -83,6 +84,8 @@ export interface RequestOptions {
83
84
  }
84
85
 
85
86
  const WRITE_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
87
+ const TOPOLO_ACTION_ID_HEADER = 'X-Topolo-Action-Id';
88
+ const TOPOLO_ACTION_NAME_HEADER = 'X-Topolo-Action-Name';
86
89
 
87
90
  const POSITIVE_CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
88
91
  const NEGATIVE_CACHE_TTL_MS = 60 * 1000; // 1 minute
@@ -104,6 +107,8 @@ export interface TopoloServiceCatalogEntry {
104
107
  name: string | null;
105
108
  description: string | null;
106
109
  baseUrl: string | null;
110
+ apiBaseUrl?: string | null;
111
+ launchUrl?: string | null;
107
112
  status: string | null;
108
113
  surfaceKind: string | null;
109
114
  launchable: boolean;
@@ -323,8 +328,12 @@ export class TopoloClient {
323
328
  private async resolveLiveCallableService(serviceKey: string): Promise<{ serviceKey: string; baseUrl: string } | null> {
324
329
  const catalog = await this.listServices();
325
330
  const match = catalog.find((entry) => catalogEntryMatches(entry, serviceKey));
326
- if (!match?.baseUrl || !isCallableCatalogEntry(match)) return null;
327
- return { serviceKey, baseUrl: match.baseUrl };
331
+ if (!match || !isCallableCatalogEntry(match)) return null;
332
+ const baseUrl = resolveCatalogServiceUrl(match, {
333
+ serviceKey,
334
+ overrides: this.serviceUrls,
335
+ });
336
+ return baseUrl ? { serviceKey, baseUrl } : null;
328
337
  }
329
338
 
330
339
  /**
@@ -446,6 +455,18 @@ export class TopoloClient {
446
455
  description:
447
456
  typeof s.description === 'string' ? s.description : null,
448
457
  baseUrl: typeof s.base_url === 'string' ? s.base_url : null,
458
+ apiBaseUrl:
459
+ typeof s.api_base_url === 'string'
460
+ ? s.api_base_url
461
+ : typeof s.apiBaseUrl === 'string'
462
+ ? s.apiBaseUrl
463
+ : null,
464
+ launchUrl:
465
+ typeof s.launch_url === 'string'
466
+ ? s.launch_url
467
+ : typeof s.launchUrl === 'string'
468
+ ? s.launchUrl
469
+ : null,
449
470
  status: typeof s.status === 'string' ? s.status : null,
450
471
  surfaceKind:
451
472
  typeof s.surface_kind === 'string' ? s.surface_kind : null,
@@ -529,6 +550,10 @@ export class TopoloClient {
529
550
  path: mapped.path,
530
551
  ...(mapped.query ? { query: mapped.query } : {}),
531
552
  ...(mapped.body !== undefined ? { body: mapped.body } : {}),
553
+ headers: {
554
+ [TOPOLO_ACTION_ID_HEADER]: entry.actionId,
555
+ [TOPOLO_ACTION_NAME_HEADER]: entry.name,
556
+ },
532
557
  confirm: options.confirm === true,
533
558
  });
534
559
  }
@@ -549,6 +574,8 @@ export class TopoloClient {
549
574
  name: app.name ?? app.id,
550
575
  description: null,
551
576
  baseUrl: app.productionUrl ?? null,
577
+ apiBaseUrl: null,
578
+ launchUrl: app.productionUrl ?? null,
552
579
  status: 'bundled',
553
580
  surfaceKind: 'application',
554
581
  launchable: Boolean(app.productionUrl),
@@ -672,7 +699,7 @@ function isCallableCatalogEntry(entry: TopoloServiceCatalogEntry): boolean {
672
699
  const kind = normalizeLookupKey(entry.surfaceKind || '');
673
700
  if (kind === 'api' || kind === 'technical_service' || kind === 'service') return true;
674
701
  return Boolean(
675
- entry.baseUrl &&
702
+ (entry.baseUrl || entry.apiBaseUrl) &&
676
703
  catalogEntryLookupKeys(entry).some((key) => Object.prototype.hasOwnProperty.call(DEFAULT_SERVICE_URLS, key)),
677
704
  );
678
705
  }
package/src/index.ts CHANGED
@@ -17,6 +17,7 @@ export {
17
17
  auditApplicationRequirements,
18
18
  applicationRequirementScopes,
19
19
  requirementsForApplication,
20
+ resolveCatalogServiceUrl,
20
21
  resolveServiceUrl,
21
22
  type ApplicationCatalogEntry,
22
23
  type ApplicationDeployTarget,
@@ -29,6 +30,7 @@ export {
29
30
  type ApplicationRequirementScope,
30
31
  type ApplicationRequirementStatus,
31
32
  type ApplicationRequirementsAuditReport,
33
+ type ServiceCatalogUrlEntry,
32
34
  type ServiceId,
33
35
  } from "./services.js";
34
36
  export {
@@ -7,6 +7,7 @@ import {
7
7
  auditAllApplicationRequirements,
8
8
  auditApplicationRequirements,
9
9
  applicationRequirementScopes,
10
+ resolveCatalogServiceUrl,
10
11
  requirementsForApplication,
11
12
  } from './services.js';
12
13
 
@@ -97,6 +98,34 @@ describe('service registry', () => {
97
98
  expect(applicationServices.has(serviceId)).toBe(true);
98
99
  }
99
100
  });
101
+
102
+ it('resolves known split-surface services to the generated callable URL', () => {
103
+ expect(resolveCatalogServiceUrl({
104
+ serviceId: 'srv_crm',
105
+ slug: 'topolo-crm',
106
+ name: 'Topolo CRM',
107
+ baseUrl: 'https://crm.topolo.app',
108
+ }, { serviceKey: 'crm' })).toBe('https://crm-api.topolo.app');
109
+ });
110
+
111
+ it('uses a live staging URL instead of a production bundled default', () => {
112
+ expect(resolveCatalogServiceUrl({
113
+ serviceId: 'srv_crm',
114
+ slug: 'topolo-crm',
115
+ name: 'Topolo CRM',
116
+ baseUrl: 'https://crm.stg.topolo.us',
117
+ }, { serviceKey: 'crm' })).toBe('https://crm.stg.topolo.us');
118
+ });
119
+
120
+ it('prefers explicit catalog API URLs over launcher URLs', () => {
121
+ expect(resolveCatalogServiceUrl({
122
+ serviceId: 'srv_future',
123
+ slug: 'topolo-future',
124
+ name: 'Future',
125
+ baseUrl: 'https://future.topolo.app',
126
+ apiBaseUrl: 'https://future-api.topolo.app',
127
+ })).toBe('https://future-api.topolo.app');
128
+ });
100
129
  });
101
130
 
102
131
  describe('application catalog', () => {
package/src/services.ts CHANGED
@@ -47,16 +47,122 @@ export type { ApplicationCatalogEntry, ApplicationDeployTarget, ApplicationId };
47
47
 
48
48
  export const DEFAULT_SERVICE_URLS: Record<ServiceId, string> = SERVICES;
49
49
 
50
+ export interface ServiceCatalogUrlEntry {
51
+ serviceId?: string | null;
52
+ slug?: string | null;
53
+ name?: string | null;
54
+ baseUrl?: string | null;
55
+ apiBaseUrl?: string | null;
56
+ launchUrl?: string | null;
57
+ api_base_url?: string | null;
58
+ launch_url?: string | null;
59
+ }
60
+
61
+ export function resolveCatalogServiceUrl(
62
+ entry: ServiceCatalogUrlEntry,
63
+ options: {
64
+ serviceKey?: string;
65
+ overrides?: Record<string, string> | Partial<Record<ServiceId, string>>;
66
+ } = {},
67
+ ): string | null {
68
+ const keys = catalogServiceKeyCandidates(entry, options.serviceKey);
69
+ const override = resolveServiceUrlOverride(keys, options.overrides);
70
+ if (override) return override;
71
+
72
+ const apiBaseUrl = firstString(entry.apiBaseUrl, entry.api_base_url);
73
+ if (apiBaseUrl) return apiBaseUrl;
74
+
75
+ const bundledUrl = firstBundledServiceUrl(keys);
76
+ const liveUrl = firstString(entry.baseUrl);
77
+ if (bundledUrl && (!liveUrl || sameUrlEnvironment(bundledUrl, liveUrl))) return bundledUrl;
78
+
79
+ return liveUrl ?? bundledUrl ?? null;
80
+ }
81
+
50
82
  export function resolveServiceUrl(
51
83
  service: ServiceId,
52
84
  overrides?: Partial<Record<ServiceId, string>>,
53
85
  ): string {
54
- const override = overrides?.[service];
86
+ const override = resolveServiceUrlOverride([service], overrides);
55
87
  if (override) return override;
56
88
 
57
- const envKey = `TOPOLO_SERVICE_URL_${service.replace(/\./g, '_').toUpperCase()}`;
58
- const envValue = typeof process !== 'undefined' ? process.env?.[envKey] : undefined;
59
- if (envValue) return envValue;
60
-
61
89
  return DEFAULT_SERVICE_URLS[service];
62
90
  }
91
+
92
+ function catalogServiceKeyCandidates(entry: ServiceCatalogUrlEntry, preferred?: string): string[] {
93
+ const values = [preferred, entry.slug, entry.name, entry.serviceId].filter(
94
+ (value): value is string => typeof value === 'string' && value.trim().length > 0,
95
+ );
96
+ const keys = new Set<string>();
97
+ for (const value of values) {
98
+ for (const candidate of serviceKeyCandidates(value)) keys.add(candidate);
99
+ }
100
+ return [...keys];
101
+ }
102
+
103
+ function serviceKeyCandidates(value: string): string[] {
104
+ const normalized = value.trim().toLowerCase().replace(/\s+/g, '-');
105
+ const withoutTopolo = normalized.startsWith('topolo-')
106
+ ? normalized.slice('topolo-'.length)
107
+ : normalized;
108
+ return [...new Set([
109
+ normalized,
110
+ normalized.replace(/-/g, '_'),
111
+ withoutTopolo,
112
+ withoutTopolo.replace(/-/g, '_'),
113
+ ])].filter(Boolean);
114
+ }
115
+
116
+ function resolveServiceUrlOverride(
117
+ keys: string[],
118
+ overrides?: Record<string, string> | Partial<Record<ServiceId, string>>,
119
+ ): string | null {
120
+ for (const key of keys) {
121
+ const direct = overrides?.[key as ServiceId];
122
+ if (direct) return direct;
123
+
124
+ const envStyleKey = key.replace(/\./g, '_');
125
+ const envStyle = overrides?.[envStyleKey as ServiceId];
126
+ if (envStyle) return envStyle;
127
+ }
128
+
129
+ for (const key of keys) {
130
+ const envKey = `TOPOLO_SERVICE_URL_${key.replace(/\./g, '_').toUpperCase()}`;
131
+ const envValue = typeof process !== 'undefined' ? process.env?.[envKey] : undefined;
132
+ if (envValue) return envValue;
133
+ }
134
+
135
+ return null;
136
+ }
137
+
138
+ function firstBundledServiceUrl(keys: string[]): string | null {
139
+ for (const key of keys) {
140
+ const url = DEFAULT_SERVICE_URLS[key as ServiceId];
141
+ if (url) return url;
142
+ }
143
+ return null;
144
+ }
145
+
146
+ function firstString(...values: Array<string | null | undefined>): string | null {
147
+ for (const value of values) {
148
+ if (typeof value === 'string' && value.trim()) return value.trim();
149
+ }
150
+ return null;
151
+ }
152
+
153
+ function sameUrlEnvironment(left: string, right: string): boolean {
154
+ return urlEnvironment(left) === urlEnvironment(right);
155
+ }
156
+
157
+ function urlEnvironment(value: string): 'staging' | 'production' | 'local' | 'external' {
158
+ try {
159
+ const url = new URL(value);
160
+ const host = url.hostname.toLowerCase();
161
+ if (host === 'localhost' || host === '127.0.0.1') return 'local';
162
+ if (host.endsWith('.stg.topolo.us') || host.includes('topolo-staging')) return 'staging';
163
+ if (host.endsWith('.topolo.app') || host.endsWith('.topolo.us')) return 'production';
164
+ return 'external';
165
+ } catch {
166
+ return 'external';
167
+ }
168
+ }