@topolo/sdk 0.4.4 → 0.4.6

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
  });
@@ -136,7 +137,7 @@ var SERVICES = {
136
137
  inventory: "https://inventory.topolo.app",
137
138
  learn: "https://learn-api.topolo.app",
138
139
  localize: "https://localize.topolo.app",
139
- mail: "https://mail.topolo.app",
140
+ mail: "https://mail-api.topolo.app",
140
141
  mdm: "https://mdm-api.topolo.app",
141
142
  messages: "https://messages.topolo.app",
142
143
  nexus: "https://nexus-api.topolo.app",
@@ -974,15 +975,17 @@ var APPLICATIONS = {
974
975
  "mail.topolo.app"
975
976
  ],
976
977
  "routes": [],
977
- "serviceId": "mail"
978
+ "serviceId": null
978
979
  },
979
980
  {
980
981
  "name": "topolo-mail-api",
981
982
  "kind": "worker",
982
- "apiUrl": null,
983
- "domains": [],
983
+ "apiUrl": "https://mail-api.topolo.app",
984
+ "domains": [
985
+ "mail-api.topolo.app"
986
+ ],
984
987
  "routes": [],
985
- "serviceId": null
988
+ "serviceId": "mail"
986
989
  }
987
990
  ]
988
991
  },
