@topolo/sdk 0.4.5 → 0.4.7
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 +25 -50
- package/dist/index.d.cts +17 -19
- package/dist/index.d.ts +17 -19
- package/dist/index.js +25 -50
- package/package.json +1 -1
- package/src/application-requirements.ts +3 -1
- package/src/applications.generated.ts +15 -10
- package/src/client.test.ts +19 -14
- package/src/client.ts +1 -13
- package/src/modules/p2p.test.ts +2 -1
- package/src/services.generated.ts +2 -2
- package/src/services.test.ts +10 -9
- package/src/services.ts +1 -31
- package/src/widgets.test.ts +39 -1
- package/src/widgets.ts +5 -3
package/dist/index.cjs
CHANGED
|
@@ -137,13 +137,13 @@ var SERVICES = {
|
|
|
137
137
|
inventory: "https://inventory.topolo.app",
|
|
138
138
|
learn: "https://learn-api.topolo.app",
|
|
139
139
|
localize: "https://localize.topolo.app",
|
|
140
|
-
mail: "https://mail.topolo.app",
|
|
140
|
+
mail: "https://mail-api.topolo.app",
|
|
141
141
|
mdm: "https://mdm-api.topolo.app",
|
|
142
142
|
messages: "https://messages.topolo.app",
|
|
143
143
|
nexus: "https://nexus-api.topolo.app",
|
|
144
144
|
notify: "https://notify.topolo.app",
|
|
145
145
|
observability_watch: "https://observability-watch.topolo.app",
|
|
146
|
-
one: "https://
|
|
146
|
+
one: "https://www.topolo.app",
|
|
147
147
|
p2p: "https://p2p.topolo.app",
|
|
148
148
|
pay: "https://pay.topolo.app",
|
|
149
149
|
people: "https://people.topolo.app",
|
|
@@ -975,15 +975,17 @@ var APPLICATIONS = {
|
|
|
975
975
|
"mail.topolo.app"
|
|
976
976
|
],
|
|
977
977
|
"routes": [],
|
|
978
|
-
"serviceId":
|
|
978
|
+
"serviceId": null
|
|
979
979
|
},
|
|
980
980
|
{
|
|
981
981
|
"name": "topolo-mail-api",
|
|
982
982
|
"kind": "worker",
|
|
983
|
-
"apiUrl":
|
|
984
|
-
"domains": [
|
|
983
|
+
"apiUrl": "https://mail-api.topolo.app",
|
|
984
|
+
"domains": [
|
|
985
|
+
"mail-api.topolo.app"
|
|
986
|
+
],
|
|
985
987
|
"routes": [],
|
|
986
|
-
"serviceId":
|
|
988
|
+
"serviceId": "mail"
|
|
987
989
|
}
|
|
988
990
|
]
|
|
989
991
|
},
|
|
@@ -1148,20 +1150,23 @@ var APPLICATIONS = {
|
|
|
1148
1150
|
{
|
|
1149
1151
|
"name": "topolo-one",
|
|
1150
1152
|
"kind": "worker",
|
|
1151
|
-
"apiUrl":
|
|
1152
|
-
"domains": [
|
|
1153
|
-
|
|
1153
|
+
"apiUrl": "https://one.topolo.app",
|
|
1154
|
+
"domains": [
|
|
1155
|
+
"one.topolo.app"
|
|
1156
|
+
],
|
|
1157
|
+
"routes": [
|
|
1158
|
+
"one.topolo.app/*"
|
|
1159
|
+
],
|
|
1154
1160
|
"serviceId": null
|
|
1155
1161
|
},
|
|
1156
1162
|
{
|
|
1157
1163
|
"name": "topolo-one-api",
|
|
1158
1164
|
"kind": "worker",
|
|
1159
|
-
"apiUrl": "https://
|
|
1165
|
+
"apiUrl": "https://www.topolo.app",
|
|
1160
1166
|
"domains": [
|
|
1161
|
-
"
|
|
1167
|
+
"www.topolo.app"
|
|
1162
1168
|
],
|
|
1163
1169
|
"routes": [
|
|
1164
|
-
"one.topolo.app/*",
|
|
1165
1170
|
"www.topolo.app/*"
|
|
1166
1171
|
],
|
|
1167
1172
|
"serviceId": "one"
|
|
@@ -1714,7 +1719,7 @@ var APPLICATIONS = {
|
|
|
1714
1719
|
};
|
|
1715
1720
|
|
|
1716
1721
|
// src/application-requirements.ts
|
|
1717
|
-
var APPLICATION_REQUIREMENTS_VERSION = "2026-
|
|
1722
|
+
var APPLICATION_REQUIREMENTS_VERSION = "2026-06-07.1";
|
|
1718
1723
|
var APPLICATION_REQUIREMENTS = [
|
|
1719
1724
|
{
|
|
1720
1725
|
id: "platform-metadata",
|
|
@@ -1858,11 +1863,13 @@ var APPLICATION_REQUIREMENTS = [
|
|
|
1858
1863
|
evidence: [
|
|
1859
1864
|
"The app-owned API exposes GET /api/widget",
|
|
1860
1865
|
"The endpoint returns TopoloWidgetApiResponse from @topolo/sdk",
|
|
1866
|
+
"Every returned widget includes snapshot.summary with app-owned operational prose",
|
|
1861
1867
|
"The endpoint derives organization/user context from the app auth boundary"
|
|
1862
1868
|
],
|
|
1863
1869
|
implementation: [
|
|
1864
1870
|
"Import createTopoloWidgetResponse from @topolo/sdk in the app backend",
|
|
1865
1871
|
"Return native app metrics or action widgets instead of generic launch-card fallbacks",
|
|
1872
|
+
"Populate snapshot.summary in the producing app for every widget instead of synthesizing summaries in TopoloOne",
|
|
1866
1873
|
"Add a route-level test that validates the response with validateTopoloWidgetResponse"
|
|
1867
1874
|
]
|
|
1868
1875
|
},
|
|
@@ -2106,10 +2113,7 @@ function resolveCatalogServiceUrl(entry, options = {}) {
|
|
|
2106
2113
|
if (override) return override;
|
|
2107
2114
|
const apiBaseUrl = firstString(entry.apiBaseUrl, entry.api_base_url);
|
|
2108
2115
|
if (apiBaseUrl) return apiBaseUrl;
|
|
2109
|
-
|
|
2110
|
-
const liveUrl = firstString(entry.baseUrl);
|
|
2111
|
-
if (bundledUrl && (!liveUrl || sameUrlEnvironment(bundledUrl, liveUrl))) return bundledUrl;
|
|
2112
|
-
return liveUrl ?? bundledUrl ?? null;
|
|
2116
|
+
return null;
|
|
2113
2117
|
}
|
|
2114
2118
|
function resolveServiceUrl(service, overrides) {
|
|
2115
2119
|
const override = resolveServiceUrlOverride([service], overrides);
|
|
@@ -2151,34 +2155,12 @@ function resolveServiceUrlOverride(keys, overrides) {
|
|
|
2151
2155
|
}
|
|
2152
2156
|
return null;
|
|
2153
2157
|
}
|
|
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
2158
|
function firstString(...values) {
|
|
2162
2159
|
for (const value of values) {
|
|
2163
2160
|
if (typeof value === "string" && value.trim()) return value.trim();
|
|
2164
2161
|
}
|
|
2165
2162
|
return null;
|
|
2166
2163
|
}
|
|
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
|
-
}
|
|
2182
2164
|
|
|
2183
2165
|
// src/client.ts
|
|
2184
2166
|
var WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
@@ -2351,7 +2333,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
2351
2333
|
async resolveLiveCallableService(serviceKey) {
|
|
2352
2334
|
const catalog = await this.listServices();
|
|
2353
2335
|
const match = catalog.find((entry) => catalogEntryMatches(entry, serviceKey));
|
|
2354
|
-
if (!match
|
|
2336
|
+
if (!match) return null;
|
|
2355
2337
|
const baseUrl = resolveCatalogServiceUrl(match, {
|
|
2356
2338
|
serviceKey,
|
|
2357
2339
|
overrides: this.serviceUrls
|
|
@@ -2449,7 +2431,6 @@ var TopoloClient = class _TopoloClient {
|
|
|
2449
2431
|
slug: typeof s.app_slug === "string" ? s.app_slug : null,
|
|
2450
2432
|
name: typeof s.name === "string" ? s.name : null,
|
|
2451
2433
|
description: typeof s.description === "string" ? s.description : null,
|
|
2452
|
-
baseUrl: typeof s.base_url === "string" ? s.base_url : null,
|
|
2453
2434
|
apiBaseUrl: typeof s.api_base_url === "string" ? s.api_base_url : typeof s.apiBaseUrl === "string" ? s.apiBaseUrl : null,
|
|
2454
2435
|
launchUrl: typeof s.launch_url === "string" ? s.launch_url : typeof s.launchUrl === "string" ? s.launchUrl : null,
|
|
2455
2436
|
status: typeof s.status === "string" ? s.status : null,
|
|
@@ -2536,7 +2517,6 @@ var TopoloClient = class _TopoloClient {
|
|
|
2536
2517
|
slug: app.id,
|
|
2537
2518
|
name: app.name ?? app.id,
|
|
2538
2519
|
description: null,
|
|
2539
|
-
baseUrl: app.productionUrl ?? null,
|
|
2540
2520
|
apiBaseUrl: null,
|
|
2541
2521
|
launchUrl: app.productionUrl ?? null,
|
|
2542
2522
|
status: "bundled",
|
|
@@ -2629,13 +2609,6 @@ function catalogEntryLookupKeys(entry) {
|
|
|
2629
2609
|
}
|
|
2630
2610
|
return [...keys];
|
|
2631
2611
|
}
|
|
2632
|
-
function isCallableCatalogEntry(entry) {
|
|
2633
|
-
const kind = normalizeLookupKey(entry.surfaceKind || "");
|
|
2634
|
-
if (kind === "api" || kind === "technical_service" || kind === "service") return true;
|
|
2635
|
-
return Boolean(
|
|
2636
|
-
(entry.baseUrl || entry.apiBaseUrl) && catalogEntryLookupKeys(entry).some((key) => Object.prototype.hasOwnProperty.call(DEFAULT_SERVICE_URLS, key))
|
|
2637
|
-
);
|
|
2638
|
-
}
|
|
2639
2612
|
function normalizeActionCatalogEntry(item) {
|
|
2640
2613
|
if (!item || typeof item !== "object" || Array.isArray(item)) return null;
|
|
2641
2614
|
const a = item;
|
|
@@ -3224,12 +3197,14 @@ function validateWidget(widget, path, errors) {
|
|
|
3224
3197
|
}
|
|
3225
3198
|
function validateSnapshotMetadata(snapshot, path, errors) {
|
|
3226
3199
|
if (snapshot === void 0) {
|
|
3200
|
+
errors.push(`${path} must be an object`);
|
|
3227
3201
|
return;
|
|
3228
3202
|
}
|
|
3229
3203
|
if (!isRecord(snapshot)) {
|
|
3230
|
-
errors.push(`${path} must be an object
|
|
3204
|
+
errors.push(`${path} must be an object`);
|
|
3231
3205
|
return;
|
|
3232
3206
|
}
|
|
3207
|
+
validateNonEmptyString(snapshot.summary, `${path}.summary`, errors);
|
|
3233
3208
|
if (snapshot.defaultMetricKeys !== void 0 && !Array.isArray(snapshot.defaultMetricKeys)) {
|
|
3234
3209
|
errors.push(`${path}.defaultMetricKeys must be an array when provided`);
|
|
3235
3210
|
}
|
package/dist/index.d.cts
CHANGED
|
@@ -104,7 +104,6 @@ interface TopoloServiceCatalogEntry {
|
|
|
104
104
|
slug: string | null;
|
|
105
105
|
name: string | null;
|
|
106
106
|
description: string | null;
|
|
107
|
-
baseUrl: string | null;
|
|
108
107
|
apiBaseUrl?: string | null;
|
|
109
108
|
launchUrl?: string | null;
|
|
110
109
|
status: string | null;
|
|
@@ -259,13 +258,13 @@ declare const SERVICES: {
|
|
|
259
258
|
readonly inventory: "https://inventory.topolo.app";
|
|
260
259
|
readonly learn: "https://learn-api.topolo.app";
|
|
261
260
|
readonly localize: "https://localize.topolo.app";
|
|
262
|
-
readonly mail: "https://mail.topolo.app";
|
|
261
|
+
readonly mail: "https://mail-api.topolo.app";
|
|
263
262
|
readonly mdm: "https://mdm-api.topolo.app";
|
|
264
263
|
readonly messages: "https://messages.topolo.app";
|
|
265
264
|
readonly nexus: "https://nexus-api.topolo.app";
|
|
266
265
|
readonly notify: "https://notify.topolo.app";
|
|
267
266
|
readonly observability_watch: "https://observability-watch.topolo.app";
|
|
268
|
-
readonly one: "https://
|
|
267
|
+
readonly one: "https://www.topolo.app";
|
|
269
268
|
readonly p2p: "https://p2p.topolo.app";
|
|
270
269
|
readonly pay: "https://pay.topolo.app";
|
|
271
270
|
readonly people: "https://people.topolo.app";
|
|
@@ -891,14 +890,14 @@ declare const APPLICATIONS: {
|
|
|
891
890
|
readonly apiUrl: "https://mail.topolo.app";
|
|
892
891
|
readonly domains: readonly ["mail.topolo.app"];
|
|
893
892
|
readonly routes: readonly [];
|
|
894
|
-
readonly serviceId:
|
|
893
|
+
readonly serviceId: null;
|
|
895
894
|
}, {
|
|
896
895
|
readonly name: "topolo-mail-api";
|
|
897
896
|
readonly kind: "worker";
|
|
898
|
-
readonly apiUrl:
|
|
899
|
-
readonly domains: readonly [];
|
|
897
|
+
readonly apiUrl: "https://mail-api.topolo.app";
|
|
898
|
+
readonly domains: readonly ["mail-api.topolo.app"];
|
|
900
899
|
readonly routes: readonly [];
|
|
901
|
-
readonly serviceId:
|
|
900
|
+
readonly serviceId: "mail";
|
|
902
901
|
}];
|
|
903
902
|
};
|
|
904
903
|
readonly mdm: {
|
|
@@ -1019,16 +1018,16 @@ declare const APPLICATIONS: {
|
|
|
1019
1018
|
readonly deployTargets: readonly [{
|
|
1020
1019
|
readonly name: "topolo-one";
|
|
1021
1020
|
readonly kind: "worker";
|
|
1022
|
-
readonly apiUrl:
|
|
1023
|
-
readonly domains: readonly [];
|
|
1024
|
-
readonly routes: readonly [];
|
|
1021
|
+
readonly apiUrl: "https://one.topolo.app";
|
|
1022
|
+
readonly domains: readonly ["one.topolo.app"];
|
|
1023
|
+
readonly routes: readonly ["one.topolo.app/*"];
|
|
1025
1024
|
readonly serviceId: null;
|
|
1026
1025
|
}, {
|
|
1027
1026
|
readonly name: "topolo-one-api";
|
|
1028
1027
|
readonly kind: "worker";
|
|
1029
|
-
readonly apiUrl: "https://
|
|
1030
|
-
readonly domains: readonly ["
|
|
1031
|
-
readonly routes: readonly ["
|
|
1028
|
+
readonly apiUrl: "https://www.topolo.app";
|
|
1029
|
+
readonly domains: readonly ["www.topolo.app"];
|
|
1030
|
+
readonly routes: readonly ["www.topolo.app/*"];
|
|
1032
1031
|
readonly serviceId: "one";
|
|
1033
1032
|
}, {
|
|
1034
1033
|
readonly name: "topolo-one-website";
|
|
@@ -1433,7 +1432,7 @@ type ApplicationId = keyof typeof APPLICATIONS;
|
|
|
1433
1432
|
type ApplicationCatalogEntry = (typeof APPLICATIONS)[ApplicationId];
|
|
1434
1433
|
type ApplicationDeployTarget = ApplicationCatalogEntry['deployTargets'][number];
|
|
1435
1434
|
|
|
1436
|
-
declare const APPLICATION_REQUIREMENTS_VERSION = "2026-
|
|
1435
|
+
declare const APPLICATION_REQUIREMENTS_VERSION = "2026-06-07.1";
|
|
1437
1436
|
type ApplicationRequirementScope = 'all' | 'browser' | 'api' | 'tooling' | 'agent_surface';
|
|
1438
1437
|
interface ApplicationRequirement {
|
|
1439
1438
|
id: string;
|
|
@@ -1542,8 +1541,8 @@ declare const APPLICATION_REQUIREMENTS: readonly [{
|
|
|
1542
1541
|
readonly title: "Expose a native dashboard widget contract";
|
|
1543
1542
|
readonly summary: "Launchable first-party applications must populate TopoloOne live workspace through an app-owned /api/widget endpoint using the SDK widget response contract.";
|
|
1544
1543
|
readonly appliesTo: readonly ["api"];
|
|
1545
|
-
readonly evidence: readonly ["The app-owned API exposes GET /api/widget", "The endpoint returns TopoloWidgetApiResponse from @topolo/sdk", "The endpoint derives organization/user context from the app auth boundary"];
|
|
1546
|
-
readonly implementation: readonly ["Import createTopoloWidgetResponse from @topolo/sdk in the app backend", "Return native app metrics or action widgets instead of generic launch-card fallbacks", "Add a route-level test that validates the response with validateTopoloWidgetResponse"];
|
|
1544
|
+
readonly evidence: readonly ["The app-owned API exposes GET /api/widget", "The endpoint returns TopoloWidgetApiResponse from @topolo/sdk", "Every returned widget includes snapshot.summary with app-owned operational prose", "The endpoint derives organization/user context from the app auth boundary"];
|
|
1545
|
+
readonly implementation: readonly ["Import createTopoloWidgetResponse from @topolo/sdk in the app backend", "Return native app metrics or action widgets instead of generic launch-card fallbacks", "Populate snapshot.summary in the producing app for every widget instead of synthesizing summaries in TopoloOne", "Add a route-level test that validates the response with validateTopoloWidgetResponse"];
|
|
1547
1546
|
}, {
|
|
1548
1547
|
readonly id: "observability-and-audit";
|
|
1549
1548
|
readonly title: "Emit platform audit and debugging signals";
|
|
@@ -1585,7 +1584,6 @@ interface ServiceCatalogUrlEntry {
|
|
|
1585
1584
|
serviceId?: string | null;
|
|
1586
1585
|
slug?: string | null;
|
|
1587
1586
|
name?: string | null;
|
|
1588
|
-
baseUrl?: string | null;
|
|
1589
1587
|
apiBaseUrl?: string | null;
|
|
1590
1588
|
launchUrl?: string | null;
|
|
1591
1589
|
api_base_url?: string | null;
|
|
@@ -1973,7 +1971,7 @@ interface TopoloWidgetBase {
|
|
|
1973
1971
|
serviceIcon?: string;
|
|
1974
1972
|
type: TopoloWidgetType;
|
|
1975
1973
|
lastUpdated: string;
|
|
1976
|
-
snapshot
|
|
1974
|
+
snapshot: TopoloWidgetSnapshotMetadata;
|
|
1977
1975
|
}
|
|
1978
1976
|
interface TopoloWidgetMetric {
|
|
1979
1977
|
key?: string;
|
|
@@ -1997,7 +1995,7 @@ interface TopoloWidgetSnapshotMetadata {
|
|
|
1997
1995
|
scope?: string;
|
|
1998
1996
|
scopeLabel?: string;
|
|
1999
1997
|
generatedAt?: string;
|
|
2000
|
-
summary
|
|
1998
|
+
summary: string;
|
|
2001
1999
|
defaultMetricKeys?: string[];
|
|
2002
2000
|
availableMetrics?: TopoloWidgetSnapshotMetric[];
|
|
2003
2001
|
highlights?: string[];
|
package/dist/index.d.ts
CHANGED
|
@@ -104,7 +104,6 @@ interface TopoloServiceCatalogEntry {
|
|
|
104
104
|
slug: string | null;
|
|
105
105
|
name: string | null;
|
|
106
106
|
description: string | null;
|
|
107
|
-
baseUrl: string | null;
|
|
108
107
|
apiBaseUrl?: string | null;
|
|
109
108
|
launchUrl?: string | null;
|
|
110
109
|
status: string | null;
|
|
@@ -259,13 +258,13 @@ declare const SERVICES: {
|
|
|
259
258
|
readonly inventory: "https://inventory.topolo.app";
|
|
260
259
|
readonly learn: "https://learn-api.topolo.app";
|
|
261
260
|
readonly localize: "https://localize.topolo.app";
|
|
262
|
-
readonly mail: "https://mail.topolo.app";
|
|
261
|
+
readonly mail: "https://mail-api.topolo.app";
|
|
263
262
|
readonly mdm: "https://mdm-api.topolo.app";
|
|
264
263
|
readonly messages: "https://messages.topolo.app";
|
|
265
264
|
readonly nexus: "https://nexus-api.topolo.app";
|
|
266
265
|
readonly notify: "https://notify.topolo.app";
|
|
267
266
|
readonly observability_watch: "https://observability-watch.topolo.app";
|
|
268
|
-
readonly one: "https://
|
|
267
|
+
readonly one: "https://www.topolo.app";
|
|
269
268
|
readonly p2p: "https://p2p.topolo.app";
|
|
270
269
|
readonly pay: "https://pay.topolo.app";
|
|
271
270
|
readonly people: "https://people.topolo.app";
|
|
@@ -891,14 +890,14 @@ declare const APPLICATIONS: {
|
|
|
891
890
|
readonly apiUrl: "https://mail.topolo.app";
|
|
892
891
|
readonly domains: readonly ["mail.topolo.app"];
|
|
893
892
|
readonly routes: readonly [];
|
|
894
|
-
readonly serviceId:
|
|
893
|
+
readonly serviceId: null;
|
|
895
894
|
}, {
|
|
896
895
|
readonly name: "topolo-mail-api";
|
|
897
896
|
readonly kind: "worker";
|
|
898
|
-
readonly apiUrl:
|
|
899
|
-
readonly domains: readonly [];
|
|
897
|
+
readonly apiUrl: "https://mail-api.topolo.app";
|
|
898
|
+
readonly domains: readonly ["mail-api.topolo.app"];
|
|
900
899
|
readonly routes: readonly [];
|
|
901
|
-
readonly serviceId:
|
|
900
|
+
readonly serviceId: "mail";
|
|
902
901
|
}];
|
|
903
902
|
};
|
|
904
903
|
readonly mdm: {
|
|
@@ -1019,16 +1018,16 @@ declare const APPLICATIONS: {
|
|
|
1019
1018
|
readonly deployTargets: readonly [{
|
|
1020
1019
|
readonly name: "topolo-one";
|
|
1021
1020
|
readonly kind: "worker";
|
|
1022
|
-
readonly apiUrl:
|
|
1023
|
-
readonly domains: readonly [];
|
|
1024
|
-
readonly routes: readonly [];
|
|
1021
|
+
readonly apiUrl: "https://one.topolo.app";
|
|
1022
|
+
readonly domains: readonly ["one.topolo.app"];
|
|
1023
|
+
readonly routes: readonly ["one.topolo.app/*"];
|
|
1025
1024
|
readonly serviceId: null;
|
|
1026
1025
|
}, {
|
|
1027
1026
|
readonly name: "topolo-one-api";
|
|
1028
1027
|
readonly kind: "worker";
|
|
1029
|
-
readonly apiUrl: "https://
|
|
1030
|
-
readonly domains: readonly ["
|
|
1031
|
-
readonly routes: readonly ["
|
|
1028
|
+
readonly apiUrl: "https://www.topolo.app";
|
|
1029
|
+
readonly domains: readonly ["www.topolo.app"];
|
|
1030
|
+
readonly routes: readonly ["www.topolo.app/*"];
|
|
1032
1031
|
readonly serviceId: "one";
|
|
1033
1032
|
}, {
|
|
1034
1033
|
readonly name: "topolo-one-website";
|
|
@@ -1433,7 +1432,7 @@ type ApplicationId = keyof typeof APPLICATIONS;
|
|
|
1433
1432
|
type ApplicationCatalogEntry = (typeof APPLICATIONS)[ApplicationId];
|
|
1434
1433
|
type ApplicationDeployTarget = ApplicationCatalogEntry['deployTargets'][number];
|
|
1435
1434
|
|
|
1436
|
-
declare const APPLICATION_REQUIREMENTS_VERSION = "2026-
|
|
1435
|
+
declare const APPLICATION_REQUIREMENTS_VERSION = "2026-06-07.1";
|
|
1437
1436
|
type ApplicationRequirementScope = 'all' | 'browser' | 'api' | 'tooling' | 'agent_surface';
|
|
1438
1437
|
interface ApplicationRequirement {
|
|
1439
1438
|
id: string;
|
|
@@ -1542,8 +1541,8 @@ declare const APPLICATION_REQUIREMENTS: readonly [{
|
|
|
1542
1541
|
readonly title: "Expose a native dashboard widget contract";
|
|
1543
1542
|
readonly summary: "Launchable first-party applications must populate TopoloOne live workspace through an app-owned /api/widget endpoint using the SDK widget response contract.";
|
|
1544
1543
|
readonly appliesTo: readonly ["api"];
|
|
1545
|
-
readonly evidence: readonly ["The app-owned API exposes GET /api/widget", "The endpoint returns TopoloWidgetApiResponse from @topolo/sdk", "The endpoint derives organization/user context from the app auth boundary"];
|
|
1546
|
-
readonly implementation: readonly ["Import createTopoloWidgetResponse from @topolo/sdk in the app backend", "Return native app metrics or action widgets instead of generic launch-card fallbacks", "Add a route-level test that validates the response with validateTopoloWidgetResponse"];
|
|
1544
|
+
readonly evidence: readonly ["The app-owned API exposes GET /api/widget", "The endpoint returns TopoloWidgetApiResponse from @topolo/sdk", "Every returned widget includes snapshot.summary with app-owned operational prose", "The endpoint derives organization/user context from the app auth boundary"];
|
|
1545
|
+
readonly implementation: readonly ["Import createTopoloWidgetResponse from @topolo/sdk in the app backend", "Return native app metrics or action widgets instead of generic launch-card fallbacks", "Populate snapshot.summary in the producing app for every widget instead of synthesizing summaries in TopoloOne", "Add a route-level test that validates the response with validateTopoloWidgetResponse"];
|
|
1547
1546
|
}, {
|
|
1548
1547
|
readonly id: "observability-and-audit";
|
|
1549
1548
|
readonly title: "Emit platform audit and debugging signals";
|
|
@@ -1585,7 +1584,6 @@ interface ServiceCatalogUrlEntry {
|
|
|
1585
1584
|
serviceId?: string | null;
|
|
1586
1585
|
slug?: string | null;
|
|
1587
1586
|
name?: string | null;
|
|
1588
|
-
baseUrl?: string | null;
|
|
1589
1587
|
apiBaseUrl?: string | null;
|
|
1590
1588
|
launchUrl?: string | null;
|
|
1591
1589
|
api_base_url?: string | null;
|
|
@@ -1973,7 +1971,7 @@ interface TopoloWidgetBase {
|
|
|
1973
1971
|
serviceIcon?: string;
|
|
1974
1972
|
type: TopoloWidgetType;
|
|
1975
1973
|
lastUpdated: string;
|
|
1976
|
-
snapshot
|
|
1974
|
+
snapshot: TopoloWidgetSnapshotMetadata;
|
|
1977
1975
|
}
|
|
1978
1976
|
interface TopoloWidgetMetric {
|
|
1979
1977
|
key?: string;
|
|
@@ -1997,7 +1995,7 @@ interface TopoloWidgetSnapshotMetadata {
|
|
|
1997
1995
|
scope?: string;
|
|
1998
1996
|
scopeLabel?: string;
|
|
1999
1997
|
generatedAt?: string;
|
|
2000
|
-
summary
|
|
1998
|
+
summary: string;
|
|
2001
1999
|
defaultMetricKeys?: string[];
|
|
2002
2000
|
availableMetrics?: TopoloWidgetSnapshotMetric[];
|
|
2003
2001
|
highlights?: string[];
|
package/dist/index.js
CHANGED
|
@@ -82,13 +82,13 @@ var SERVICES = {
|
|
|
82
82
|
inventory: "https://inventory.topolo.app",
|
|
83
83
|
learn: "https://learn-api.topolo.app",
|
|
84
84
|
localize: "https://localize.topolo.app",
|
|
85
|
-
mail: "https://mail.topolo.app",
|
|
85
|
+
mail: "https://mail-api.topolo.app",
|
|
86
86
|
mdm: "https://mdm-api.topolo.app",
|
|
87
87
|
messages: "https://messages.topolo.app",
|
|
88
88
|
nexus: "https://nexus-api.topolo.app",
|
|
89
89
|
notify: "https://notify.topolo.app",
|
|
90
90
|
observability_watch: "https://observability-watch.topolo.app",
|
|
91
|
-
one: "https://
|
|
91
|
+
one: "https://www.topolo.app",
|
|
92
92
|
p2p: "https://p2p.topolo.app",
|
|
93
93
|
pay: "https://pay.topolo.app",
|
|
94
94
|
people: "https://people.topolo.app",
|
|
@@ -920,15 +920,17 @@ var APPLICATIONS = {
|
|
|
920
920
|
"mail.topolo.app"
|
|
921
921
|
],
|
|
922
922
|
"routes": [],
|
|
923
|
-
"serviceId":
|
|
923
|
+
"serviceId": null
|
|
924
924
|
},
|
|
925
925
|
{
|
|
926
926
|
"name": "topolo-mail-api",
|
|
927
927
|
"kind": "worker",
|
|
928
|
-
"apiUrl":
|
|
929
|
-
"domains": [
|
|
928
|
+
"apiUrl": "https://mail-api.topolo.app",
|
|
929
|
+
"domains": [
|
|
930
|
+
"mail-api.topolo.app"
|
|
931
|
+
],
|
|
930
932
|
"routes": [],
|
|
931
|
-
"serviceId":
|
|
933
|
+
"serviceId": "mail"
|
|
932
934
|
}
|
|
933
935
|
]
|
|
934
936
|
},
|
|
@@ -1093,20 +1095,23 @@ var APPLICATIONS = {
|
|
|
1093
1095
|
{
|
|
1094
1096
|
"name": "topolo-one",
|
|
1095
1097
|
"kind": "worker",
|
|
1096
|
-
"apiUrl":
|
|
1097
|
-
"domains": [
|
|
1098
|
-
|
|
1098
|
+
"apiUrl": "https://one.topolo.app",
|
|
1099
|
+
"domains": [
|
|
1100
|
+
"one.topolo.app"
|
|
1101
|
+
],
|
|
1102
|
+
"routes": [
|
|
1103
|
+
"one.topolo.app/*"
|
|
1104
|
+
],
|
|
1099
1105
|
"serviceId": null
|
|
1100
1106
|
},
|
|
1101
1107
|
{
|
|
1102
1108
|
"name": "topolo-one-api",
|
|
1103
1109
|
"kind": "worker",
|
|
1104
|
-
"apiUrl": "https://
|
|
1110
|
+
"apiUrl": "https://www.topolo.app",
|
|
1105
1111
|
"domains": [
|
|
1106
|
-
"
|
|
1112
|
+
"www.topolo.app"
|
|
1107
1113
|
],
|
|
1108
1114
|
"routes": [
|
|
1109
|
-
"one.topolo.app/*",
|
|
1110
1115
|
"www.topolo.app/*"
|
|
1111
1116
|
],
|
|
1112
1117
|
"serviceId": "one"
|
|
@@ -1659,7 +1664,7 @@ var APPLICATIONS = {
|
|
|
1659
1664
|
};
|
|
1660
1665
|
|
|
1661
1666
|
// src/application-requirements.ts
|
|
1662
|
-
var APPLICATION_REQUIREMENTS_VERSION = "2026-
|
|
1667
|
+
var APPLICATION_REQUIREMENTS_VERSION = "2026-06-07.1";
|
|
1663
1668
|
var APPLICATION_REQUIREMENTS = [
|
|
1664
1669
|
{
|
|
1665
1670
|
id: "platform-metadata",
|
|
@@ -1803,11 +1808,13 @@ var APPLICATION_REQUIREMENTS = [
|
|
|
1803
1808
|
evidence: [
|
|
1804
1809
|
"The app-owned API exposes GET /api/widget",
|
|
1805
1810
|
"The endpoint returns TopoloWidgetApiResponse from @topolo/sdk",
|
|
1811
|
+
"Every returned widget includes snapshot.summary with app-owned operational prose",
|
|
1806
1812
|
"The endpoint derives organization/user context from the app auth boundary"
|
|
1807
1813
|
],
|
|
1808
1814
|
implementation: [
|
|
1809
1815
|
"Import createTopoloWidgetResponse from @topolo/sdk in the app backend",
|
|
1810
1816
|
"Return native app metrics or action widgets instead of generic launch-card fallbacks",
|
|
1817
|
+
"Populate snapshot.summary in the producing app for every widget instead of synthesizing summaries in TopoloOne",
|
|
1811
1818
|
"Add a route-level test that validates the response with validateTopoloWidgetResponse"
|
|
1812
1819
|
]
|
|
1813
1820
|
},
|
|
@@ -2051,10 +2058,7 @@ function resolveCatalogServiceUrl(entry, options = {}) {
|
|
|
2051
2058
|
if (override) return override;
|
|
2052
2059
|
const apiBaseUrl = firstString(entry.apiBaseUrl, entry.api_base_url);
|
|
2053
2060
|
if (apiBaseUrl) return apiBaseUrl;
|
|
2054
|
-
|
|
2055
|
-
const liveUrl = firstString(entry.baseUrl);
|
|
2056
|
-
if (bundledUrl && (!liveUrl || sameUrlEnvironment(bundledUrl, liveUrl))) return bundledUrl;
|
|
2057
|
-
return liveUrl ?? bundledUrl ?? null;
|
|
2061
|
+
return null;
|
|
2058
2062
|
}
|
|
2059
2063
|
function resolveServiceUrl(service, overrides) {
|
|
2060
2064
|
const override = resolveServiceUrlOverride([service], overrides);
|
|
@@ -2096,34 +2100,12 @@ function resolveServiceUrlOverride(keys, overrides) {
|
|
|
2096
2100
|
}
|
|
2097
2101
|
return null;
|
|
2098
2102
|
}
|
|
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
2103
|
function firstString(...values) {
|
|
2107
2104
|
for (const value of values) {
|
|
2108
2105
|
if (typeof value === "string" && value.trim()) return value.trim();
|
|
2109
2106
|
}
|
|
2110
2107
|
return null;
|
|
2111
2108
|
}
|
|
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
|
-
}
|
|
2127
2109
|
|
|
2128
2110
|
// src/client.ts
|
|
2129
2111
|
var WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
@@ -2296,7 +2278,7 @@ var TopoloClient = class _TopoloClient {
|
|
|
2296
2278
|
async resolveLiveCallableService(serviceKey) {
|
|
2297
2279
|
const catalog = await this.listServices();
|
|
2298
2280
|
const match = catalog.find((entry) => catalogEntryMatches(entry, serviceKey));
|
|
2299
|
-
if (!match
|
|
2281
|
+
if (!match) return null;
|
|
2300
2282
|
const baseUrl = resolveCatalogServiceUrl(match, {
|
|
2301
2283
|
serviceKey,
|
|
2302
2284
|
overrides: this.serviceUrls
|
|
@@ -2394,7 +2376,6 @@ var TopoloClient = class _TopoloClient {
|
|
|
2394
2376
|
slug: typeof s.app_slug === "string" ? s.app_slug : null,
|
|
2395
2377
|
name: typeof s.name === "string" ? s.name : null,
|
|
2396
2378
|
description: typeof s.description === "string" ? s.description : null,
|
|
2397
|
-
baseUrl: typeof s.base_url === "string" ? s.base_url : null,
|
|
2398
2379
|
apiBaseUrl: typeof s.api_base_url === "string" ? s.api_base_url : typeof s.apiBaseUrl === "string" ? s.apiBaseUrl : null,
|
|
2399
2380
|
launchUrl: typeof s.launch_url === "string" ? s.launch_url : typeof s.launchUrl === "string" ? s.launchUrl : null,
|
|
2400
2381
|
status: typeof s.status === "string" ? s.status : null,
|
|
@@ -2481,7 +2462,6 @@ var TopoloClient = class _TopoloClient {
|
|
|
2481
2462
|
slug: app.id,
|
|
2482
2463
|
name: app.name ?? app.id,
|
|
2483
2464
|
description: null,
|
|
2484
|
-
baseUrl: app.productionUrl ?? null,
|
|
2485
2465
|
apiBaseUrl: null,
|
|
2486
2466
|
launchUrl: app.productionUrl ?? null,
|
|
2487
2467
|
status: "bundled",
|
|
@@ -2574,13 +2554,6 @@ function catalogEntryLookupKeys(entry) {
|
|
|
2574
2554
|
}
|
|
2575
2555
|
return [...keys];
|
|
2576
2556
|
}
|
|
2577
|
-
function isCallableCatalogEntry(entry) {
|
|
2578
|
-
const kind = normalizeLookupKey(entry.surfaceKind || "");
|
|
2579
|
-
if (kind === "api" || kind === "technical_service" || kind === "service") return true;
|
|
2580
|
-
return Boolean(
|
|
2581
|
-
(entry.baseUrl || entry.apiBaseUrl) && catalogEntryLookupKeys(entry).some((key) => Object.prototype.hasOwnProperty.call(DEFAULT_SERVICE_URLS, key))
|
|
2582
|
-
);
|
|
2583
|
-
}
|
|
2584
2557
|
function normalizeActionCatalogEntry(item) {
|
|
2585
2558
|
if (!item || typeof item !== "object" || Array.isArray(item)) return null;
|
|
2586
2559
|
const a = item;
|
|
@@ -3169,12 +3142,14 @@ function validateWidget(widget, path, errors) {
|
|
|
3169
3142
|
}
|
|
3170
3143
|
function validateSnapshotMetadata(snapshot, path, errors) {
|
|
3171
3144
|
if (snapshot === void 0) {
|
|
3145
|
+
errors.push(`${path} must be an object`);
|
|
3172
3146
|
return;
|
|
3173
3147
|
}
|
|
3174
3148
|
if (!isRecord(snapshot)) {
|
|
3175
|
-
errors.push(`${path} must be an object
|
|
3149
|
+
errors.push(`${path} must be an object`);
|
|
3176
3150
|
return;
|
|
3177
3151
|
}
|
|
3152
|
+
validateNonEmptyString(snapshot.summary, `${path}.summary`, errors);
|
|
3178
3153
|
if (snapshot.defaultMetricKeys !== void 0 && !Array.isArray(snapshot.defaultMetricKeys)) {
|
|
3179
3154
|
errors.push(`${path}.defaultMetricKeys must be an array when provided`);
|
|
3180
3155
|
}
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
type ApplicationId,
|
|
5
5
|
} from './applications.generated.js';
|
|
6
6
|
|
|
7
|
-
export const APPLICATION_REQUIREMENTS_VERSION = '2026-
|
|
7
|
+
export const APPLICATION_REQUIREMENTS_VERSION = '2026-06-07.1';
|
|
8
8
|
|
|
9
9
|
export type ApplicationRequirementScope =
|
|
10
10
|
| 'all'
|
|
@@ -218,11 +218,13 @@ export const APPLICATION_REQUIREMENTS = [
|
|
|
218
218
|
evidence: [
|
|
219
219
|
'The app-owned API exposes GET /api/widget',
|
|
220
220
|
'The endpoint returns TopoloWidgetApiResponse from @topolo/sdk',
|
|
221
|
+
'Every returned widget includes snapshot.summary with app-owned operational prose',
|
|
221
222
|
'The endpoint derives organization/user context from the app auth boundary',
|
|
222
223
|
],
|
|
223
224
|
implementation: [
|
|
224
225
|
'Import createTopoloWidgetResponse from @topolo/sdk in the app backend',
|
|
225
226
|
'Return native app metrics or action widgets instead of generic launch-card fallbacks',
|
|
227
|
+
'Populate snapshot.summary in the producing app for every widget instead of synthesizing summaries in TopoloOne',
|
|
226
228
|
'Add a route-level test that validates the response with validateTopoloWidgetResponse',
|
|
227
229
|
],
|
|
228
230
|
},
|
|
@@ -813,15 +813,17 @@ export const APPLICATIONS = {
|
|
|
813
813
|
"mail.topolo.app"
|
|
814
814
|
],
|
|
815
815
|
"routes": [],
|
|
816
|
-
"serviceId":
|
|
816
|
+
"serviceId": null
|
|
817
817
|
},
|
|
818
818
|
{
|
|
819
819
|
"name": "topolo-mail-api",
|
|
820
820
|
"kind": "worker",
|
|
821
|
-
"apiUrl":
|
|
822
|
-
"domains": [
|
|
821
|
+
"apiUrl": "https://mail-api.topolo.app",
|
|
822
|
+
"domains": [
|
|
823
|
+
"mail-api.topolo.app"
|
|
824
|
+
],
|
|
823
825
|
"routes": [],
|
|
824
|
-
"serviceId":
|
|
826
|
+
"serviceId": "mail"
|
|
825
827
|
}
|
|
826
828
|
]
|
|
827
829
|
},
|
|
@@ -986,20 +988,23 @@ export const APPLICATIONS = {
|
|
|
986
988
|
{
|
|
987
989
|
"name": "topolo-one",
|
|
988
990
|
"kind": "worker",
|
|
989
|
-
"apiUrl":
|
|
990
|
-
"domains": [
|
|
991
|
-
|
|
991
|
+
"apiUrl": "https://one.topolo.app",
|
|
992
|
+
"domains": [
|
|
993
|
+
"one.topolo.app"
|
|
994
|
+
],
|
|
995
|
+
"routes": [
|
|
996
|
+
"one.topolo.app/*"
|
|
997
|
+
],
|
|
992
998
|
"serviceId": null
|
|
993
999
|
},
|
|
994
1000
|
{
|
|
995
1001
|
"name": "topolo-one-api",
|
|
996
1002
|
"kind": "worker",
|
|
997
|
-
"apiUrl": "https://
|
|
1003
|
+
"apiUrl": "https://www.topolo.app",
|
|
998
1004
|
"domains": [
|
|
999
|
-
"
|
|
1005
|
+
"www.topolo.app"
|
|
1000
1006
|
],
|
|
1001
1007
|
"routes": [
|
|
1002
|
-
"one.topolo.app/*",
|
|
1003
1008
|
"www.topolo.app/*"
|
|
1004
1009
|
],
|
|
1005
1010
|
"serviceId": "one"
|
package/src/client.test.ts
CHANGED
|
@@ -25,7 +25,8 @@ const MAIL_CATALOG = {
|
|
|
25
25
|
service_id: 'srv_mail',
|
|
26
26
|
app_slug: 'topolo-mail',
|
|
27
27
|
name: 'Topolo Mail',
|
|
28
|
-
|
|
28
|
+
api_base_url: 'https://mail-api.topolo.app',
|
|
29
|
+
launch_url: 'https://mail.topolo.app',
|
|
29
30
|
surface_kind: 'api',
|
|
30
31
|
status: 'active',
|
|
31
32
|
granted_permissions: ['srv_mail.messages:read'],
|
|
@@ -376,7 +377,7 @@ describe('TopoloClient.getServiceIdBySlug', () => {
|
|
|
376
377
|
|
|
377
378
|
it('falls back to /api/services/by-slug/:slug for unknown slugs', async () => {
|
|
378
379
|
const { impl, calls } = stubFetch([
|
|
379
|
-
{ body: { success: true, data: { service_id: 'service_abc123',
|
|
380
|
+
{ body: { success: true, data: { service_id: 'service_abc123', api_base_url: 'https://x-api' } } },
|
|
380
381
|
]);
|
|
381
382
|
const client = freshClient(impl, { auth: 'https://auth.example.com' });
|
|
382
383
|
const id = await client.getServiceIdBySlug('external-service');
|
|
@@ -452,7 +453,8 @@ describe('TopoloClient.listServices', () => {
|
|
|
452
453
|
app_slug: 'topolo-director',
|
|
453
454
|
name: 'Topolo Director',
|
|
454
455
|
description: 'Demo control plane',
|
|
455
|
-
|
|
456
|
+
api_base_url: 'https://director-api.topolo.app',
|
|
457
|
+
launch_url: 'https://director.topolo.app',
|
|
456
458
|
status: 'active',
|
|
457
459
|
surface_kind: 'application',
|
|
458
460
|
launchable: true,
|
|
@@ -476,9 +478,8 @@ describe('TopoloClient.listServices', () => {
|
|
|
476
478
|
slug: 'topolo-director',
|
|
477
479
|
name: 'Topolo Director',
|
|
478
480
|
description: 'Demo control plane',
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
launchUrl: null,
|
|
481
|
+
apiBaseUrl: 'https://director-api.topolo.app',
|
|
482
|
+
launchUrl: 'https://director.topolo.app',
|
|
482
483
|
status: 'active',
|
|
483
484
|
surfaceKind: 'application',
|
|
484
485
|
launchable: true,
|
|
@@ -514,7 +515,8 @@ describe('TopoloClient.listServices', () => {
|
|
|
514
515
|
service_id: 'srv_mail',
|
|
515
516
|
app_slug: 'topolo-mail',
|
|
516
517
|
name: 'Topolo Mail',
|
|
517
|
-
|
|
518
|
+
api_base_url: 'https://mail-api.topolo.app',
|
|
519
|
+
launch_url: 'https://mail.topolo.app',
|
|
518
520
|
surface_kind: 'application',
|
|
519
521
|
status: 'active',
|
|
520
522
|
granted_permissions: ['srv_mail.messages:read'],
|
|
@@ -535,7 +537,7 @@ describe('TopoloClient.listServices', () => {
|
|
|
535
537
|
await client.request({ service: 'mail', path: '/api/health' });
|
|
536
538
|
|
|
537
539
|
expect(calls[0]!.url).toBe('https://auth.example.com/api/services/catalog');
|
|
538
|
-
expect(calls[1]!.url).toBe('https://mail.topolo.app/api/health');
|
|
540
|
+
expect(calls[1]!.url).toBe('https://mail-api.topolo.app/api/health');
|
|
539
541
|
});
|
|
540
542
|
|
|
541
543
|
it('routes known split-surface services to the callable API URL', async () => {
|
|
@@ -548,7 +550,8 @@ describe('TopoloClient.listServices', () => {
|
|
|
548
550
|
service_id: 'srv_crm',
|
|
549
551
|
app_slug: 'topolo-crm',
|
|
550
552
|
name: 'Topolo CRM',
|
|
551
|
-
|
|
553
|
+
api_base_url: 'https://crm-api.topolo.app',
|
|
554
|
+
launch_url: 'https://crm.topolo.app',
|
|
552
555
|
surface_kind: 'application',
|
|
553
556
|
status: 'active',
|
|
554
557
|
granted_permissions: ['srv_crm.contacts:read'],
|
|
@@ -582,8 +585,8 @@ describe('TopoloClient.listServices', () => {
|
|
|
582
585
|
service_id: 'srv_future',
|
|
583
586
|
app_slug: 'topolo-future',
|
|
584
587
|
name: 'Future',
|
|
585
|
-
base_url: 'https://future.topolo.app',
|
|
586
588
|
api_base_url: 'https://future-api.topolo.app',
|
|
589
|
+
launch_url: 'https://future.topolo.app',
|
|
587
590
|
surface_kind: 'api',
|
|
588
591
|
status: 'active',
|
|
589
592
|
granted_permissions: ['srv_future.read:all'],
|
|
@@ -616,7 +619,8 @@ describe('TopoloClient.listServices', () => {
|
|
|
616
619
|
service_id: 'srv_crm',
|
|
617
620
|
app_slug: 'topolo-crm',
|
|
618
621
|
name: 'Topolo CRM',
|
|
619
|
-
|
|
622
|
+
api_base_url: 'https://crm-api.topolo.app',
|
|
623
|
+
launch_url: 'https://crm.topolo.app',
|
|
620
624
|
surface_kind: 'api',
|
|
621
625
|
status: 'active',
|
|
622
626
|
granted_permissions: ['srv_crm.contacts:read'],
|
|
@@ -649,7 +653,7 @@ describe('TopoloClient.listServices', () => {
|
|
|
649
653
|
service_id: 'srv_future_app',
|
|
650
654
|
app_slug: 'topolo-future-app',
|
|
651
655
|
name: 'Future App',
|
|
652
|
-
|
|
656
|
+
launch_url: 'https://future-app.topolo.app',
|
|
653
657
|
surface_kind: 'application',
|
|
654
658
|
status: 'active',
|
|
655
659
|
granted_permissions: ['srv_future_app.launch:read'],
|
|
@@ -682,7 +686,8 @@ describe('TopoloClient.listServices', () => {
|
|
|
682
686
|
service_id: 'srv_future',
|
|
683
687
|
app_slug: 'topolo-warehouse',
|
|
684
688
|
name: 'Warehouse',
|
|
685
|
-
|
|
689
|
+
api_base_url: 'https://warehouse.topolo.app',
|
|
690
|
+
launch_url: 'https://warehouse.topolo.app',
|
|
686
691
|
surface_kind: 'api',
|
|
687
692
|
status: 'active',
|
|
688
693
|
granted_permissions: ['srv_future.read:all'],
|
|
@@ -741,7 +746,7 @@ describe('TopoloClient action catalog', () => {
|
|
|
741
746
|
await client.callAction('messages.list', { mailboxId: 'primary', q: 'Ada', page: 2 });
|
|
742
747
|
const call = requestCalls(calls)[0]!;
|
|
743
748
|
const url = new URL(call.url);
|
|
744
|
-
expect(url.toString()).toBe('https://mail.topolo.app/api/mail/mailboxes/primary/messages?q=Ada&page=2');
|
|
749
|
+
expect(url.toString()).toBe('https://mail-api.topolo.app/api/mail/mailboxes/primary/messages?q=Ada&page=2');
|
|
745
750
|
expect(call.init.method).toBe('GET');
|
|
746
751
|
const headers = new Headers(call.init.headers);
|
|
747
752
|
expect(headers.get('X-Topolo-Action-Id')).toBe('srv_mail.messages.list');
|
package/src/client.ts
CHANGED
|
@@ -106,7 +106,6 @@ export interface TopoloServiceCatalogEntry {
|
|
|
106
106
|
slug: string | null;
|
|
107
107
|
name: string | null;
|
|
108
108
|
description: string | null;
|
|
109
|
-
baseUrl: string | null;
|
|
110
109
|
apiBaseUrl?: string | null;
|
|
111
110
|
launchUrl?: string | null;
|
|
112
111
|
status: string | null;
|
|
@@ -328,7 +327,7 @@ export class TopoloClient {
|
|
|
328
327
|
private async resolveLiveCallableService(serviceKey: string): Promise<{ serviceKey: string; baseUrl: string } | null> {
|
|
329
328
|
const catalog = await this.listServices();
|
|
330
329
|
const match = catalog.find((entry) => catalogEntryMatches(entry, serviceKey));
|
|
331
|
-
if (!match
|
|
330
|
+
if (!match) return null;
|
|
332
331
|
const baseUrl = resolveCatalogServiceUrl(match, {
|
|
333
332
|
serviceKey,
|
|
334
333
|
overrides: this.serviceUrls,
|
|
@@ -454,7 +453,6 @@ export class TopoloClient {
|
|
|
454
453
|
name: typeof s.name === 'string' ? s.name : null,
|
|
455
454
|
description:
|
|
456
455
|
typeof s.description === 'string' ? s.description : null,
|
|
457
|
-
baseUrl: typeof s.base_url === 'string' ? s.base_url : null,
|
|
458
456
|
apiBaseUrl:
|
|
459
457
|
typeof s.api_base_url === 'string'
|
|
460
458
|
? s.api_base_url
|
|
@@ -573,7 +571,6 @@ export class TopoloClient {
|
|
|
573
571
|
slug: app.id,
|
|
574
572
|
name: app.name ?? app.id,
|
|
575
573
|
description: null,
|
|
576
|
-
baseUrl: app.productionUrl ?? null,
|
|
577
574
|
apiBaseUrl: null,
|
|
578
575
|
launchUrl: app.productionUrl ?? null,
|
|
579
576
|
status: 'bundled',
|
|
@@ -695,15 +692,6 @@ function catalogEntryLookupKeys(entry: TopoloServiceCatalogEntry): string[] {
|
|
|
695
692
|
return [...keys];
|
|
696
693
|
}
|
|
697
694
|
|
|
698
|
-
function isCallableCatalogEntry(entry: TopoloServiceCatalogEntry): boolean {
|
|
699
|
-
const kind = normalizeLookupKey(entry.surfaceKind || '');
|
|
700
|
-
if (kind === 'api' || kind === 'technical_service' || kind === 'service') return true;
|
|
701
|
-
return Boolean(
|
|
702
|
-
(entry.baseUrl || entry.apiBaseUrl) &&
|
|
703
|
-
catalogEntryLookupKeys(entry).some((key) => Object.prototype.hasOwnProperty.call(DEFAULT_SERVICE_URLS, key)),
|
|
704
|
-
);
|
|
705
|
-
}
|
|
706
|
-
|
|
707
695
|
function normalizeActionCatalogEntry(item: unknown): TopoloActionCatalogEntry | null {
|
|
708
696
|
if (!item || typeof item !== 'object' || Array.isArray(item)) return null;
|
|
709
697
|
const a = item as Record<string, unknown>;
|
package/src/modules/p2p.test.ts
CHANGED
|
@@ -22,7 +22,8 @@ function stubFetch(responses: Array<{ status?: number; body?: unknown }>) {
|
|
|
22
22
|
service_id: 'srv_p2p',
|
|
23
23
|
app_slug: 'topolo-p2p',
|
|
24
24
|
name: 'Topolo P2P',
|
|
25
|
-
|
|
25
|
+
api_base_url: 'https://p2p.topolo.app',
|
|
26
|
+
launch_url: 'https://p2p.topolo.app',
|
|
26
27
|
surface_kind: 'api',
|
|
27
28
|
status: 'active',
|
|
28
29
|
granted_permissions: ['srv_p2p.directory:read'],
|
|
@@ -27,13 +27,13 @@ export const SERVICES = {
|
|
|
27
27
|
inventory: "https://inventory.topolo.app",
|
|
28
28
|
learn: "https://learn-api.topolo.app",
|
|
29
29
|
localize: "https://localize.topolo.app",
|
|
30
|
-
mail: "https://mail.topolo.app",
|
|
30
|
+
mail: "https://mail-api.topolo.app",
|
|
31
31
|
mdm: "https://mdm-api.topolo.app",
|
|
32
32
|
messages: "https://messages.topolo.app",
|
|
33
33
|
nexus: "https://nexus-api.topolo.app",
|
|
34
34
|
notify: "https://notify.topolo.app",
|
|
35
35
|
observability_watch: "https://observability-watch.topolo.app",
|
|
36
|
-
one: "https://
|
|
36
|
+
one: "https://www.topolo.app",
|
|
37
37
|
p2p: "https://p2p.topolo.app",
|
|
38
38
|
pay: "https://pay.topolo.app",
|
|
39
39
|
people: "https://people.topolo.app",
|
package/src/services.test.ts
CHANGED
|
@@ -70,8 +70,8 @@ describe('service registry', () => {
|
|
|
70
70
|
books: 'https://books.topolo.app',
|
|
71
71
|
consent: 'https://consent.topolo.app',
|
|
72
72
|
feed: 'https://feed-api.topolo.app',
|
|
73
|
-
mail: 'https://mail.topolo.app',
|
|
74
|
-
one: 'https://
|
|
73
|
+
mail: 'https://mail-api.topolo.app',
|
|
74
|
+
one: 'https://www.topolo.app',
|
|
75
75
|
web: 'https://web-api.topolo.app',
|
|
76
76
|
});
|
|
77
77
|
expect(DEFAULT_SERVICE_URLS).toMatchObject({
|
|
@@ -99,30 +99,31 @@ describe('service registry', () => {
|
|
|
99
99
|
}
|
|
100
100
|
});
|
|
101
101
|
|
|
102
|
-
it('resolves
|
|
102
|
+
it('resolves services from explicit catalog API URLs', () => {
|
|
103
103
|
expect(resolveCatalogServiceUrl({
|
|
104
104
|
serviceId: 'srv_crm',
|
|
105
105
|
slug: 'topolo-crm',
|
|
106
106
|
name: 'Topolo CRM',
|
|
107
|
-
|
|
107
|
+
launchUrl: 'https://crm.topolo.app',
|
|
108
|
+
apiBaseUrl: 'https://crm-api.topolo.app',
|
|
108
109
|
}, { serviceKey: 'crm' })).toBe('https://crm-api.topolo.app');
|
|
109
110
|
});
|
|
110
111
|
|
|
111
|
-
it('
|
|
112
|
+
it('does not infer callable URLs from launcher URLs', () => {
|
|
112
113
|
expect(resolveCatalogServiceUrl({
|
|
113
114
|
serviceId: 'srv_crm',
|
|
114
115
|
slug: 'topolo-crm',
|
|
115
116
|
name: 'Topolo CRM',
|
|
116
|
-
|
|
117
|
-
}, { serviceKey: 'crm' })).
|
|
117
|
+
launchUrl: 'https://crm.stg.topolo.us',
|
|
118
|
+
}, { serviceKey: 'crm' })).toBeNull();
|
|
118
119
|
});
|
|
119
120
|
|
|
120
|
-
it('prefers explicit catalog API URLs over
|
|
121
|
+
it('prefers explicit catalog API URLs over launch URLs', () => {
|
|
121
122
|
expect(resolveCatalogServiceUrl({
|
|
122
123
|
serviceId: 'srv_future',
|
|
123
124
|
slug: 'topolo-future',
|
|
124
125
|
name: 'Future',
|
|
125
|
-
|
|
126
|
+
launchUrl: 'https://future.topolo.app',
|
|
126
127
|
apiBaseUrl: 'https://future-api.topolo.app',
|
|
127
128
|
})).toBe('https://future-api.topolo.app');
|
|
128
129
|
});
|
package/src/services.ts
CHANGED
|
@@ -51,7 +51,6 @@ export interface ServiceCatalogUrlEntry {
|
|
|
51
51
|
serviceId?: string | null;
|
|
52
52
|
slug?: string | null;
|
|
53
53
|
name?: string | null;
|
|
54
|
-
baseUrl?: string | null;
|
|
55
54
|
apiBaseUrl?: string | null;
|
|
56
55
|
launchUrl?: string | null;
|
|
57
56
|
api_base_url?: string | null;
|
|
@@ -72,11 +71,7 @@ export function resolveCatalogServiceUrl(
|
|
|
72
71
|
const apiBaseUrl = firstString(entry.apiBaseUrl, entry.api_base_url);
|
|
73
72
|
if (apiBaseUrl) return apiBaseUrl;
|
|
74
73
|
|
|
75
|
-
|
|
76
|
-
const liveUrl = firstString(entry.baseUrl);
|
|
77
|
-
if (bundledUrl && (!liveUrl || sameUrlEnvironment(bundledUrl, liveUrl))) return bundledUrl;
|
|
78
|
-
|
|
79
|
-
return liveUrl ?? bundledUrl ?? null;
|
|
74
|
+
return null;
|
|
80
75
|
}
|
|
81
76
|
|
|
82
77
|
export function resolveServiceUrl(
|
|
@@ -135,34 +130,9 @@ function resolveServiceUrlOverride(
|
|
|
135
130
|
return null;
|
|
136
131
|
}
|
|
137
132
|
|
|
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
133
|
function firstString(...values: Array<string | null | undefined>): string | null {
|
|
147
134
|
for (const value of values) {
|
|
148
135
|
if (typeof value === 'string' && value.trim()) return value.trim();
|
|
149
136
|
}
|
|
150
137
|
return null;
|
|
151
138
|
}
|
|
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
|
-
}
|
package/src/widgets.test.ts
CHANGED
|
@@ -21,6 +21,9 @@ describe("Topolo widget contract", () => {
|
|
|
21
21
|
widgets: [
|
|
22
22
|
{
|
|
23
23
|
type: "stats",
|
|
24
|
+
snapshot: {
|
|
25
|
+
summary: "Bytes is reporting 12 files in the current workspace.",
|
|
26
|
+
},
|
|
24
27
|
stats: [{ key: "files", label: "Files", value: 12 }],
|
|
25
28
|
},
|
|
26
29
|
],
|
|
@@ -34,6 +37,9 @@ describe("Topolo widget contract", () => {
|
|
|
34
37
|
serviceName: "Bytes",
|
|
35
38
|
lastUpdated: "2026-04-25T00:00:00.000Z",
|
|
36
39
|
type: "stats",
|
|
40
|
+
snapshot: {
|
|
41
|
+
summary: "Bytes is reporting 12 files in the current workspace.",
|
|
42
|
+
},
|
|
37
43
|
stats: [{ key: "files", label: "Files", value: 12 }],
|
|
38
44
|
},
|
|
39
45
|
],
|
|
@@ -51,6 +57,9 @@ describe("Topolo widget contract", () => {
|
|
|
51
57
|
serviceName: "CRM",
|
|
52
58
|
lastUpdated: "2026-04-24T23:00:00.000Z",
|
|
53
59
|
type: "stats",
|
|
60
|
+
snapshot: {
|
|
61
|
+
summary: "CRM is tracking 3 contacts for this workspace.",
|
|
62
|
+
},
|
|
54
63
|
stats: [{ label: "Contacts", value: 3 }],
|
|
55
64
|
},
|
|
56
65
|
],
|
|
@@ -79,6 +88,9 @@ describe("Topolo widget contract", () => {
|
|
|
79
88
|
widgets: [
|
|
80
89
|
{
|
|
81
90
|
type: "notifications",
|
|
91
|
+
snapshot: {
|
|
92
|
+
summary: "CRM has one unread lead notification for review.",
|
|
93
|
+
},
|
|
82
94
|
unreadCount: 1,
|
|
83
95
|
notifications: [
|
|
84
96
|
{
|
|
@@ -100,7 +112,7 @@ describe("Topolo widget contract", () => {
|
|
|
100
112
|
});
|
|
101
113
|
});
|
|
102
114
|
|
|
103
|
-
it("keeps
|
|
115
|
+
it("keeps required snapshot metadata for configurable TopoloOne datapoints", () => {
|
|
104
116
|
const response = createTopoloWidgetResponse({
|
|
105
117
|
serviceId: "service_admin",
|
|
106
118
|
serviceName: "Admin",
|
|
@@ -111,6 +123,8 @@ describe("Topolo widget contract", () => {
|
|
|
111
123
|
stats: [{ label: "Team Members", value: 5 }],
|
|
112
124
|
snapshot: {
|
|
113
125
|
presetKey: "admin_morning_snapshot",
|
|
126
|
+
summary:
|
|
127
|
+
"Admin has 5 team members visible in the current workspace snapshot.",
|
|
114
128
|
defaultMetricKeys: ["team_members"],
|
|
115
129
|
availableMetrics: [
|
|
116
130
|
{
|
|
@@ -131,6 +145,8 @@ describe("Topolo widget contract", () => {
|
|
|
131
145
|
});
|
|
132
146
|
expect(response.widgets[0]).toMatchObject({
|
|
133
147
|
snapshot: {
|
|
148
|
+
summary:
|
|
149
|
+
"Admin has 5 team members visible in the current workspace snapshot.",
|
|
134
150
|
defaultMetricKeys: ["team_members"],
|
|
135
151
|
availableMetrics: [
|
|
136
152
|
{ key: "team_members", label: "Team Members", value: 5 },
|
|
@@ -156,7 +172,29 @@ describe("Topolo widget contract", () => {
|
|
|
156
172
|
expect(result.errors).toEqual([
|
|
157
173
|
"widgets[0].serviceId must be a non-empty string",
|
|
158
174
|
"widgets[0].lastUpdated must be a non-empty string",
|
|
175
|
+
"widgets[0].snapshot must be an object",
|
|
159
176
|
"widgets[0].type must be a supported Topolo widget type",
|
|
160
177
|
]);
|
|
161
178
|
});
|
|
179
|
+
|
|
180
|
+
it("reports missing snapshot summaries", () => {
|
|
181
|
+
const result = validateTopoloWidgetResponse({
|
|
182
|
+
success: true,
|
|
183
|
+
widgets: [
|
|
184
|
+
{
|
|
185
|
+
serviceId: "service_bad",
|
|
186
|
+
serviceName: "Bad App",
|
|
187
|
+
lastUpdated: "2026-04-25T00:00:00.000Z",
|
|
188
|
+
type: "stats",
|
|
189
|
+
snapshot: { summary: "" },
|
|
190
|
+
stats: [],
|
|
191
|
+
},
|
|
192
|
+
],
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
expect(result.ok).toBe(false);
|
|
196
|
+
expect(result.errors).toEqual([
|
|
197
|
+
"widgets[0].snapshot.summary must be a non-empty string",
|
|
198
|
+
]);
|
|
199
|
+
});
|
|
162
200
|
});
|
package/src/widgets.ts
CHANGED
|
@@ -14,7 +14,7 @@ export interface TopoloWidgetBase {
|
|
|
14
14
|
serviceIcon?: string;
|
|
15
15
|
type: TopoloWidgetType;
|
|
16
16
|
lastUpdated: string;
|
|
17
|
-
snapshot
|
|
17
|
+
snapshot: TopoloWidgetSnapshotMetadata;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
export interface TopoloWidgetMetric {
|
|
@@ -41,7 +41,7 @@ export interface TopoloWidgetSnapshotMetadata {
|
|
|
41
41
|
scope?: string;
|
|
42
42
|
scopeLabel?: string;
|
|
43
43
|
generatedAt?: string;
|
|
44
|
-
summary
|
|
44
|
+
summary: string;
|
|
45
45
|
defaultMetricKeys?: string[];
|
|
46
46
|
availableMetrics?: TopoloWidgetSnapshotMetric[];
|
|
47
47
|
highlights?: string[];
|
|
@@ -291,12 +291,14 @@ function validateSnapshotMetadata(
|
|
|
291
291
|
errors: string[],
|
|
292
292
|
) {
|
|
293
293
|
if (snapshot === undefined) {
|
|
294
|
+
errors.push(`${path} must be an object`);
|
|
294
295
|
return;
|
|
295
296
|
}
|
|
296
297
|
if (!isRecord(snapshot)) {
|
|
297
|
-
errors.push(`${path} must be an object
|
|
298
|
+
errors.push(`${path} must be an object`);
|
|
298
299
|
return;
|
|
299
300
|
}
|
|
301
|
+
validateNonEmptyString(snapshot.summary, `${path}.summary`, errors);
|
|
300
302
|
if (
|
|
301
303
|
snapshot.defaultMetricKeys !== undefined &&
|
|
302
304
|
!Array.isArray(snapshot.defaultMetricKeys)
|