@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 +92 -11
- package/dist/index.d.cts +17 -1
- package/dist/index.d.ts +17 -1
- package/dist/index.js +91 -11
- package/package.json +1 -1
- package/src/client.test.ts +71 -1
- package/src/client.ts +24 -3
- package/src/index.ts +2 -0
- package/src/services.test.ts +29 -0
- package/src/services.ts +111 -5
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 =
|
|
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
|
|
2283
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
2229
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
package/src/client.test.ts
CHANGED
|
@@ -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
|
|
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
|
|
329
|
-
|
|
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 {
|
package/src/services.test.ts
CHANGED
|
@@ -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 =
|
|
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
|
+
}
|