@topolo/sdk 0.4.4 → 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,14 +2100,85 @@ 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"]);
@@ -2279,8 +2351,12 @@ var TopoloClient = class _TopoloClient {
2279
2351
  async resolveLiveCallableService(serviceKey) {
2280
2352
  const catalog = await this.listServices();
2281
2353
  const match = catalog.find((entry) => catalogEntryMatches(entry, serviceKey));
2282
- if (!match?.baseUrl || !isCallableCatalogEntry(match)) return null;
2283
- 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;
2284
2360
  }
2285
2361
  /**
2286
2362
  * Resolve a service-slug → opaque service-id via caller config or TopoloAuth.
@@ -2374,6 +2450,8 @@ var TopoloClient = class _TopoloClient {
2374
2450
  name: typeof s.name === "string" ? s.name : null,
2375
2451
  description: typeof s.description === "string" ? s.description : null,
2376
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,
2377
2455
  status: typeof s.status === "string" ? s.status : null,
2378
2456
  surfaceKind: typeof s.surface_kind === "string" ? s.surface_kind : null,
2379
2457
  launchable: s.launchable === true,
@@ -2459,6 +2537,8 @@ var TopoloClient = class _TopoloClient {
2459
2537
  name: app.name ?? app.id,
2460
2538
  description: null,
2461
2539
  baseUrl: app.productionUrl ?? null,
2540
+ apiBaseUrl: null,
2541
+ launchUrl: app.productionUrl ?? null,
2462
2542
  status: "bundled",
2463
2543
  surfaceKind: "application",
2464
2544
  launchable: Boolean(app.productionUrl),
@@ -2502,7 +2582,7 @@ var TopoloClient = class _TopoloClient {
2502
2582
  }
2503
2583
  };
2504
2584
  function resolveConfiguredServiceId(slug, serviceIds) {
2505
- for (const serviceKey of serviceKeyCandidates(slug)) {
2585
+ for (const serviceKey of serviceKeyCandidates2(slug)) {
2506
2586
  const configured = serviceIds?.[serviceKey];
2507
2587
  if (typeof configured === "string" && configured.trim()) return configured.trim();
2508
2588
  const envKey = `TOPOLO_SERVICE_ID_${serviceKey.replace(/\./g, "_").toUpperCase()}`;
@@ -2512,7 +2592,7 @@ function resolveConfiguredServiceId(slug, serviceIds) {
2512
2592
  return null;
2513
2593
  }
2514
2594
  function resolveConfiguredServiceUrl(slug, serviceUrls) {
2515
- for (const serviceKey of serviceKeyCandidates(slug)) {
2595
+ for (const serviceKey of serviceKeyCandidates2(slug)) {
2516
2596
  const configured = serviceUrls?.[serviceKey];
2517
2597
  if (typeof configured === "string" && configured.trim()) return configured.trim();
2518
2598
  const envKey = `TOPOLO_SERVICE_URL_${serviceKey.replace(/\./g, "_").toUpperCase()}`;
@@ -2521,7 +2601,7 @@ function resolveConfiguredServiceUrl(slug, serviceUrls) {
2521
2601
  }
2522
2602
  return null;
2523
2603
  }
2524
- function serviceKeyCandidates(slug) {
2604
+ function serviceKeyCandidates2(slug) {
2525
2605
  const normalized = normalizeLookupKey(slug);
2526
2606
  const withoutTopolo = normalized.startsWith("topolo-") ? normalized.slice("topolo-".length) : normalized;
2527
2607
  return [.../* @__PURE__ */ new Set([
@@ -2543,7 +2623,7 @@ function catalogEntryLookupKeys(entry) {
2543
2623
  for (const value of values) {
2544
2624
  const normalized = normalizeLookupKey(value);
2545
2625
  if (!normalized) continue;
2546
- for (const candidate of serviceKeyCandidates(normalized)) {
2626
+ for (const candidate of serviceKeyCandidates2(normalized)) {
2547
2627
  keys.add(candidate);
2548
2628
  }
2549
2629
  }
@@ -2553,7 +2633,7 @@ function isCallableCatalogEntry(entry) {
2553
2633
  const kind = normalizeLookupKey(entry.surfaceKind || "");
2554
2634
  if (kind === "api" || kind === "technical_service" || kind === "service") return true;
2555
2635
  return Boolean(
2556
- 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))
2557
2637
  );
2558
2638
  }
2559
2639
  function normalizeActionCatalogEntry(item) {
@@ -3234,6 +3314,7 @@ function createTopolo(options) {
3234
3314
  defineTopoloRecentActivityWidget,
3235
3315
  defineTopoloStatsWidget,
3236
3316
  requirementsForApplication,
3317
+ resolveCatalogServiceUrl,
3237
3318
  resolveServiceUrl,
3238
3319
  validateTopoloWidgetResponse
3239
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,14 +2045,85 @@ 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"]);
@@ -2225,8 +2296,12 @@ var TopoloClient = class _TopoloClient {
2225
2296
  async resolveLiveCallableService(serviceKey) {
2226
2297
  const catalog = await this.listServices();
2227
2298
  const match = catalog.find((entry) => catalogEntryMatches(entry, serviceKey));
2228
- if (!match?.baseUrl || !isCallableCatalogEntry(match)) return null;
2229
- 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;
2230
2305
  }
2231
2306
  /**
2232
2307
  * Resolve a service-slug → opaque service-id via caller config or TopoloAuth.
@@ -2320,6 +2395,8 @@ var TopoloClient = class _TopoloClient {
2320
2395
  name: typeof s.name === "string" ? s.name : null,
2321
2396
  description: typeof s.description === "string" ? s.description : null,
2322
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,
2323
2400
  status: typeof s.status === "string" ? s.status : null,
2324
2401
  surfaceKind: typeof s.surface_kind === "string" ? s.surface_kind : null,
2325
2402
  launchable: s.launchable === true,
@@ -2405,6 +2482,8 @@ var TopoloClient = class _TopoloClient {
2405
2482
  name: app.name ?? app.id,
2406
2483
  description: null,
2407
2484
  baseUrl: app.productionUrl ?? null,
2485
+ apiBaseUrl: null,
2486
+ launchUrl: app.productionUrl ?? null,
2408
2487
  status: "bundled",
2409
2488
  surfaceKind: "application",
2410
2489
  launchable: Boolean(app.productionUrl),
@@ -2448,7 +2527,7 @@ var TopoloClient = class _TopoloClient {
2448
2527
  }
2449
2528
  };
2450
2529
  function resolveConfiguredServiceId(slug, serviceIds) {
2451
- for (const serviceKey of serviceKeyCandidates(slug)) {
2530
+ for (const serviceKey of serviceKeyCandidates2(slug)) {
2452
2531
  const configured = serviceIds?.[serviceKey];
2453
2532
  if (typeof configured === "string" && configured.trim()) return configured.trim();
2454
2533
  const envKey = `TOPOLO_SERVICE_ID_${serviceKey.replace(/\./g, "_").toUpperCase()}`;
@@ -2458,7 +2537,7 @@ function resolveConfiguredServiceId(slug, serviceIds) {
2458
2537
  return null;
2459
2538
  }
2460
2539
  function resolveConfiguredServiceUrl(slug, serviceUrls) {
2461
- for (const serviceKey of serviceKeyCandidates(slug)) {
2540
+ for (const serviceKey of serviceKeyCandidates2(slug)) {
2462
2541
  const configured = serviceUrls?.[serviceKey];
2463
2542
  if (typeof configured === "string" && configured.trim()) return configured.trim();
2464
2543
  const envKey = `TOPOLO_SERVICE_URL_${serviceKey.replace(/\./g, "_").toUpperCase()}`;
@@ -2467,7 +2546,7 @@ function resolveConfiguredServiceUrl(slug, serviceUrls) {
2467
2546
  }
2468
2547
  return null;
2469
2548
  }
2470
- function serviceKeyCandidates(slug) {
2549
+ function serviceKeyCandidates2(slug) {
2471
2550
  const normalized = normalizeLookupKey(slug);
2472
2551
  const withoutTopolo = normalized.startsWith("topolo-") ? normalized.slice("topolo-".length) : normalized;
2473
2552
  return [.../* @__PURE__ */ new Set([
@@ -2489,7 +2568,7 @@ function catalogEntryLookupKeys(entry) {
2489
2568
  for (const value of values) {
2490
2569
  const normalized = normalizeLookupKey(value);
2491
2570
  if (!normalized) continue;
2492
- for (const candidate of serviceKeyCandidates(normalized)) {
2571
+ for (const candidate of serviceKeyCandidates2(normalized)) {
2493
2572
  keys.add(candidate);
2494
2573
  }
2495
2574
  }
@@ -2499,7 +2578,7 @@ function isCallableCatalogEntry(entry) {
2499
2578
  const kind = normalizeLookupKey(entry.surfaceKind || "");
2500
2579
  if (kind === "api" || kind === "technical_service" || kind === "service") return true;
2501
2580
  return Boolean(
2502
- 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))
2503
2582
  );
2504
2583
  }
2505
2584
  function normalizeActionCatalogEntry(item) {
@@ -3179,6 +3258,7 @@ export {
3179
3258
  defineTopoloRecentActivityWidget,
3180
3259
  defineTopoloStatsWidget,
3181
3260
  requirementsForApplication,
3261
+ resolveCatalogServiceUrl,
3182
3262
  resolveServiceUrl,
3183
3263
  validateTopoloWidgetResponse
3184
3264
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@topolo/sdk",
3
- "version": "0.4.4",
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,7 +741,7 @@ 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');
676
746
  const headers = new Headers(call.init.headers);
677
747
  expect(headers.get('X-Topolo-Action-Id')).toBe('srv_mail.messages.list');
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';
@@ -106,6 +107,8 @@ export interface TopoloServiceCatalogEntry {
106
107
  name: string | null;
107
108
  description: string | null;
108
109
  baseUrl: string | null;
110
+ apiBaseUrl?: string | null;
111
+ launchUrl?: string | null;
109
112
  status: string | null;
110
113
  surfaceKind: string | null;
111
114
  launchable: boolean;
@@ -325,8 +328,12 @@ export class TopoloClient {
325
328
  private async resolveLiveCallableService(serviceKey: string): Promise<{ serviceKey: string; baseUrl: string } | null> {
326
329
  const catalog = await this.listServices();
327
330
  const match = catalog.find((entry) => catalogEntryMatches(entry, serviceKey));
328
- if (!match?.baseUrl || !isCallableCatalogEntry(match)) return null;
329
- 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;
330
337
  }
331
338
 
332
339
  /**
@@ -448,6 +455,18 @@ export class TopoloClient {
448
455
  description:
449
456
  typeof s.description === 'string' ? s.description : null,
450
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,
451
470
  status: typeof s.status === 'string' ? s.status : null,
452
471
  surfaceKind:
453
472
  typeof s.surface_kind === 'string' ? s.surface_kind : null,
@@ -555,6 +574,8 @@ export class TopoloClient {
555
574
  name: app.name ?? app.id,
556
575
  description: null,
557
576
  baseUrl: app.productionUrl ?? null,
577
+ apiBaseUrl: null,
578
+ launchUrl: app.productionUrl ?? null,
558
579
  status: 'bundled',
559
580
  surfaceKind: 'application',
560
581
  launchable: Boolean(app.productionUrl),
@@ -678,7 +699,7 @@ function isCallableCatalogEntry(entry: TopoloServiceCatalogEntry): boolean {
678
699
  const kind = normalizeLookupKey(entry.surfaceKind || '');
679
700
  if (kind === 'api' || kind === 'technical_service' || kind === 'service') return true;
680
701
  return Boolean(
681
- entry.baseUrl &&
702
+ (entry.baseUrl || entry.apiBaseUrl) &&
682
703
  catalogEntryLookupKeys(entry).some((key) => Object.prototype.hasOwnProperty.call(DEFAULT_SERVICE_URLS, key)),
683
704
  );
684
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
+ }