@topolo/sdk 0.4.3 → 0.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +98 -11
- package/dist/index.d.cts +17 -1
- package/dist/index.d.ts +17 -1
- package/dist/index.js +97 -11
- package/package.json +1 -1
- package/src/client.test.ts +74 -1
- package/src/client.ts +30 -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,17 +2100,90 @@ function statusRank(status) {
|
|
|
2099
2100
|
|
|
2100
2101
|
// src/services.ts
|
|
2101
2102
|
var DEFAULT_SERVICE_URLS = SERVICES;
|
|
2103
|
+
function resolveCatalogServiceUrl(entry, options = {}) {
|
|
2104
|
+
const keys = catalogServiceKeyCandidates(entry, options.serviceKey);
|
|
2105
|
+
const override = resolveServiceUrlOverride(keys, options.overrides);
|
|
2106
|
+
if (override) return override;
|
|
2107
|
+
const apiBaseUrl = firstString(entry.apiBaseUrl, entry.api_base_url);
|
|
2108
|
+
if (apiBaseUrl) return apiBaseUrl;
|
|
2109
|
+
const bundledUrl = firstBundledServiceUrl(keys);
|
|
2110
|
+
const liveUrl = firstString(entry.baseUrl);
|
|
2111
|
+
if (bundledUrl && (!liveUrl || sameUrlEnvironment(bundledUrl, liveUrl))) return bundledUrl;
|
|
2112
|
+
return liveUrl ?? bundledUrl ?? null;
|
|
2113
|
+
}
|
|
2102
2114
|
function resolveServiceUrl(service, overrides) {
|
|
2103
|
-
const override =
|
|
2115
|
+
const override = resolveServiceUrlOverride([service], overrides);
|
|
2104
2116
|
if (override) return override;
|
|
2105
|
-
const envKey = `TOPOLO_SERVICE_URL_${service.replace(/\./g, "_").toUpperCase()}`;
|
|
2106
|
-
const envValue = typeof process !== "undefined" ? process.env?.[envKey] : void 0;
|
|
2107
|
-
if (envValue) return envValue;
|
|
2108
2117
|
return DEFAULT_SERVICE_URLS[service];
|
|
2109
2118
|
}
|
|
2119
|
+
function catalogServiceKeyCandidates(entry, preferred) {
|
|
2120
|
+
const values = [preferred, entry.slug, entry.name, entry.serviceId].filter(
|
|
2121
|
+
(value) => typeof value === "string" && value.trim().length > 0
|
|
2122
|
+
);
|
|
2123
|
+
const keys = /* @__PURE__ */ new Set();
|
|
2124
|
+
for (const value of values) {
|
|
2125
|
+
for (const candidate of serviceKeyCandidates(value)) keys.add(candidate);
|
|
2126
|
+
}
|
|
2127
|
+
return [...keys];
|
|
2128
|
+
}
|
|
2129
|
+
function serviceKeyCandidates(value) {
|
|
2130
|
+
const normalized = value.trim().toLowerCase().replace(/\s+/g, "-");
|
|
2131
|
+
const withoutTopolo = normalized.startsWith("topolo-") ? normalized.slice("topolo-".length) : normalized;
|
|
2132
|
+
return [.../* @__PURE__ */ new Set([
|
|
2133
|
+
normalized,
|
|
2134
|
+
normalized.replace(/-/g, "_"),
|
|
2135
|
+
withoutTopolo,
|
|
2136
|
+
withoutTopolo.replace(/-/g, "_")
|
|
2137
|
+
])].filter(Boolean);
|
|
2138
|
+
}
|
|
2139
|
+
function resolveServiceUrlOverride(keys, overrides) {
|
|
2140
|
+
for (const key of keys) {
|
|
2141
|
+
const direct = overrides?.[key];
|
|
2142
|
+
if (direct) return direct;
|
|
2143
|
+
const envStyleKey = key.replace(/\./g, "_");
|
|
2144
|
+
const envStyle = overrides?.[envStyleKey];
|
|
2145
|
+
if (envStyle) return envStyle;
|
|
2146
|
+
}
|
|
2147
|
+
for (const key of keys) {
|
|
2148
|
+
const envKey = `TOPOLO_SERVICE_URL_${key.replace(/\./g, "_").toUpperCase()}`;
|
|
2149
|
+
const envValue = typeof process !== "undefined" ? process.env?.[envKey] : void 0;
|
|
2150
|
+
if (envValue) return envValue;
|
|
2151
|
+
}
|
|
2152
|
+
return null;
|
|
2153
|
+
}
|
|
2154
|
+
function firstBundledServiceUrl(keys) {
|
|
2155
|
+
for (const key of keys) {
|
|
2156
|
+
const url = DEFAULT_SERVICE_URLS[key];
|
|
2157
|
+
if (url) return url;
|
|
2158
|
+
}
|
|
2159
|
+
return null;
|
|
2160
|
+
}
|
|
2161
|
+
function firstString(...values) {
|
|
2162
|
+
for (const value of values) {
|
|
2163
|
+
if (typeof value === "string" && value.trim()) return value.trim();
|
|
2164
|
+
}
|
|
2165
|
+
return null;
|
|
2166
|
+
}
|
|
2167
|
+
function sameUrlEnvironment(left, right) {
|
|
2168
|
+
return urlEnvironment(left) === urlEnvironment(right);
|
|
2169
|
+
}
|
|
2170
|
+
function urlEnvironment(value) {
|
|
2171
|
+
try {
|
|
2172
|
+
const url = new URL(value);
|
|
2173
|
+
const host = url.hostname.toLowerCase();
|
|
2174
|
+
if (host === "localhost" || host === "127.0.0.1") return "local";
|
|
2175
|
+
if (host.endsWith(".stg.topolo.us") || host.includes("topolo-staging")) return "staging";
|
|
2176
|
+
if (host.endsWith(".topolo.app") || host.endsWith(".topolo.us")) return "production";
|
|
2177
|
+
return "external";
|
|
2178
|
+
} catch {
|
|
2179
|
+
return "external";
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2110
2182
|
|
|
2111
2183
|
// src/client.ts
|
|
2112
2184
|
var WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
2185
|
+
var TOPOLO_ACTION_ID_HEADER = "X-Topolo-Action-Id";
|
|
2186
|
+
var TOPOLO_ACTION_NAME_HEADER = "X-Topolo-Action-Name";
|
|
2113
2187
|
var POSITIVE_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
2114
2188
|
var NEGATIVE_CACHE_TTL_MS = 60 * 1e3;
|
|
2115
2189
|
var CATALOG_CACHE_TTL_MS = 60 * 1e3;
|
|
@@ -2277,8 +2351,12 @@ var TopoloClient = class _TopoloClient {
|
|
|
2277
2351
|
async resolveLiveCallableService(serviceKey) {
|
|
2278
2352
|
const catalog = await this.listServices();
|
|
2279
2353
|
const match = catalog.find((entry) => catalogEntryMatches(entry, serviceKey));
|
|
2280
|
-
if (!match
|
|
2281
|
-
|
|
2354
|
+
if (!match || !isCallableCatalogEntry(match)) return null;
|
|
2355
|
+
const baseUrl = resolveCatalogServiceUrl(match, {
|
|
2356
|
+
serviceKey,
|
|
2357
|
+
overrides: this.serviceUrls
|
|
2358
|
+
});
|
|
2359
|
+
return baseUrl ? { serviceKey, baseUrl } : null;
|
|
2282
2360
|
}
|
|
2283
2361
|
/**
|
|
2284
2362
|
* Resolve a service-slug → opaque service-id via caller config or TopoloAuth.
|
|
@@ -2372,6 +2450,8 @@ var TopoloClient = class _TopoloClient {
|
|
|
2372
2450
|
name: typeof s.name === "string" ? s.name : null,
|
|
2373
2451
|
description: typeof s.description === "string" ? s.description : null,
|
|
2374
2452
|
baseUrl: typeof s.base_url === "string" ? s.base_url : null,
|
|
2453
|
+
apiBaseUrl: typeof s.api_base_url === "string" ? s.api_base_url : typeof s.apiBaseUrl === "string" ? s.apiBaseUrl : null,
|
|
2454
|
+
launchUrl: typeof s.launch_url === "string" ? s.launch_url : typeof s.launchUrl === "string" ? s.launchUrl : null,
|
|
2375
2455
|
status: typeof s.status === "string" ? s.status : null,
|
|
2376
2456
|
surfaceKind: typeof s.surface_kind === "string" ? s.surface_kind : null,
|
|
2377
2457
|
launchable: s.launchable === true,
|
|
@@ -2438,6 +2518,10 @@ var TopoloClient = class _TopoloClient {
|
|
|
2438
2518
|
path: mapped.path,
|
|
2439
2519
|
...mapped.query ? { query: mapped.query } : {},
|
|
2440
2520
|
...mapped.body !== void 0 ? { body: mapped.body } : {},
|
|
2521
|
+
headers: {
|
|
2522
|
+
[TOPOLO_ACTION_ID_HEADER]: entry.actionId,
|
|
2523
|
+
[TOPOLO_ACTION_NAME_HEADER]: entry.name
|
|
2524
|
+
},
|
|
2441
2525
|
confirm: options.confirm === true
|
|
2442
2526
|
});
|
|
2443
2527
|
}
|
|
@@ -2453,6 +2537,8 @@ var TopoloClient = class _TopoloClient {
|
|
|
2453
2537
|
name: app.name ?? app.id,
|
|
2454
2538
|
description: null,
|
|
2455
2539
|
baseUrl: app.productionUrl ?? null,
|
|
2540
|
+
apiBaseUrl: null,
|
|
2541
|
+
launchUrl: app.productionUrl ?? null,
|
|
2456
2542
|
status: "bundled",
|
|
2457
2543
|
surfaceKind: "application",
|
|
2458
2544
|
launchable: Boolean(app.productionUrl),
|
|
@@ -2496,7 +2582,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
2496
2582
|
}
|
|
2497
2583
|
};
|
|
2498
2584
|
function resolveConfiguredServiceId(slug, serviceIds) {
|
|
2499
|
-
for (const serviceKey of
|
|
2585
|
+
for (const serviceKey of serviceKeyCandidates2(slug)) {
|
|
2500
2586
|
const configured = serviceIds?.[serviceKey];
|
|
2501
2587
|
if (typeof configured === "string" && configured.trim()) return configured.trim();
|
|
2502
2588
|
const envKey = `TOPOLO_SERVICE_ID_${serviceKey.replace(/\./g, "_").toUpperCase()}`;
|
|
@@ -2506,7 +2592,7 @@ function resolveConfiguredServiceId(slug, serviceIds) {
|
|
|
2506
2592
|
return null;
|
|
2507
2593
|
}
|
|
2508
2594
|
function resolveConfiguredServiceUrl(slug, serviceUrls) {
|
|
2509
|
-
for (const serviceKey of
|
|
2595
|
+
for (const serviceKey of serviceKeyCandidates2(slug)) {
|
|
2510
2596
|
const configured = serviceUrls?.[serviceKey];
|
|
2511
2597
|
if (typeof configured === "string" && configured.trim()) return configured.trim();
|
|
2512
2598
|
const envKey = `TOPOLO_SERVICE_URL_${serviceKey.replace(/\./g, "_").toUpperCase()}`;
|
|
@@ -2515,7 +2601,7 @@ function resolveConfiguredServiceUrl(slug, serviceUrls) {
|
|
|
2515
2601
|
}
|
|
2516
2602
|
return null;
|
|
2517
2603
|
}
|
|
2518
|
-
function
|
|
2604
|
+
function serviceKeyCandidates2(slug) {
|
|
2519
2605
|
const normalized = normalizeLookupKey(slug);
|
|
2520
2606
|
const withoutTopolo = normalized.startsWith("topolo-") ? normalized.slice("topolo-".length) : normalized;
|
|
2521
2607
|
return [.../* @__PURE__ */ new Set([
|
|
@@ -2537,7 +2623,7 @@ function catalogEntryLookupKeys(entry) {
|
|
|
2537
2623
|
for (const value of values) {
|
|
2538
2624
|
const normalized = normalizeLookupKey(value);
|
|
2539
2625
|
if (!normalized) continue;
|
|
2540
|
-
for (const candidate of
|
|
2626
|
+
for (const candidate of serviceKeyCandidates2(normalized)) {
|
|
2541
2627
|
keys.add(candidate);
|
|
2542
2628
|
}
|
|
2543
2629
|
}
|
|
@@ -2547,7 +2633,7 @@ function isCallableCatalogEntry(entry) {
|
|
|
2547
2633
|
const kind = normalizeLookupKey(entry.surfaceKind || "");
|
|
2548
2634
|
if (kind === "api" || kind === "technical_service" || kind === "service") return true;
|
|
2549
2635
|
return Boolean(
|
|
2550
|
-
entry.baseUrl && catalogEntryLookupKeys(entry).some((key) => Object.prototype.hasOwnProperty.call(DEFAULT_SERVICE_URLS, key))
|
|
2636
|
+
(entry.baseUrl || entry.apiBaseUrl) && catalogEntryLookupKeys(entry).some((key) => Object.prototype.hasOwnProperty.call(DEFAULT_SERVICE_URLS, key))
|
|
2551
2637
|
);
|
|
2552
2638
|
}
|
|
2553
2639
|
function normalizeActionCatalogEntry(item) {
|
|
@@ -3228,6 +3314,7 @@ function createTopolo(options) {
|
|
|
3228
3314
|
defineTopoloRecentActivityWidget,
|
|
3229
3315
|
defineTopoloStatsWidget,
|
|
3230
3316
|
requirementsForApplication,
|
|
3317
|
+
resolveCatalogServiceUrl,
|
|
3231
3318
|
resolveServiceUrl,
|
|
3232
3319
|
validateTopoloWidgetResponse
|
|
3233
3320
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -105,6 +105,8 @@ interface TopoloServiceCatalogEntry {
|
|
|
105
105
|
name: string | null;
|
|
106
106
|
description: string | null;
|
|
107
107
|
baseUrl: string | null;
|
|
108
|
+
apiBaseUrl?: string | null;
|
|
109
|
+
launchUrl?: string | null;
|
|
108
110
|
status: string | null;
|
|
109
111
|
surfaceKind: string | null;
|
|
110
112
|
launchable: boolean;
|
|
@@ -1579,6 +1581,20 @@ declare function auditAllApplicationRequirements(applicationIds?: readonly Appli
|
|
|
1579
1581
|
*/
|
|
1580
1582
|
|
|
1581
1583
|
declare const DEFAULT_SERVICE_URLS: Record<ServiceId, string>;
|
|
1584
|
+
interface ServiceCatalogUrlEntry {
|
|
1585
|
+
serviceId?: string | null;
|
|
1586
|
+
slug?: string | null;
|
|
1587
|
+
name?: string | null;
|
|
1588
|
+
baseUrl?: string | null;
|
|
1589
|
+
apiBaseUrl?: string | null;
|
|
1590
|
+
launchUrl?: string | null;
|
|
1591
|
+
api_base_url?: string | null;
|
|
1592
|
+
launch_url?: string | null;
|
|
1593
|
+
}
|
|
1594
|
+
declare function resolveCatalogServiceUrl(entry: ServiceCatalogUrlEntry, options?: {
|
|
1595
|
+
serviceKey?: string;
|
|
1596
|
+
overrides?: Record<string, string> | Partial<Record<ServiceId, string>>;
|
|
1597
|
+
}): string | null;
|
|
1582
1598
|
declare function resolveServiceUrl(service: ServiceId, overrides?: Partial<Record<ServiceId, string>>): string;
|
|
1583
1599
|
|
|
1584
1600
|
declare class TopoloSdkError extends Error {
|
|
@@ -2085,4 +2101,4 @@ declare function createTopolo(options: TopoloClientOptions): {
|
|
|
2085
2101
|
};
|
|
2086
2102
|
type Topolo = ReturnType<typeof createTopolo>;
|
|
2087
2103
|
|
|
2088
|
-
export { APPLICATIONS, APPLICATION_REQUIREMENTS, APPLICATION_REQUIREMENTS_VERSION, type AgentIdentity, type ApplicationCatalogEntry, type ApplicationDeployTarget, type ApplicationId, type ApplicationRequirement, type ApplicationRequirementAudit, type ApplicationRequirementFinding, type ApplicationRequirementMigrationItem, type ApplicationRequirementScope, type ApplicationRequirementScore, type ApplicationRequirementStatus, type ApplicationRequirementsAuditReport, type CredentialIntrospection, type CrmContactSummary, CrmModule, DEFAULT_SERVICE_URLS, type DeviceAuthorizationResponse, IdentityModule, type ListContactsOptions, type ListContactsResult, type OAuthHelperOptions, type P2PActionDetail, type P2PActionRequestInput, type P2PActionRequestResult, type P2PActionRow, type P2PActionRun, type P2PCapability, type P2PConnection, type P2PHold, type P2PLedgerEntry, P2PModule, type P2PPolicy, type P2PPricingModel, type P2PSettlementBatch, type P2PSettlementGroup, type PublishCapabilityInput, type RequestOptions, type ServiceId, TOPOLO_WIDGET_ENDPOINT, TOPOLO_WIDGET_REFRESH_INTERVAL_MS, type TokenResponse, type Topolo, type TopoloActionCatalogEntry, TopoloAuthError, type TopoloChartWidget, TopoloClient, type TopoloClientOptions, type TopoloCredential, type TopoloDebugEvent, TopoloHttpError, type TopoloNotificationsWidget, TopoloOAuth, TopoloPermissionError, type TopoloQuickActionsWidget, type TopoloRecentActivityWidget, TopoloSdkError, type TopoloServiceCatalogEntry, type TopoloStatsWidget, type TopoloWidget, type TopoloWidgetActionItem, type TopoloWidgetActivityItem, type TopoloWidgetApiResponse, type TopoloWidgetBase, type TopoloWidgetConfig, type TopoloWidgetDraft, type TopoloWidgetMetric, type TopoloWidgetNotificationItem, type TopoloWidgetResponseOptions, type TopoloWidgetSnapshotMetadata, type TopoloWidgetSnapshotMetric, type TopoloWidgetType, type TopoloWidgetValidationResult, applicationRequirementScopes, auditAllApplicationRequirements, auditApplicationRequirements, createTopolo, createTopoloWidgetError, createTopoloWidgetResponse, defineTopoloChartWidget, defineTopoloNotificationsWidget, defineTopoloQuickActionsWidget, defineTopoloRecentActivityWidget, defineTopoloStatsWidget, requirementsForApplication, resolveServiceUrl, validateTopoloWidgetResponse };
|
|
2104
|
+
export { APPLICATIONS, APPLICATION_REQUIREMENTS, APPLICATION_REQUIREMENTS_VERSION, type AgentIdentity, type ApplicationCatalogEntry, type ApplicationDeployTarget, type ApplicationId, type ApplicationRequirement, type ApplicationRequirementAudit, type ApplicationRequirementFinding, type ApplicationRequirementMigrationItem, type ApplicationRequirementScope, type ApplicationRequirementScore, type ApplicationRequirementStatus, type ApplicationRequirementsAuditReport, type CredentialIntrospection, type CrmContactSummary, CrmModule, DEFAULT_SERVICE_URLS, type DeviceAuthorizationResponse, IdentityModule, type ListContactsOptions, type ListContactsResult, type OAuthHelperOptions, type P2PActionDetail, type P2PActionRequestInput, type P2PActionRequestResult, type P2PActionRow, type P2PActionRun, type P2PCapability, type P2PConnection, type P2PHold, type P2PLedgerEntry, P2PModule, type P2PPolicy, type P2PPricingModel, type P2PSettlementBatch, type P2PSettlementGroup, type PublishCapabilityInput, type RequestOptions, type ServiceCatalogUrlEntry, type ServiceId, TOPOLO_WIDGET_ENDPOINT, TOPOLO_WIDGET_REFRESH_INTERVAL_MS, type TokenResponse, type Topolo, type TopoloActionCatalogEntry, TopoloAuthError, type TopoloChartWidget, TopoloClient, type TopoloClientOptions, type TopoloCredential, type TopoloDebugEvent, TopoloHttpError, type TopoloNotificationsWidget, TopoloOAuth, TopoloPermissionError, type TopoloQuickActionsWidget, type TopoloRecentActivityWidget, TopoloSdkError, type TopoloServiceCatalogEntry, type TopoloStatsWidget, type TopoloWidget, type TopoloWidgetActionItem, type TopoloWidgetActivityItem, type TopoloWidgetApiResponse, type TopoloWidgetBase, type TopoloWidgetConfig, type TopoloWidgetDraft, type TopoloWidgetMetric, type TopoloWidgetNotificationItem, type TopoloWidgetResponseOptions, type TopoloWidgetSnapshotMetadata, type TopoloWidgetSnapshotMetric, type TopoloWidgetType, type TopoloWidgetValidationResult, applicationRequirementScopes, auditAllApplicationRequirements, auditApplicationRequirements, createTopolo, createTopoloWidgetError, createTopoloWidgetResponse, defineTopoloChartWidget, defineTopoloNotificationsWidget, defineTopoloQuickActionsWidget, defineTopoloRecentActivityWidget, defineTopoloStatsWidget, requirementsForApplication, resolveCatalogServiceUrl, resolveServiceUrl, validateTopoloWidgetResponse };
|
package/dist/index.d.ts
CHANGED
|
@@ -105,6 +105,8 @@ interface TopoloServiceCatalogEntry {
|
|
|
105
105
|
name: string | null;
|
|
106
106
|
description: string | null;
|
|
107
107
|
baseUrl: string | null;
|
|
108
|
+
apiBaseUrl?: string | null;
|
|
109
|
+
launchUrl?: string | null;
|
|
108
110
|
status: string | null;
|
|
109
111
|
surfaceKind: string | null;
|
|
110
112
|
launchable: boolean;
|
|
@@ -1579,6 +1581,20 @@ declare function auditAllApplicationRequirements(applicationIds?: readonly Appli
|
|
|
1579
1581
|
*/
|
|
1580
1582
|
|
|
1581
1583
|
declare const DEFAULT_SERVICE_URLS: Record<ServiceId, string>;
|
|
1584
|
+
interface ServiceCatalogUrlEntry {
|
|
1585
|
+
serviceId?: string | null;
|
|
1586
|
+
slug?: string | null;
|
|
1587
|
+
name?: string | null;
|
|
1588
|
+
baseUrl?: string | null;
|
|
1589
|
+
apiBaseUrl?: string | null;
|
|
1590
|
+
launchUrl?: string | null;
|
|
1591
|
+
api_base_url?: string | null;
|
|
1592
|
+
launch_url?: string | null;
|
|
1593
|
+
}
|
|
1594
|
+
declare function resolveCatalogServiceUrl(entry: ServiceCatalogUrlEntry, options?: {
|
|
1595
|
+
serviceKey?: string;
|
|
1596
|
+
overrides?: Record<string, string> | Partial<Record<ServiceId, string>>;
|
|
1597
|
+
}): string | null;
|
|
1582
1598
|
declare function resolveServiceUrl(service: ServiceId, overrides?: Partial<Record<ServiceId, string>>): string;
|
|
1583
1599
|
|
|
1584
1600
|
declare class TopoloSdkError extends Error {
|
|
@@ -2085,4 +2101,4 @@ declare function createTopolo(options: TopoloClientOptions): {
|
|
|
2085
2101
|
};
|
|
2086
2102
|
type Topolo = ReturnType<typeof createTopolo>;
|
|
2087
2103
|
|
|
2088
|
-
export { APPLICATIONS, APPLICATION_REQUIREMENTS, APPLICATION_REQUIREMENTS_VERSION, type AgentIdentity, type ApplicationCatalogEntry, type ApplicationDeployTarget, type ApplicationId, type ApplicationRequirement, type ApplicationRequirementAudit, type ApplicationRequirementFinding, type ApplicationRequirementMigrationItem, type ApplicationRequirementScope, type ApplicationRequirementScore, type ApplicationRequirementStatus, type ApplicationRequirementsAuditReport, type CredentialIntrospection, type CrmContactSummary, CrmModule, DEFAULT_SERVICE_URLS, type DeviceAuthorizationResponse, IdentityModule, type ListContactsOptions, type ListContactsResult, type OAuthHelperOptions, type P2PActionDetail, type P2PActionRequestInput, type P2PActionRequestResult, type P2PActionRow, type P2PActionRun, type P2PCapability, type P2PConnection, type P2PHold, type P2PLedgerEntry, P2PModule, type P2PPolicy, type P2PPricingModel, type P2PSettlementBatch, type P2PSettlementGroup, type PublishCapabilityInput, type RequestOptions, type ServiceId, TOPOLO_WIDGET_ENDPOINT, TOPOLO_WIDGET_REFRESH_INTERVAL_MS, type TokenResponse, type Topolo, type TopoloActionCatalogEntry, TopoloAuthError, type TopoloChartWidget, TopoloClient, type TopoloClientOptions, type TopoloCredential, type TopoloDebugEvent, TopoloHttpError, type TopoloNotificationsWidget, TopoloOAuth, TopoloPermissionError, type TopoloQuickActionsWidget, type TopoloRecentActivityWidget, TopoloSdkError, type TopoloServiceCatalogEntry, type TopoloStatsWidget, type TopoloWidget, type TopoloWidgetActionItem, type TopoloWidgetActivityItem, type TopoloWidgetApiResponse, type TopoloWidgetBase, type TopoloWidgetConfig, type TopoloWidgetDraft, type TopoloWidgetMetric, type TopoloWidgetNotificationItem, type TopoloWidgetResponseOptions, type TopoloWidgetSnapshotMetadata, type TopoloWidgetSnapshotMetric, type TopoloWidgetType, type TopoloWidgetValidationResult, applicationRequirementScopes, auditAllApplicationRequirements, auditApplicationRequirements, createTopolo, createTopoloWidgetError, createTopoloWidgetResponse, defineTopoloChartWidget, defineTopoloNotificationsWidget, defineTopoloQuickActionsWidget, defineTopoloRecentActivityWidget, defineTopoloStatsWidget, requirementsForApplication, resolveServiceUrl, validateTopoloWidgetResponse };
|
|
2104
|
+
export { APPLICATIONS, APPLICATION_REQUIREMENTS, APPLICATION_REQUIREMENTS_VERSION, type AgentIdentity, type ApplicationCatalogEntry, type ApplicationDeployTarget, type ApplicationId, type ApplicationRequirement, type ApplicationRequirementAudit, type ApplicationRequirementFinding, type ApplicationRequirementMigrationItem, type ApplicationRequirementScope, type ApplicationRequirementScore, type ApplicationRequirementStatus, type ApplicationRequirementsAuditReport, type CredentialIntrospection, type CrmContactSummary, CrmModule, DEFAULT_SERVICE_URLS, type DeviceAuthorizationResponse, IdentityModule, type ListContactsOptions, type ListContactsResult, type OAuthHelperOptions, type P2PActionDetail, type P2PActionRequestInput, type P2PActionRequestResult, type P2PActionRow, type P2PActionRun, type P2PCapability, type P2PConnection, type P2PHold, type P2PLedgerEntry, P2PModule, type P2PPolicy, type P2PPricingModel, type P2PSettlementBatch, type P2PSettlementGroup, type PublishCapabilityInput, type RequestOptions, type ServiceCatalogUrlEntry, type ServiceId, TOPOLO_WIDGET_ENDPOINT, TOPOLO_WIDGET_REFRESH_INTERVAL_MS, type TokenResponse, type Topolo, type TopoloActionCatalogEntry, TopoloAuthError, type TopoloChartWidget, TopoloClient, type TopoloClientOptions, type TopoloCredential, type TopoloDebugEvent, TopoloHttpError, type TopoloNotificationsWidget, TopoloOAuth, TopoloPermissionError, type TopoloQuickActionsWidget, type TopoloRecentActivityWidget, TopoloSdkError, type TopoloServiceCatalogEntry, type TopoloStatsWidget, type TopoloWidget, type TopoloWidgetActionItem, type TopoloWidgetActivityItem, type TopoloWidgetApiResponse, type TopoloWidgetBase, type TopoloWidgetConfig, type TopoloWidgetDraft, type TopoloWidgetMetric, type TopoloWidgetNotificationItem, type TopoloWidgetResponseOptions, type TopoloWidgetSnapshotMetadata, type TopoloWidgetSnapshotMetric, type TopoloWidgetType, type TopoloWidgetValidationResult, applicationRequirementScopes, auditAllApplicationRequirements, auditApplicationRequirements, createTopolo, createTopoloWidgetError, createTopoloWidgetResponse, defineTopoloChartWidget, defineTopoloNotificationsWidget, defineTopoloQuickActionsWidget, defineTopoloRecentActivityWidget, defineTopoloStatsWidget, requirementsForApplication, resolveCatalogServiceUrl, resolveServiceUrl, validateTopoloWidgetResponse };
|
package/dist/index.js
CHANGED
|
@@ -2045,17 +2045,90 @@ function statusRank(status) {
|
|
|
2045
2045
|
|
|
2046
2046
|
// src/services.ts
|
|
2047
2047
|
var DEFAULT_SERVICE_URLS = SERVICES;
|
|
2048
|
+
function resolveCatalogServiceUrl(entry, options = {}) {
|
|
2049
|
+
const keys = catalogServiceKeyCandidates(entry, options.serviceKey);
|
|
2050
|
+
const override = resolveServiceUrlOverride(keys, options.overrides);
|
|
2051
|
+
if (override) return override;
|
|
2052
|
+
const apiBaseUrl = firstString(entry.apiBaseUrl, entry.api_base_url);
|
|
2053
|
+
if (apiBaseUrl) return apiBaseUrl;
|
|
2054
|
+
const bundledUrl = firstBundledServiceUrl(keys);
|
|
2055
|
+
const liveUrl = firstString(entry.baseUrl);
|
|
2056
|
+
if (bundledUrl && (!liveUrl || sameUrlEnvironment(bundledUrl, liveUrl))) return bundledUrl;
|
|
2057
|
+
return liveUrl ?? bundledUrl ?? null;
|
|
2058
|
+
}
|
|
2048
2059
|
function resolveServiceUrl(service, overrides) {
|
|
2049
|
-
const override =
|
|
2060
|
+
const override = resolveServiceUrlOverride([service], overrides);
|
|
2050
2061
|
if (override) return override;
|
|
2051
|
-
const envKey = `TOPOLO_SERVICE_URL_${service.replace(/\./g, "_").toUpperCase()}`;
|
|
2052
|
-
const envValue = typeof process !== "undefined" ? process.env?.[envKey] : void 0;
|
|
2053
|
-
if (envValue) return envValue;
|
|
2054
2062
|
return DEFAULT_SERVICE_URLS[service];
|
|
2055
2063
|
}
|
|
2064
|
+
function catalogServiceKeyCandidates(entry, preferred) {
|
|
2065
|
+
const values = [preferred, entry.slug, entry.name, entry.serviceId].filter(
|
|
2066
|
+
(value) => typeof value === "string" && value.trim().length > 0
|
|
2067
|
+
);
|
|
2068
|
+
const keys = /* @__PURE__ */ new Set();
|
|
2069
|
+
for (const value of values) {
|
|
2070
|
+
for (const candidate of serviceKeyCandidates(value)) keys.add(candidate);
|
|
2071
|
+
}
|
|
2072
|
+
return [...keys];
|
|
2073
|
+
}
|
|
2074
|
+
function serviceKeyCandidates(value) {
|
|
2075
|
+
const normalized = value.trim().toLowerCase().replace(/\s+/g, "-");
|
|
2076
|
+
const withoutTopolo = normalized.startsWith("topolo-") ? normalized.slice("topolo-".length) : normalized;
|
|
2077
|
+
return [.../* @__PURE__ */ new Set([
|
|
2078
|
+
normalized,
|
|
2079
|
+
normalized.replace(/-/g, "_"),
|
|
2080
|
+
withoutTopolo,
|
|
2081
|
+
withoutTopolo.replace(/-/g, "_")
|
|
2082
|
+
])].filter(Boolean);
|
|
2083
|
+
}
|
|
2084
|
+
function resolveServiceUrlOverride(keys, overrides) {
|
|
2085
|
+
for (const key of keys) {
|
|
2086
|
+
const direct = overrides?.[key];
|
|
2087
|
+
if (direct) return direct;
|
|
2088
|
+
const envStyleKey = key.replace(/\./g, "_");
|
|
2089
|
+
const envStyle = overrides?.[envStyleKey];
|
|
2090
|
+
if (envStyle) return envStyle;
|
|
2091
|
+
}
|
|
2092
|
+
for (const key of keys) {
|
|
2093
|
+
const envKey = `TOPOLO_SERVICE_URL_${key.replace(/\./g, "_").toUpperCase()}`;
|
|
2094
|
+
const envValue = typeof process !== "undefined" ? process.env?.[envKey] : void 0;
|
|
2095
|
+
if (envValue) return envValue;
|
|
2096
|
+
}
|
|
2097
|
+
return null;
|
|
2098
|
+
}
|
|
2099
|
+
function firstBundledServiceUrl(keys) {
|
|
2100
|
+
for (const key of keys) {
|
|
2101
|
+
const url = DEFAULT_SERVICE_URLS[key];
|
|
2102
|
+
if (url) return url;
|
|
2103
|
+
}
|
|
2104
|
+
return null;
|
|
2105
|
+
}
|
|
2106
|
+
function firstString(...values) {
|
|
2107
|
+
for (const value of values) {
|
|
2108
|
+
if (typeof value === "string" && value.trim()) return value.trim();
|
|
2109
|
+
}
|
|
2110
|
+
return null;
|
|
2111
|
+
}
|
|
2112
|
+
function sameUrlEnvironment(left, right) {
|
|
2113
|
+
return urlEnvironment(left) === urlEnvironment(right);
|
|
2114
|
+
}
|
|
2115
|
+
function urlEnvironment(value) {
|
|
2116
|
+
try {
|
|
2117
|
+
const url = new URL(value);
|
|
2118
|
+
const host = url.hostname.toLowerCase();
|
|
2119
|
+
if (host === "localhost" || host === "127.0.0.1") return "local";
|
|
2120
|
+
if (host.endsWith(".stg.topolo.us") || host.includes("topolo-staging")) return "staging";
|
|
2121
|
+
if (host.endsWith(".topolo.app") || host.endsWith(".topolo.us")) return "production";
|
|
2122
|
+
return "external";
|
|
2123
|
+
} catch {
|
|
2124
|
+
return "external";
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2056
2127
|
|
|
2057
2128
|
// src/client.ts
|
|
2058
2129
|
var WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
2130
|
+
var TOPOLO_ACTION_ID_HEADER = "X-Topolo-Action-Id";
|
|
2131
|
+
var TOPOLO_ACTION_NAME_HEADER = "X-Topolo-Action-Name";
|
|
2059
2132
|
var POSITIVE_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
2060
2133
|
var NEGATIVE_CACHE_TTL_MS = 60 * 1e3;
|
|
2061
2134
|
var CATALOG_CACHE_TTL_MS = 60 * 1e3;
|
|
@@ -2223,8 +2296,12 @@ var TopoloClient = class _TopoloClient {
|
|
|
2223
2296
|
async resolveLiveCallableService(serviceKey) {
|
|
2224
2297
|
const catalog = await this.listServices();
|
|
2225
2298
|
const match = catalog.find((entry) => catalogEntryMatches(entry, serviceKey));
|
|
2226
|
-
if (!match
|
|
2227
|
-
|
|
2299
|
+
if (!match || !isCallableCatalogEntry(match)) return null;
|
|
2300
|
+
const baseUrl = resolveCatalogServiceUrl(match, {
|
|
2301
|
+
serviceKey,
|
|
2302
|
+
overrides: this.serviceUrls
|
|
2303
|
+
});
|
|
2304
|
+
return baseUrl ? { serviceKey, baseUrl } : null;
|
|
2228
2305
|
}
|
|
2229
2306
|
/**
|
|
2230
2307
|
* Resolve a service-slug → opaque service-id via caller config or TopoloAuth.
|
|
@@ -2318,6 +2395,8 @@ var TopoloClient = class _TopoloClient {
|
|
|
2318
2395
|
name: typeof s.name === "string" ? s.name : null,
|
|
2319
2396
|
description: typeof s.description === "string" ? s.description : null,
|
|
2320
2397
|
baseUrl: typeof s.base_url === "string" ? s.base_url : null,
|
|
2398
|
+
apiBaseUrl: typeof s.api_base_url === "string" ? s.api_base_url : typeof s.apiBaseUrl === "string" ? s.apiBaseUrl : null,
|
|
2399
|
+
launchUrl: typeof s.launch_url === "string" ? s.launch_url : typeof s.launchUrl === "string" ? s.launchUrl : null,
|
|
2321
2400
|
status: typeof s.status === "string" ? s.status : null,
|
|
2322
2401
|
surfaceKind: typeof s.surface_kind === "string" ? s.surface_kind : null,
|
|
2323
2402
|
launchable: s.launchable === true,
|
|
@@ -2384,6 +2463,10 @@ var TopoloClient = class _TopoloClient {
|
|
|
2384
2463
|
path: mapped.path,
|
|
2385
2464
|
...mapped.query ? { query: mapped.query } : {},
|
|
2386
2465
|
...mapped.body !== void 0 ? { body: mapped.body } : {},
|
|
2466
|
+
headers: {
|
|
2467
|
+
[TOPOLO_ACTION_ID_HEADER]: entry.actionId,
|
|
2468
|
+
[TOPOLO_ACTION_NAME_HEADER]: entry.name
|
|
2469
|
+
},
|
|
2387
2470
|
confirm: options.confirm === true
|
|
2388
2471
|
});
|
|
2389
2472
|
}
|
|
@@ -2399,6 +2482,8 @@ var TopoloClient = class _TopoloClient {
|
|
|
2399
2482
|
name: app.name ?? app.id,
|
|
2400
2483
|
description: null,
|
|
2401
2484
|
baseUrl: app.productionUrl ?? null,
|
|
2485
|
+
apiBaseUrl: null,
|
|
2486
|
+
launchUrl: app.productionUrl ?? null,
|
|
2402
2487
|
status: "bundled",
|
|
2403
2488
|
surfaceKind: "application",
|
|
2404
2489
|
launchable: Boolean(app.productionUrl),
|
|
@@ -2442,7 +2527,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
2442
2527
|
}
|
|
2443
2528
|
};
|
|
2444
2529
|
function resolveConfiguredServiceId(slug, serviceIds) {
|
|
2445
|
-
for (const serviceKey of
|
|
2530
|
+
for (const serviceKey of serviceKeyCandidates2(slug)) {
|
|
2446
2531
|
const configured = serviceIds?.[serviceKey];
|
|
2447
2532
|
if (typeof configured === "string" && configured.trim()) return configured.trim();
|
|
2448
2533
|
const envKey = `TOPOLO_SERVICE_ID_${serviceKey.replace(/\./g, "_").toUpperCase()}`;
|
|
@@ -2452,7 +2537,7 @@ function resolveConfiguredServiceId(slug, serviceIds) {
|
|
|
2452
2537
|
return null;
|
|
2453
2538
|
}
|
|
2454
2539
|
function resolveConfiguredServiceUrl(slug, serviceUrls) {
|
|
2455
|
-
for (const serviceKey of
|
|
2540
|
+
for (const serviceKey of serviceKeyCandidates2(slug)) {
|
|
2456
2541
|
const configured = serviceUrls?.[serviceKey];
|
|
2457
2542
|
if (typeof configured === "string" && configured.trim()) return configured.trim();
|
|
2458
2543
|
const envKey = `TOPOLO_SERVICE_URL_${serviceKey.replace(/\./g, "_").toUpperCase()}`;
|
|
@@ -2461,7 +2546,7 @@ function resolveConfiguredServiceUrl(slug, serviceUrls) {
|
|
|
2461
2546
|
}
|
|
2462
2547
|
return null;
|
|
2463
2548
|
}
|
|
2464
|
-
function
|
|
2549
|
+
function serviceKeyCandidates2(slug) {
|
|
2465
2550
|
const normalized = normalizeLookupKey(slug);
|
|
2466
2551
|
const withoutTopolo = normalized.startsWith("topolo-") ? normalized.slice("topolo-".length) : normalized;
|
|
2467
2552
|
return [.../* @__PURE__ */ new Set([
|
|
@@ -2483,7 +2568,7 @@ function catalogEntryLookupKeys(entry) {
|
|
|
2483
2568
|
for (const value of values) {
|
|
2484
2569
|
const normalized = normalizeLookupKey(value);
|
|
2485
2570
|
if (!normalized) continue;
|
|
2486
|
-
for (const candidate of
|
|
2571
|
+
for (const candidate of serviceKeyCandidates2(normalized)) {
|
|
2487
2572
|
keys.add(candidate);
|
|
2488
2573
|
}
|
|
2489
2574
|
}
|
|
@@ -2493,7 +2578,7 @@ function isCallableCatalogEntry(entry) {
|
|
|
2493
2578
|
const kind = normalizeLookupKey(entry.surfaceKind || "");
|
|
2494
2579
|
if (kind === "api" || kind === "technical_service" || kind === "service") return true;
|
|
2495
2580
|
return Boolean(
|
|
2496
|
-
entry.baseUrl && catalogEntryLookupKeys(entry).some((key) => Object.prototype.hasOwnProperty.call(DEFAULT_SERVICE_URLS, key))
|
|
2581
|
+
(entry.baseUrl || entry.apiBaseUrl) && catalogEntryLookupKeys(entry).some((key) => Object.prototype.hasOwnProperty.call(DEFAULT_SERVICE_URLS, key))
|
|
2497
2582
|
);
|
|
2498
2583
|
}
|
|
2499
2584
|
function normalizeActionCatalogEntry(item) {
|
|
@@ -3173,6 +3258,7 @@ export {
|
|
|
3173
3258
|
defineTopoloRecentActivityWidget,
|
|
3174
3259
|
defineTopoloStatsWidget,
|
|
3175
3260
|
requirementsForApplication,
|
|
3261
|
+
resolveCatalogServiceUrl,
|
|
3176
3262
|
resolveServiceUrl,
|
|
3177
3263
|
validateTopoloWidgetResponse
|
|
3178
3264
|
};
|
package/package.json
CHANGED
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,8 +741,11 @@ describe('TopoloClient action catalog', () => {
|
|
|
671
741
|
await client.callAction('messages.list', { mailboxId: 'primary', q: 'Ada', page: 2 });
|
|
672
742
|
const call = requestCalls(calls)[0]!;
|
|
673
743
|
const url = new URL(call.url);
|
|
674
|
-
expect(url.toString()).toBe('https://mail
|
|
744
|
+
expect(url.toString()).toBe('https://mail.topolo.app/api/mail/mailboxes/primary/messages?q=Ada&page=2');
|
|
675
745
|
expect(call.init.method).toBe('GET');
|
|
746
|
+
const headers = new Headers(call.init.headers);
|
|
747
|
+
expect(headers.get('X-Topolo-Action-Id')).toBe('srv_mail.messages.list');
|
|
748
|
+
expect(headers.get('X-Topolo-Action-Name')).toBe('messages.list');
|
|
676
749
|
});
|
|
677
750
|
|
|
678
751
|
it('substitutes action path params and removes them from the query', async () => {
|
package/src/client.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { TopoloAuthError, TopoloHttpError, TopoloPermissionError, TopoloSdkError
|
|
|
9
9
|
import {
|
|
10
10
|
DEFAULT_SERVICE_URLS,
|
|
11
11
|
APPLICATIONS,
|
|
12
|
+
resolveCatalogServiceUrl,
|
|
12
13
|
resolveServiceUrl,
|
|
13
14
|
type ServiceId,
|
|
14
15
|
} from './services.js';
|
|
@@ -83,6 +84,8 @@ export interface RequestOptions {
|
|
|
83
84
|
}
|
|
84
85
|
|
|
85
86
|
const WRITE_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
|
|
87
|
+
const TOPOLO_ACTION_ID_HEADER = 'X-Topolo-Action-Id';
|
|
88
|
+
const TOPOLO_ACTION_NAME_HEADER = 'X-Topolo-Action-Name';
|
|
86
89
|
|
|
87
90
|
const POSITIVE_CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
88
91
|
const NEGATIVE_CACHE_TTL_MS = 60 * 1000; // 1 minute
|
|
@@ -104,6 +107,8 @@ export interface TopoloServiceCatalogEntry {
|
|
|
104
107
|
name: string | null;
|
|
105
108
|
description: string | null;
|
|
106
109
|
baseUrl: string | null;
|
|
110
|
+
apiBaseUrl?: string | null;
|
|
111
|
+
launchUrl?: string | null;
|
|
107
112
|
status: string | null;
|
|
108
113
|
surfaceKind: string | null;
|
|
109
114
|
launchable: boolean;
|
|
@@ -323,8 +328,12 @@ export class TopoloClient {
|
|
|
323
328
|
private async resolveLiveCallableService(serviceKey: string): Promise<{ serviceKey: string; baseUrl: string } | null> {
|
|
324
329
|
const catalog = await this.listServices();
|
|
325
330
|
const match = catalog.find((entry) => catalogEntryMatches(entry, serviceKey));
|
|
326
|
-
if (!match
|
|
327
|
-
|
|
331
|
+
if (!match || !isCallableCatalogEntry(match)) return null;
|
|
332
|
+
const baseUrl = resolveCatalogServiceUrl(match, {
|
|
333
|
+
serviceKey,
|
|
334
|
+
overrides: this.serviceUrls,
|
|
335
|
+
});
|
|
336
|
+
return baseUrl ? { serviceKey, baseUrl } : null;
|
|
328
337
|
}
|
|
329
338
|
|
|
330
339
|
/**
|
|
@@ -446,6 +455,18 @@ export class TopoloClient {
|
|
|
446
455
|
description:
|
|
447
456
|
typeof s.description === 'string' ? s.description : null,
|
|
448
457
|
baseUrl: typeof s.base_url === 'string' ? s.base_url : null,
|
|
458
|
+
apiBaseUrl:
|
|
459
|
+
typeof s.api_base_url === 'string'
|
|
460
|
+
? s.api_base_url
|
|
461
|
+
: typeof s.apiBaseUrl === 'string'
|
|
462
|
+
? s.apiBaseUrl
|
|
463
|
+
: null,
|
|
464
|
+
launchUrl:
|
|
465
|
+
typeof s.launch_url === 'string'
|
|
466
|
+
? s.launch_url
|
|
467
|
+
: typeof s.launchUrl === 'string'
|
|
468
|
+
? s.launchUrl
|
|
469
|
+
: null,
|
|
449
470
|
status: typeof s.status === 'string' ? s.status : null,
|
|
450
471
|
surfaceKind:
|
|
451
472
|
typeof s.surface_kind === 'string' ? s.surface_kind : null,
|
|
@@ -529,6 +550,10 @@ export class TopoloClient {
|
|
|
529
550
|
path: mapped.path,
|
|
530
551
|
...(mapped.query ? { query: mapped.query } : {}),
|
|
531
552
|
...(mapped.body !== undefined ? { body: mapped.body } : {}),
|
|
553
|
+
headers: {
|
|
554
|
+
[TOPOLO_ACTION_ID_HEADER]: entry.actionId,
|
|
555
|
+
[TOPOLO_ACTION_NAME_HEADER]: entry.name,
|
|
556
|
+
},
|
|
532
557
|
confirm: options.confirm === true,
|
|
533
558
|
});
|
|
534
559
|
}
|
|
@@ -549,6 +574,8 @@ export class TopoloClient {
|
|
|
549
574
|
name: app.name ?? app.id,
|
|
550
575
|
description: null,
|
|
551
576
|
baseUrl: app.productionUrl ?? null,
|
|
577
|
+
apiBaseUrl: null,
|
|
578
|
+
launchUrl: app.productionUrl ?? null,
|
|
552
579
|
status: 'bundled',
|
|
553
580
|
surfaceKind: 'application',
|
|
554
581
|
launchable: Boolean(app.productionUrl),
|
|
@@ -672,7 +699,7 @@ function isCallableCatalogEntry(entry: TopoloServiceCatalogEntry): boolean {
|
|
|
672
699
|
const kind = normalizeLookupKey(entry.surfaceKind || '');
|
|
673
700
|
if (kind === 'api' || kind === 'technical_service' || kind === 'service') return true;
|
|
674
701
|
return Boolean(
|
|
675
|
-
entry.baseUrl &&
|
|
702
|
+
(entry.baseUrl || entry.apiBaseUrl) &&
|
|
676
703
|
catalogEntryLookupKeys(entry).some((key) => Object.prototype.hasOwnProperty.call(DEFAULT_SERVICE_URLS, key)),
|
|
677
704
|
);
|
|
678
705
|
}
|
package/src/index.ts
CHANGED
|
@@ -17,6 +17,7 @@ export {
|
|
|
17
17
|
auditApplicationRequirements,
|
|
18
18
|
applicationRequirementScopes,
|
|
19
19
|
requirementsForApplication,
|
|
20
|
+
resolveCatalogServiceUrl,
|
|
20
21
|
resolveServiceUrl,
|
|
21
22
|
type ApplicationCatalogEntry,
|
|
22
23
|
type ApplicationDeployTarget,
|
|
@@ -29,6 +30,7 @@ export {
|
|
|
29
30
|
type ApplicationRequirementScope,
|
|
30
31
|
type ApplicationRequirementStatus,
|
|
31
32
|
type ApplicationRequirementsAuditReport,
|
|
33
|
+
type ServiceCatalogUrlEntry,
|
|
32
34
|
type ServiceId,
|
|
33
35
|
} from "./services.js";
|
|
34
36
|
export {
|
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
|
+
}
|