@@ -2099,14 +2102,60 @@ function statusRank(status) {
2099
2102
 
2100
2103
  // src/services.ts
2101
2104
  var DEFAULT_SERVICE_URLS = SERVICES;
2105
+ function resolveCatalogServiceUrl(entry, options = {}) {
2106
+ const keys = catalogServiceKeyCandidates(entry, options.serviceKey);
2107
+ const override = resolveServiceUrlOverride(keys, options.overrides);
2108
+ if (override) return override;
2109
+ const apiBaseUrl = firstString(entry.apiBaseUrl, entry.api_base_url);
2110
+ if (apiBaseUrl) return apiBaseUrl;
2111
+ return null;
2112
+ }
2102
2113
  function resolveServiceUrl(service, overrides) {
2103
- const override = overrides?.[service];
2114
+ const override = resolveServiceUrlOverride([service], overrides);
2104
2115
  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
2116
  return DEFAULT_SERVICE_URLS[service];
2109
2117
  }
2118
+ function catalogServiceKeyCandidates(entry, preferred) {
2119
+ const values = [preferred, entry.slug, entry.name, entry.serviceId].filter(
2120
+ (value) => typeof value === "string" && value.trim().length > 0
2121
+ );
2122
+ const keys = /* @__PURE__ */ new Set();
2123
+ for (const value of values) {
2124
+ for (const candidate of serviceKeyCandidates(value)) keys.add(candidate);
2125
+ }
2126
+ return [...keys];
2127
+ }
2128
+ function serviceKeyCandidates(value) {
2129
+ const normalized = value.trim().toLowerCase().replace(/\s+/g, "-");
2130
+ const withoutTopolo = normalized.startsWith("topolo-") ? normalized.slice("topolo-".length) : normalized;
2131
+ return [.../* @__PURE__ */ new Set([
2132
+ normalized,
2133
+ normalized.replace(/-/g, "_"),
2134
+ withoutTopolo,
2135
+ withoutTopolo.replace(/-/g, "_")
2136
+ ])].filter(Boolean);
2137
+ }
2138
+ function resolveServiceUrlOverride(keys, overrides) {
2139
+ for (const key of keys) {
2140
+ const direct = overrides?.[key];
2141
+ if (direct) return direct;
2142
+ const envStyleKey = key.replace(/\./g, "_");
2143
+ const envStyle = overrides?.[envStyleKey];
2144
+ if (envStyle) return envStyle;
2145
+ }
2146
+ for (const key of keys) {
2147
+ const envKey = `TOPOLO_SERVICE_URL_${key.replace(/\./g, "_").toUpperCase()}`;
2148
+ const envValue = typeof process !== "undefined" ? process.env?.[envKey] : void 0;
2149
+ if (envValue) return envValue;
2150
+ }
2151
+ return null;
2152
+ }
2153
+ function firstString(...values) {
2154
+ for (const value of values) {
2155
+ if (typeof value === "string" && value.trim()) return value.trim();
2156
+ }
2157
+ return null;
2158
+ }
2110
2159
 
2111
2160
  // src/client.ts
2112
2161
  var WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
@@ -2279,8 +2328,12 @@ var TopoloClient = class _TopoloClient {
2279
2328
  async resolveLiveCallableService(serviceKey) {
2280
2329
  const catalog = await this.listServices();
2281
2330
  const match = catalog.find((entry) => catalogEntryMatches(entry, serviceKey));
2282
- if (!match?.baseUrl || !isCallableCatalogEntry(match)) return null;
2283
- return { serviceKey, baseUrl: match.baseUrl };
2331
+ if (!match) return null;
2332
+ const baseUrl = resolveCatalogServiceUrl(match, {
2333
+ serviceKey,
2334
+ overrides: this.serviceUrls
2335
+ });
2336
+ return baseUrl ? { serviceKey, baseUrl } : null;
2284
2337
  }
2285
2338
  /**
2286
2339
  * Resolve a service-slug → opaque service-id via caller config or TopoloAuth.
@@ -2373,7 +2426,8 @@ var TopoloClient = class _TopoloClient {
2373
2426
  slug: typeof s.app_slug === "string" ? s.app_slug : null,
2374
2427
  name: typeof s.name === "string" ? s.name : null,
2375
2428
  description: typeof s.description === "string" ? s.description : null,
2376
- baseUrl: typeof s.base_url === "string" ? s.base_url : null,
2429
+ apiBaseUrl: typeof s.api_base_url === "string" ? s.api_base_url : typeof s.apiBaseUrl === "string" ? s.apiBaseUrl : null,
2430
+ launchUrl: typeof s.launch_url === "string" ? s.launch_url : typeof s.launchUrl === "string" ? s.launchUrl : null,
2377
2431
  status: typeof s.status === "string" ? s.status : null,
2378
2432
  surfaceKind: typeof s.surface_kind === "string" ? s.surface_kind : null,
2379
2433
  launchable: s.launchable === true,
@@ -2458,7 +2512,8 @@ var TopoloClient = class _TopoloClient {
2458
2512
  slug: app.id,
2459
2513
  name: app.name ?? app.id,
2460
2514
  description: null,
2461
- baseUrl: app.productionUrl ?? null,
2515
+ apiBaseUrl: null,
2516
+ launchUrl: app.productionUrl ?? null,
2462
2517
  status: "bundled",
2463
2518
  surfaceKind: "application",
2464
2519
  launchable: Boolean(app.productionUrl),
@@ -2502,7 +2557,7 @@ var TopoloClient = class _TopoloClient {
2502
2557
  }
2503
2558
  };
2504
2559
  function resolveConfiguredServiceId(slug, serviceIds) {
2505
- for (const serviceKey of serviceKeyCandidates(slug)) {
2560
+ for (const serviceKey of serviceKeyCandidates2(slug)) {
2506
2561
  const configured = serviceIds?.[serviceKey];
2507
2562
  if (typeof configured === "string" && configured.trim()) return configured.trim();
2508
2563
  const envKey = `TOPOLO_SERVICE_ID_${serviceKey.replace(/\./g, "_").toUpperCase()}`;
@@ -2512,7 +2567,7 @@ function resolveConfiguredServiceId(slug, serviceIds) {
2512
2567
  return null;
2513
2568
  }
2514
2569
  function resolveConfiguredServiceUrl(slug, serviceUrls) {
2515
- for (const serviceKey of serviceKeyCandidates(slug)) {
2570
+ for (const serviceKey of serviceKeyCandidates2(slug)) {
2516
2571
  const configured = serviceUrls?.[serviceKey];
2517
2572
  if (typeof configured === "string" && configured.trim()) return configured.trim();
2518
2573
  const envKey = `TOPOLO_SERVICE_URL_${serviceKey.replace(/\./g, "_").toUpperCase()}`;
@@ -2521,7 +2576,7 @@ function resolveConfiguredServiceUrl(slug, serviceUrls) {
2521
2576
  }
2522
2577
  return null;
2523
2578
  }
2524
- function serviceKeyCandidates(slug) {
2579
+ function serviceKeyCandidates2(slug) {
2525
2580
  const normalized = normalizeLookupKey(slug);
2526
2581
  const withoutTopolo = normalized.startsWith("topolo-") ? normalized.slice("topolo-".length) : normalized;
2527
2582
  return [.../* @__PURE__ */ new Set([
@@ -2543,19 +2598,12 @@ function catalogEntryLookupKeys(entry) {
2543
2598
  for (const value of values) {
2544
2599
  const normalized = normalizeLookupKey(value);
2545
2600
  if (!normalized) continue;
2546
- for (const candidate of serviceKeyCandidates(normalized)) {
2601
+ for (const candidate of serviceKeyCandidates2(normalized)) {
2547
2602
  keys.add(candidate);
2548
2603
  }
2549
2604
  }
2550
2605
  return [...keys];
2551
2606
  }
2552
- function isCallableCatalogEntry(entry) {
2553
- const kind = normalizeLookupKey(entry.surfaceKind || "");
2554
- if (kind === "api" || kind === "technical_service" || kind === "service") return true;
2555
- return Boolean(
2556
- entry.baseUrl && catalogEntryLookupKeys(entry).some((key) => Object.prototype.hasOwnProperty.call(DEFAULT_SERVICE_URLS, key))
2557
- );
2558
- }
2559
2607
  function normalizeActionCatalogEntry(item) {
2560
2608
  if (!item || typeof item !== "object" || Array.isArray(item)) return null;
2561
2609
  const a = item;
@@ -3234,6 +3282,7 @@ function createTopolo(options) {
3234
3282
  defineTopoloRecentActivityWidget,
3235
3283
  defineTopoloStatsWidget,
3236
3284
  requirementsForApplication,
3285
+ resolveCatalogServiceUrl,
3237
3286
  resolveServiceUrl,
3238
3287
  validateTopoloWidgetResponse
3239
3288
  });
package/dist/index.d.cts CHANGED
@@ -104,7 +104,8 @@ interface TopoloServiceCatalogEntry {
104
104
  slug: string | null;
105
105
  name: string | null;
106
106
  description: string | null;
107
- baseUrl: string | null;
107
+ apiBaseUrl?: string | null;
108
+ launchUrl?: string | null;
108
109
  status: string | null;
109
110
  surfaceKind: string | null;
110
111
  launchable: boolean;
@@ -257,7 +258,7 @@ declare const SERVICES: {
257
258
  readonly inventory: "https://inventory.topolo.app";
258
259
  readonly learn: "https://learn-api.topolo.app";
259
260
  readonly localize: "https://localize.topolo.app";
260
- readonly mail: "https://mail.topolo.app";
261
+ readonly mail: "https://mail-api.topolo.app";
261
262
  readonly mdm: "https://mdm-api.topolo.app";
262
263
  readonly messages: "https://messages.topolo.app";
263
264
  readonly nexus: "https://nexus-api.topolo.app";
@@ -889,14 +890,14 @@ declare const APPLICATIONS: {
889
890
  readonly apiUrl: "https://mail.topolo.app";
890
891
  readonly domains: readonly ["mail.topolo.app"];
891
892
  readonly routes: readonly [];
892
- readonly serviceId: "mail";
893
+ readonly serviceId: null;
893
894
  }, {
894
895
  readonly name: "topolo-mail-api";
895
896
  readonly kind: "worker";
896
- readonly apiUrl: null;
897
- readonly domains: readonly [];
897
+ readonly apiUrl: "https://mail-api.topolo.app";
898
+ readonly domains: readonly ["mail-api.topolo.app"];
898
899
  readonly routes: readonly [];
899
- readonly serviceId: null;
900
+ readonly serviceId: "mail";
900
901
  }];
901
902
  };
902
903
  readonly mdm: {
@@ -1579,6 +1580,19 @@ declare function auditAllApplicationRequirements(applicationIds?: readonly Appli
1579
1580
  */
1580
1581
 
1581
1582
  declare const DEFAULT_SERVICE_URLS: Record<ServiceId, string>;
1583
+ interface ServiceCatalogUrlEntry {
1584
+ serviceId?: string | null;
1585
+ slug?: string | null;
1586
+ name?: string | null;
1587
+ apiBaseUrl?: string | null;
1588
+ launchUrl?: string | null;
1589
+ api_base_url?: string | null;
1590
+ launch_url?: string | null;
1591
+ }
1592
+ declare function resolveCatalogServiceUrl(entry: ServiceCatalogUrlEntry, options?: {
1593
+ serviceKey?: string;
1594
+ overrides?: Record<string, string> | Partial<Record<ServiceId, string>>;
1595
+ }): string | null;
1582
1596
  declare function resolveServiceUrl(service: ServiceId, overrides?: Partial<Record<ServiceId, string>>): string;
1583
1597
 
1584
1598
  declare class TopoloSdkError extends Error {
@@ -2085,4 +2099,4 @@ declare function createTopolo(options: TopoloClientOptions): {
2085
2099
  };
2086
2100
  type Topolo = ReturnType<typeof createTopolo>;
2087
2101
 
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 };
2102
+ 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
@@ -104,7 +104,8 @@ interface TopoloServiceCatalogEntry {
104
104
  slug: string | null;
105
105
  name: string | null;
106
106
  description: string | null;
107
- baseUrl: string | null;
107
+ apiBaseUrl?: string | null;
108
+ launchUrl?: string | null;
108
109
  status: string | null;
109
110
  surfaceKind: string | null;
110
111
  launchable: boolean;
@@ -257,7 +258,7 @@ declare const SERVICES: {
257
258
  readonly inventory: "https://inventory.topolo.app";
258
259
  readonly learn: "https://learn-api.topolo.app";
259
260
  readonly localize: "https://localize.topolo.app";
260
- readonly mail: "https://mail.topolo.app";
261
+ readonly mail: "https://mail-api.topolo.app";
261
262
  readonly mdm: "https://mdm-api.topolo.app";
262
263
  readonly messages: "https://messages.topolo.app";
263
264
  readonly nexus: "https://nexus-api.topolo.app";
@@ -889,14 +890,14 @@ declare const APPLICATIONS: {
889
890
  readonly apiUrl: "https://mail.topolo.app";
890
891
  readonly domains: readonly ["mail.topolo.app"];
891
892
  readonly routes: readonly [];
892
- readonly serviceId: "mail";
893
+ readonly serviceId: null;
893
894
  }, {
894
895
  readonly name: "topolo-mail-api";
895
896
  readonly kind: "worker";
896
- readonly apiUrl: null;
897
- readonly domains: readonly [];
897
+ readonly apiUrl: "https://mail-api.topolo.app";
898
+ readonly domains: readonly ["mail-api.topolo.app"];
898
899
  readonly routes: readonly [];
899
- readonly serviceId: null;
900
+ readonly serviceId: "mail";
900
901
  }];
901
902
  };
902
903
  readonly mdm: {
@@ -1579,6 +1580,19 @@ declare function auditAllApplicationRequirements(applicationIds?: readonly Appli
1579
1580
  */
1580
1581
 
1581
1582
  declare const DEFAULT_SERVICE_URLS: Record<ServiceId, string>;
1583
+ interface ServiceCatalogUrlEntry {
1584
+ serviceId?: string | null;
1585
+ slug?: string | null;
1586
+ name?: string | null;
1587
+ apiBaseUrl?: string | null;
1588
+ launchUrl?: string | null;
1589
+ api_base_url?: string | null;
1590
+ launch_url?: string | null;
1591
+ }
1592
+ declare function resolveCatalogServiceUrl(entry: ServiceCatalogUrlEntry, options?: {
1593
+ serviceKey?: string;
1594
+ overrides?: Record<string, string> | Partial<Record<ServiceId, string>>;
1595
+ }): string | null;
1582
1596
  declare function resolveServiceUrl(service: ServiceId, overrides?: Partial<Record<ServiceId, string>>): string;
1583
1597
 
1584
1598
  declare class TopoloSdkError extends Error {
@@ -2085,4 +2099,4 @@ declare function createTopolo(options: TopoloClientOptions): {
2085
2099
  };
2086
2100
  type Topolo = ReturnType<typeof createTopolo>;
2087
2101
 
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 };
2102
+ 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
@@ -82,7 +82,7 @@ var SERVICES = {
82
82
  inventory: "https://inventory.topolo.app",
83
83
  learn: "https://learn-api.topolo.app",
84
84
  localize: "https://localize.topolo.app",
85
- mail: "https://mail.topolo.app",
85
+ mail: "https://mail-api.topolo.app",
86
86
  mdm: "https://mdm-api.topolo.app",
87
87
  messages: "https://messages.topolo.app",
88
88
  nexus: "https://nexus-api.topolo.app",
@@ -920,15 +920,17 @@ var APPLICATIONS = {
920
920
  "mail.topolo.app"
921
921
  ],
922
922
  "routes": [],
923
- "serviceId": "mail"
923
+ "serviceId": null
924
924
  },
925
925
  {
926
926
  "name": "topolo-mail-api",
927
927
  "kind": "worker",
928
- "apiUrl": null,
929
- "domains": [],
928
+ "apiUrl": "https://mail-api.topolo.app",
929
+ "domains": [
930
+ "mail-api.topolo.app"
931
+ ],
930
932
  "routes": [],
931
- "serviceId": null
933
+ "serviceId": "mail"
932
934
  }
933
935
  ]
934
936
  },
@@ -2045,14 +2047,60 @@ function statusRank(status) {
2045
2047
 
2046
2048
  // src/services.ts
2047
2049
  var DEFAULT_SERVICE_URLS = SERVICES;
2050
+ function resolveCatalogServiceUrl(entry, options = {}) {
2051
+ const keys = catalogServiceKeyCandidates(entry, options.serviceKey);
2052
+ const override = resolveServiceUrlOverride(keys, options.overrides);
2053
+ if (override) return override;
2054
+ const apiBaseUrl = firstString(entry.apiBaseUrl, entry.api_base_url);
2055
+ if (apiBaseUrl) return apiBaseUrl;
2056
+ return null;
2057
+ }
2048
2058
  function resolveServiceUrl(service, overrides) {
2049
- const override = overrides?.[service];
2059
+ const override = resolveServiceUrlOverride([service], overrides);
2050
2060
  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
2061
  return DEFAULT_SERVICE_URLS[service];
2055
2062
  }
2063
+ function catalogServiceKeyCandidates(entry, preferred) {
2064
+ const values = [preferred, entry.slug, entry.name, entry.serviceId].filter(
2065
+ (value) => typeof value === "string" && value.trim().length > 0
2066
+ );
2067
+ const keys = /* @__PURE__ */ new Set();
2068
+ for (const value of values) {
2069
+ for (const candidate of serviceKeyCandidates(value)) keys.add(candidate);
2070
+ }
2071
+ return [...keys];
2072
+ }
2073
+ function serviceKeyCandidates(value) {
2074
+ const normalized = value.trim().toLowerCase().replace(/\s+/g, "-");
2075
+ const withoutTopolo = normalized.startsWith("topolo-") ? normalized.slice("topolo-".length) : normalized;
2076
+ return [.../* @__PURE__ */ new Set([
2077
+ normalized,
2078
+ normalized.replace(/-/g, "_"),
2079
+ withoutTopolo,
2080
+ withoutTopolo.replace(/-/g, "_")
2081
+ ])].filter(Boolean);
2082
+ }
2083
+ function resolveServiceUrlOverride(keys, overrides) {
2084
+ for (const key of keys) {
2085
+ const direct = overrides?.[key];
2086
+ if (direct) return direct;
2087
+ const envStyleKey = key.replace(/\./g, "_");
2088
+ const envStyle = overrides?.[envStyleKey];
2089
+ if (envStyle) return envStyle;
2090
+ }
2091
+ for (const key of keys) {
2092
+ const envKey = `TOPOLO_SERVICE_URL_${key.replace(/\./g, "_").toUpperCase()}`;
2093
+ const envValue = typeof process !== "undefined" ? process.env?.[envKey] : void 0;
2094
+ if (envValue) return envValue;
2095
+ }
2096
+ return null;
2097
+ }
2098
+ function firstString(...values) {
2099
+ for (const value of values) {
2100
+ if (typeof value === "string" && value.trim()) return value.trim();
2101
+ }
2102
+ return null;
2103
+ }
2056
2104
 
2057
2105
  // src/client.ts
2058
2106
  var WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
@@ -2225,8 +2273,12 @@ var TopoloClient = class _TopoloClient {
2225
2273
  async resolveLiveCallableService(serviceKey) {
2226
2274
  const catalog = await this.listServices();
2227
2275
  const match = catalog.find((entry) => catalogEntryMatches(entry, serviceKey));
2228
- if (!match?.baseUrl || !isCallableCatalogEntry(match)) return null;
2229
- return { serviceKey, baseUrl: match.baseUrl };
2276
+ if (!match) return null;
2277
+ const baseUrl = resolveCatalogServiceUrl(match, {
2278
+ serviceKey,
2279
+ overrides: this.serviceUrls
2280
+ });
2281
+ return baseUrl ? { serviceKey, baseUrl } : null;
2230
2282
  }
2231
2283
  /**
2232
2284
  * Resolve a service-slug → opaque service-id via caller config or TopoloAuth.
@@ -2319,7 +2371,8 @@ var TopoloClient = class _TopoloClient {
2319
2371
  slug: typeof s.app_slug === "string" ? s.app_slug : null,
2320
2372
  name: typeof s.name === "string" ? s.name : null,
2321
2373
  description: typeof s.description === "string" ? s.description : null,
2322
- baseUrl: typeof s.base_url === "string" ? s.base_url : null,
2374
+ apiBaseUrl: typeof s.api_base_url === "string" ? s.api_base_url : typeof s.apiBaseUrl === "string" ? s.apiBaseUrl : null,
2375
+ launchUrl: typeof s.launch_url === "string" ? s.launch_url : typeof s.launchUrl === "string" ? s.launchUrl : null,
2323
2376
  status: typeof s.status === "string" ? s.status : null,
2324
2377
  surfaceKind: typeof s.surface_kind === "string" ? s.surface_kind : null,
2325
2378
  launchable: s.launchable === true,
@@ -2404,7 +2457,8 @@ var TopoloClient = class _TopoloClient {
2404
2457
  slug: app.id,
2405
2458
  name: app.name ?? app.id,
2406
2459
  description: null,
2407
- baseUrl: app.productionUrl ?? null,
2460
+ apiBaseUrl: null,
2461
+ launchUrl: app.productionUrl ?? null,
2408
2462
  status: "bundled",
2409
2463
  surfaceKind: "application",
2410
2464
  launchable: Boolean(app.productionUrl),
@@ -2448,7 +2502,7 @@ var TopoloClient = class _TopoloClient {
2448
2502
  }
2449
2503
  };
2450
2504
  function resolveConfiguredServiceId(slug, serviceIds) {
2451
- for (const serviceKey of serviceKeyCandidates(slug)) {
2505
+ for (const serviceKey of serviceKeyCandidates2(slug)) {
2452
2506
  const configured = serviceIds?.[serviceKey];
2453
2507
  if (typeof configured === "string" && configured.trim()) return configured.trim();
2454
2508
  const envKey = `TOPOLO_SERVICE_ID_${serviceKey.replace(/\./g, "_").toUpperCase()}`;
@@ -2458,7 +2512,7 @@ function resolveConfiguredServiceId(slug, serviceIds) {
2458
2512
  return null;
2459
2513
  }
2460
2514
  function resolveConfiguredServiceUrl(slug, serviceUrls) {
2461
- for (const serviceKey of serviceKeyCandidates(slug)) {
2515
+ for (const serviceKey of serviceKeyCandidates2(slug)) {
2462
2516
  const configured = serviceUrls?.[serviceKey];
2463
2517
  if (typeof configured === "string" && configured.trim()) return configured.trim();
2464
2518
  const envKey = `TOPOLO_SERVICE_URL_${serviceKey.replace(/\./g, "_").toUpperCase()}`;
@@ -2467,7 +2521,7 @@ function resolveConfiguredServiceUrl(slug, serviceUrls) {
2467
2521
  }
2468
2522
  return null;
2469
2523
  }
2470
- function serviceKeyCandidates(slug) {
2524
+ function serviceKeyCandidates2(slug) {
2471
2525
  const normalized = normalizeLookupKey(slug);
2472
2526
  const withoutTopolo = normalized.startsWith("topolo-") ? normalized.slice("topolo-".length) : normalized;
2473
2527
  return [.../* @__PURE__ */ new Set([
@@ -2489,19 +2543,12 @@ function catalogEntryLookupKeys(entry) {
2489
2543
  for (const value of values) {
2490
2544
  const normalized = normalizeLookupKey(value);
2491
2545
  if (!normalized) continue;
2492
- for (const candidate of serviceKeyCandidates(normalized)) {
2546
+ for (const candidate of serviceKeyCandidates2(normalized)) {
2493
2547
  keys.add(candidate);
2494
2548
  }
2495
2549
  }
2496
2550
  return [...keys];
2497
2551
  }
2498
- function isCallableCatalogEntry(entry) {
2499
- const kind = normalizeLookupKey(entry.surfaceKind || "");
2500
- if (kind === "api" || kind === "technical_service" || kind === "service") return true;
2501
- return Boolean(
2502
- entry.baseUrl && catalogEntryLookupKeys(entry).some((key) => Object.prototype.hasOwnProperty.call(DEFAULT_SERVICE_URLS, key))
2503
- );
2504
- }
2505
2552
  function normalizeActionCatalogEntry(item) {
2506
2553
  if (!item || typeof item !== "object" || Array.isArray(item)) return null;
2507
2554
  const a = item;
@@ -3179,6 +3226,7 @@ export {
3179
3226
  defineTopoloRecentActivityWidget,
3180
3227
  defineTopoloStatsWidget,
3181
3228
  requirementsForApplication,
3229
+ resolveCatalogServiceUrl,
3182
3230
  resolveServiceUrl,
3183
3231
  validateTopoloWidgetResponse
3184
3232
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@topolo/sdk",
3
- "version": "0.4.4",
3
+ "version": "0.4.6",
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",
@@ -813,15 +813,17 @@ export const APPLICATIONS = {
813
813
  "mail.topolo.app"
814
814
  ],
815
815
  "routes": [],
816
- "serviceId": "mail"
816
+ "serviceId": null
817
817
  },
818
818
  {
819
819
  "name": "topolo-mail-api",
820
820
  "kind": "worker",
821
- "apiUrl": null,
822
- "domains": [],
821
+ "apiUrl": "https://mail-api.topolo.app",
822
+ "domains": [
823
+ "mail-api.topolo.app"
824
+ ],
823
825
  "routes": [],
824
- "serviceId": null
826
+ "serviceId": "mail"
825
827
  }
826
828
  ]
827
829
  },
@@ -25,7 +25,8 @@ const MAIL_CATALOG = {
25
25
  service_id: 'srv_mail',
26
26
  app_slug: 'topolo-mail',
27
27
  name: 'Topolo Mail',
28
- base_url: 'https://mail-api.topolo.app',
28
+ api_base_url: 'https://mail-api.topolo.app',
29
+ launch_url: 'https://mail.topolo.app',
29
30
  surface_kind: 'api',
30
31
  status: 'active',
31
32
  granted_permissions: ['srv_mail.messages:read'],
@@ -376,7 +377,7 @@ describe('TopoloClient.getServiceIdBySlug', () => {
376
377
 
377
378
  it('falls back to /api/services/by-slug/:slug for unknown slugs', async () => {
378
379
  const { impl, calls } = stubFetch([
379
- { body: { success: true, data: { service_id: 'service_abc123', base_url: 'https://x' } } },
380
+ { body: { success: true, data: { service_id: 'service_abc123', api_base_url: 'https://x-api' } } },
380
381
  ]);
381
382
  const client = freshClient(impl, { auth: 'https://auth.example.com' });
382
383
  const id = await client.getServiceIdBySlug('external-service');
@@ -452,7 +453,8 @@ describe('TopoloClient.listServices', () => {
452
453
  app_slug: 'topolo-director',
453
454
  name: 'Topolo Director',
454
455
  description: 'Demo control plane',
455
- base_url: 'https://director.topolo.app',
456
+ api_base_url: 'https://director-api.topolo.app',
457
+ launch_url: 'https://director.topolo.app',
456
458
  status: 'active',
457
459
  surface_kind: 'application',
458
460
  launchable: true,
@@ -476,7 +478,8 @@ describe('TopoloClient.listServices', () => {
476
478
  slug: 'topolo-director',
477
479
  name: 'Topolo Director',
478
480
  description: 'Demo control plane',
479
- baseUrl: 'https://director.topolo.app',
481
+ apiBaseUrl: 'https://director-api.topolo.app',
482
+ launchUrl: 'https://director.topolo.app',
480
483
  status: 'active',
481
484
  surfaceKind: 'application',
482
485
  launchable: true,
@@ -512,7 +515,8 @@ describe('TopoloClient.listServices', () => {
512
515
  service_id: 'srv_mail',
513
516
  app_slug: 'topolo-mail',
514
517
  name: 'Topolo Mail',
515
- base_url: 'https://mail.topolo.app',
518
+ api_base_url: 'https://mail-api.topolo.app',
519
+ launch_url: 'https://mail.topolo.app',
516
520
  surface_kind: 'application',
517
521
  status: 'active',
518
522
  granted_permissions: ['srv_mail.messages:read'],
@@ -533,7 +537,76 @@ describe('TopoloClient.listServices', () => {
533
537
  await client.request({ service: 'mail', path: '/api/health' });
534
538
 
535
539
  expect(calls[0]!.url).toBe('https://auth.example.com/api/services/catalog');
536
- expect(calls[1]!.url).toBe('https://mail.topolo.app/api/health');
540
+ expect(calls[1]!.url).toBe('https://mail-api.topolo.app/api/health');
541
+ });
542
+
543
+ it('routes known split-surface services to the callable API URL', async () => {
544
+ const { impl, calls } = stubFetch([
545
+ {
546
+ body: {
547
+ data: {
548
+ services: [
549
+ {
550
+ service_id: 'srv_crm',
551
+ app_slug: 'topolo-crm',
552
+ name: 'Topolo CRM',
553
+ api_base_url: 'https://crm-api.topolo.app',
554
+ launch_url: 'https://crm.topolo.app',
555
+ surface_kind: 'application',
556
+ status: 'active',
557
+ granted_permissions: ['srv_crm.contacts:read'],
558
+ },
559
+ ],
560
+ },
561
+ },
562
+ },
563
+ { body: { ok: true } },
564
+ ]);
565
+ const client = new TopoloClient({
566
+ credential: { kind: 'api_key', apiKey: 'dak_xyz' },
567
+ agent: AGENT,
568
+ fetch: impl,
569
+ serviceUrls: { auth: 'https://auth.example.com' },
570
+ });
571
+
572
+ await client.request({ service: 'crm', path: '/health' });
573
+
574
+ expect(calls[0]!.url).toBe('https://auth.example.com/api/services/catalog');
575
+ expect(calls[1]!.url).toBe('https://crm-api.topolo.app/health');
576
+ });
577
+
578
+ it('prefers live catalog api_base_url for dynamic services', async () => {
579
+ const { impl, calls } = stubFetch([
580
+ {
581
+ body: {
582
+ data: {
583
+ services: [
584
+ {
585
+ service_id: 'srv_future',
586
+ app_slug: 'topolo-future',
587
+ name: 'Future',
588
+ api_base_url: 'https://future-api.topolo.app',
589
+ launch_url: 'https://future.topolo.app',
590
+ surface_kind: 'api',
591
+ status: 'active',
592
+ granted_permissions: ['srv_future.read:all'],
593
+ },
594
+ ],
595
+ },
596
+ },
597
+ },
598
+ { body: { ok: true } },
599
+ ]);
600
+ const client = new TopoloClient({
601
+ credential: { kind: 'api_key', apiKey: 'dak_xyz' },
602
+ agent: AGENT,
603
+ fetch: impl,
604
+ serviceUrls: { auth: 'https://auth.example.com' },
605
+ });
606
+
607
+ await client.request({ service: 'future', path: '/health' });
608
+
609
+ expect(calls[1]!.url).toBe('https://future-api.topolo.app/health');
537
610
  });
538
611
 
539
612
  it('refuses a bundled static service that is absent from the scoped live catalog', async () => {
@@ -546,7 +619,8 @@ describe('TopoloClient.listServices', () => {
546
619
  service_id: 'srv_crm',
547
620
  app_slug: 'topolo-crm',
548
621
  name: 'Topolo CRM',
549
- base_url: 'https://crm-api.topolo.app',
622
+ api_base_url: 'https://crm-api.topolo.app',
623
+ launch_url: 'https://crm.topolo.app',
550
624
  surface_kind: 'api',
551
625
  status: 'active',
552
626
  granted_permissions: ['srv_crm.contacts:read'],
@@ -579,7 +653,7 @@ describe('TopoloClient.listServices', () => {
579
653
  service_id: 'srv_future_app',
580
654
  app_slug: 'topolo-future-app',
581
655
  name: 'Future App',
582
- base_url: 'https://future-app.topolo.app',
656
+ launch_url: 'https://future-app.topolo.app',
583
657
  surface_kind: 'application',
584
658
  status: 'active',
585
659
  granted_permissions: ['srv_future_app.launch:read'],
@@ -612,7 +686,8 @@ describe('TopoloClient.listServices', () => {
612
686
  service_id: 'srv_future',
613
687
  app_slug: 'topolo-warehouse',
614
688
  name: 'Warehouse',
615
- base_url: 'https://warehouse.topolo.app',
689
+ api_base_url: 'https://warehouse.topolo.app',
690
+ launch_url: 'https://warehouse.topolo.app',
616
691
  surface_kind: 'api',
617
692
  status: 'active',
618
693
  granted_permissions: ['srv_future.read:all'],
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';
@@ -105,7 +106,8 @@ export interface TopoloServiceCatalogEntry {
105
106
  slug: string | null;
106
107
  name: string | null;
107
108
  description: string | null;
108
- baseUrl: string | null;
109
+ apiBaseUrl?: string | null;
110
+ launchUrl?: string | null;
109
111
  status: string | null;
110
112
  surfaceKind: string | null;
111
113
  launchable: boolean;
@@ -325,8 +327,12 @@ export class TopoloClient {
325
327
  private async resolveLiveCallableService(serviceKey: string): Promise<{ serviceKey: string; baseUrl: string } | null> {
326
328
  const catalog = await this.listServices();
327
329
  const match = catalog.find((entry) => catalogEntryMatches(entry, serviceKey));
328
- if (!match?.baseUrl || !isCallableCatalogEntry(match)) return null;
329
- return { serviceKey, baseUrl: match.baseUrl };
330
+ if (!match) return null;
331
+ const baseUrl = resolveCatalogServiceUrl(match, {
332
+ serviceKey,
333
+ overrides: this.serviceUrls,
334
+ });
335
+ return baseUrl ? { serviceKey, baseUrl } : null;
330
336
  }
331
337
 
332
338
  /**
@@ -447,7 +453,18 @@ export class TopoloClient {
447
453
  name: typeof s.name === 'string' ? s.name : null,
448
454
  description:
449
455
  typeof s.description === 'string' ? s.description : null,
450
- baseUrl: typeof s.base_url === 'string' ? s.base_url : null,
456
+ apiBaseUrl:
457
+ typeof s.api_base_url === 'string'
458
+ ? s.api_base_url
459
+ : typeof s.apiBaseUrl === 'string'
460
+ ? s.apiBaseUrl
461
+ : null,
462
+ launchUrl:
463
+ typeof s.launch_url === 'string'
464
+ ? s.launch_url
465
+ : typeof s.launchUrl === 'string'
466
+ ? s.launchUrl
467
+ : null,
451
468
  status: typeof s.status === 'string' ? s.status : null,
452
469
  surfaceKind:
453
470
  typeof s.surface_kind === 'string' ? s.surface_kind : null,
@@ -554,7 +571,8 @@ export class TopoloClient {
554
571
  slug: app.id,
555
572
  name: app.name ?? app.id,
556
573
  description: null,
557
- baseUrl: app.productionUrl ?? null,
574
+ apiBaseUrl: null,
575
+ launchUrl: app.productionUrl ?? null,
558
576
  status: 'bundled',
559
577
  surfaceKind: 'application',
560
578
  launchable: Boolean(app.productionUrl),
@@ -674,15 +692,6 @@ function catalogEntryLookupKeys(entry: TopoloServiceCatalogEntry): string[] {
674
692
  return [...keys];
675
693
  }
676
694
 
677
- function isCallableCatalogEntry(entry: TopoloServiceCatalogEntry): boolean {
678
- const kind = normalizeLookupKey(entry.surfaceKind || '');
679
- if (kind === 'api' || kind === 'technical_service' || kind === 'service') return true;
680
- return Boolean(
681
- entry.baseUrl &&
682
- catalogEntryLookupKeys(entry).some((key) => Object.prototype.hasOwnProperty.call(DEFAULT_SERVICE_URLS, key)),
683
- );
684
- }
685
-
686
695
  function normalizeActionCatalogEntry(item: unknown): TopoloActionCatalogEntry | null {
687
696
  if (!item || typeof item !== 'object' || Array.isArray(item)) return null;
688
697
  const a = item as Record<string, unknown>;
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 {
@@ -22,7 +22,8 @@ function stubFetch(responses: Array<{ status?: number; body?: unknown }>) {
22
22
  service_id: 'srv_p2p',
23
23
  app_slug: 'topolo-p2p',
24
24
  name: 'Topolo P2P',
25
- base_url: 'https://p2p.topolo.app',
25
+ api_base_url: 'https://p2p.topolo.app',
26
+ launch_url: 'https://p2p.topolo.app',
26
27
  surface_kind: 'api',
27
28
  status: 'active',
28
29
  granted_permissions: ['srv_p2p.directory:read'],
@@ -27,7 +27,7 @@ export const SERVICES = {
27
27
  inventory: "https://inventory.topolo.app",
28
28
  learn: "https://learn-api.topolo.app",
29
29
  localize: "https://localize.topolo.app",
30
- mail: "https://mail.topolo.app",
30
+ mail: "https://mail-api.topolo.app",
31
31
  mdm: "https://mdm-api.topolo.app",
32
32
  messages: "https://messages.topolo.app",
33
33
  nexus: "https://nexus-api.topolo.app",
@@ -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
 
@@ -69,7 +70,7 @@ describe('service registry', () => {
69
70
  books: 'https://books.topolo.app',
70
71
  consent: 'https://consent.topolo.app',
71
72
  feed: 'https://feed-api.topolo.app',
72
- mail: 'https://mail.topolo.app',
73
+ mail: 'https://mail-api.topolo.app',
73
74
  one: 'https://one.topolo.app',
74
75
  web: 'https://web-api.topolo.app',
75
76
  });
@@ -97,6 +98,35 @@ describe('service registry', () => {
97
98
  expect(applicationServices.has(serviceId)).toBe(true);
98
99
  }
99
100
  });
101
+
102
+ it('resolves services from explicit catalog API URLs', () => {
103
+ expect(resolveCatalogServiceUrl({
104
+ serviceId: 'srv_crm',
105
+ slug: 'topolo-crm',
106
+ name: 'Topolo CRM',
107
+ launchUrl: 'https://crm.topolo.app',
108
+ apiBaseUrl: 'https://crm-api.topolo.app',
109
+ }, { serviceKey: 'crm' })).toBe('https://crm-api.topolo.app');
110
+ });
111
+
112
+ it('does not infer callable URLs from launcher URLs', () => {
113
+ expect(resolveCatalogServiceUrl({
114
+ serviceId: 'srv_crm',
115
+ slug: 'topolo-crm',
116
+ name: 'Topolo CRM',
117
+ launchUrl: 'https://crm.stg.topolo.us',
118
+ }, { serviceKey: 'crm' })).toBeNull();
119
+ });
120
+
121
+ it('prefers explicit catalog API URLs over launch URLs', () => {
122
+ expect(resolveCatalogServiceUrl({
123
+ serviceId: 'srv_future',
124
+ slug: 'topolo-future',
125
+ name: 'Future',
126
+ launchUrl: 'https://future.topolo.app',
127
+ apiBaseUrl: 'https://future-api.topolo.app',
128
+ })).toBe('https://future-api.topolo.app');
129
+ });
100
130
  });
101
131
 
102
132
  describe('application catalog', () => {
package/src/services.ts CHANGED
@@ -47,16 +47,92 @@ 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
+ apiBaseUrl?: string | null;
55
+ launchUrl?: string | null;
56
+ api_base_url?: string | null;
57
+ launch_url?: string | null;
58
+ }
59
+
60
+ export function resolveCatalogServiceUrl(
61
+ entry: ServiceCatalogUrlEntry,
62
+ options: {
63
+ serviceKey?: string;
64
+ overrides?: Record<string, string> | Partial<Record<ServiceId, string>>;
65
+ } = {},
66
+ ): string | null {
67
+ const keys = catalogServiceKeyCandidates(entry, options.serviceKey);
68
+ const override = resolveServiceUrlOverride(keys, options.overrides);
69
+ if (override) return override;
70
+
71
+ const apiBaseUrl = firstString(entry.apiBaseUrl, entry.api_base_url);
72
+ if (apiBaseUrl) return apiBaseUrl;
73
+
74
+ return null;
75
+ }
76
+
50
77
  export function resolveServiceUrl(
51
78
  service: ServiceId,
52
79
  overrides?: Partial<Record<ServiceId, string>>,
53
80
  ): string {
54
- const override = overrides?.[service];
81
+ const override = resolveServiceUrlOverride([service], overrides);
55
82
  if (override) return override;
56
83
 
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
84
  return DEFAULT_SERVICE_URLS[service];
62
85
  }
86
+
87
+ function catalogServiceKeyCandidates(entry: ServiceCatalogUrlEntry, preferred?: string): string[] {
88
+ const values = [preferred, entry.slug, entry.name, entry.serviceId].filter(
89
+ (value): value is string => typeof value === 'string' && value.trim().length > 0,
90
+ );
91
+ const keys = new Set<string>();
92
+ for (const value of values) {
93
+ for (const candidate of serviceKeyCandidates(value)) keys.add(candidate);
94
+ }
95
+ return [...keys];
96
+ }
97
+
98
+ function serviceKeyCandidates(value: string): string[] {
99
+ const normalized = value.trim().toLowerCase().replace(/\s+/g, '-');
100
+ const withoutTopolo = normalized.startsWith('topolo-')
101
+ ? normalized.slice('topolo-'.length)
102
+ : normalized;
103
+ return [...new Set([
104
+ normalized,
105
+ normalized.replace(/-/g, '_'),
106
+ withoutTopolo,
107
+ withoutTopolo.replace(/-/g, '_'),
108
+ ])].filter(Boolean);
109
+ }
110
+
111
+ function resolveServiceUrlOverride(
112
+ keys: string[],
113
+ overrides?: Record<string, string> | Partial<Record<ServiceId, string>>,
114
+ ): string | null {
115
+ for (const key of keys) {
116
+ const direct = overrides?.[key as ServiceId];
117
+ if (direct) return direct;
118
+
119
+ const envStyleKey = key.replace(/\./g, '_');
120
+ const envStyle = overrides?.[envStyleKey as ServiceId];
121
+ if (envStyle) return envStyle;
122
+ }
123
+
124
+ for (const key of keys) {
125
+ const envKey = `TOPOLO_SERVICE_URL_${key.replace(/\./g, '_').toUpperCase()}`;
126
+ const envValue = typeof process !== 'undefined' ? process.env?.[envKey] : undefined;
127
+ if (envValue) return envValue;
128
+ }
129
+
130
+ return null;
131
+ }
132
+
133
+ function firstString(...values: Array<string | null | undefined>): string | null {
134
+ for (const value of values) {
135
+ if (typeof value === 'string' && value.trim()) return value.trim();
136
+ }
137
+ return null;
138
+ }