@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 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://one.topolo.app",
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": "mail"
978
+ "serviceId": null
979
979
  },
980
980
  {
981
981
  "name": "topolo-mail-api",
982
982
  "kind": "worker",
983
- "apiUrl": null,
984
- "domains": [],
983
+ "apiUrl": "https://mail-api.topolo.app",
984
+ "domains": [
985
+ "mail-api.topolo.app"
986
+ ],
985
987
  "routes": [],
986
- "serviceId": null
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": null,
1152
- "domains": [],
1153
- "routes": [],
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://one.topolo.app",
1165
+ "apiUrl": "https://www.topolo.app",
1160
1166
  "domains": [
1161
- "one.topolo.app"
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-05-01.1";
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
- const bundledUrl = firstBundledServiceUrl(keys);
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 || !isCallableCatalogEntry(match)) return null;
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 when provided`);
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://one.topolo.app";
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: "mail";
893
+ readonly serviceId: null;
895
894
  }, {
896
895
  readonly name: "topolo-mail-api";
897
896
  readonly kind: "worker";
898
- readonly apiUrl: null;
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: null;
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: null;
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://one.topolo.app";
1030
- readonly domains: readonly ["one.topolo.app"];
1031
- readonly routes: readonly ["one.topolo.app/*", "www.topolo.app/*"];
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-05-01.1";
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?: TopoloWidgetSnapshotMetadata;
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?: string;
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://one.topolo.app";
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: "mail";
893
+ readonly serviceId: null;
895
894
  }, {
896
895
  readonly name: "topolo-mail-api";
897
896
  readonly kind: "worker";
898
- readonly apiUrl: null;
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: null;
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: null;
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://one.topolo.app";
1030
- readonly domains: readonly ["one.topolo.app"];
1031
- readonly routes: readonly ["one.topolo.app/*", "www.topolo.app/*"];
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-05-01.1";
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?: TopoloWidgetSnapshotMetadata;
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?: string;
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://one.topolo.app",
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": "mail"
923
+ "serviceId": null
924
924
  },
925
925
  {
926
926
  "name": "topolo-mail-api",
927
927
  "kind": "worker",
928
- "apiUrl": null,
929
- "domains": [],
928
+ "apiUrl": "https://mail-api.topolo.app",
929
+ "domains": [
930
+ "mail-api.topolo.app"
931
+ ],
930
932
  "routes": [],
931
- "serviceId": null
933
+ "serviceId": "mail"
932
934
  }
933
935
  ]
934
936
  },
@@ -1093,20 +1095,23 @@ var APPLICATIONS = {
1093
1095
  {
1094
1096
  "name": "topolo-one",
1095
1097
  "kind": "worker",
1096
- "apiUrl": null,
1097
- "domains": [],
1098
- "routes": [],
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://one.topolo.app",
1110
+ "apiUrl": "https://www.topolo.app",
1105
1111
  "domains": [
1106
- "one.topolo.app"
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-05-01.1";
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
- const bundledUrl = firstBundledServiceUrl(keys);
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 || !isCallableCatalogEntry(match)) return null;
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 when provided`);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@topolo/sdk",
3
- "version": "0.4.5",
3
+ "version": "0.4.7",
4
4
  "description": "Typed client SDK for the Topolo platform. Used by TopoloCli, TopoloMCP, and third-party agents.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -4,7 +4,7 @@ import {
4
4
  type ApplicationId,
5
5
  } from './applications.generated.js';
6
6
 
7
- export const APPLICATION_REQUIREMENTS_VERSION = '2026-05-01.1';
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": "mail"
816
+ "serviceId": null
817
817
  },
818
818
  {
819
819
  "name": "topolo-mail-api",
820
820
  "kind": "worker",
821
- "apiUrl": null,
822
- "domains": [],
821
+ "apiUrl": "https://mail-api.topolo.app",
822
+ "domains": [
823
+ "mail-api.topolo.app"
824
+ ],
823
825
  "routes": [],
824
- "serviceId": null
826
+ "serviceId": "mail"
825
827
  }
826
828
  ]
827
829
  },
@@ -986,20 +988,23 @@ export const APPLICATIONS = {
986
988
  {
987
989
  "name": "topolo-one",
988
990
  "kind": "worker",
989
- "apiUrl": null,
990
- "domains": [],
991
- "routes": [],
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://one.topolo.app",
1003
+ "apiUrl": "https://www.topolo.app",
998
1004
  "domains": [
999
- "one.topolo.app"
1005
+ "www.topolo.app"
1000
1006
  ],
1001
1007
  "routes": [
1002
- "one.topolo.app/*",
1003
1008
  "www.topolo.app/*"
1004
1009
  ],
1005
1010
  "serviceId": "one"
@@ -25,7 +25,8 @@ const MAIL_CATALOG = {
25
25
  service_id: 'srv_mail',
26
26
  app_slug: 'topolo-mail',
27
27
  name: 'Topolo Mail',
28
- base_url: 'https://mail-api.topolo.app',
28
+ api_base_url: 'https://mail-api.topolo.app',
29
+ launch_url: 'https://mail.topolo.app',
29
30
  surface_kind: 'api',
30
31
  status: 'active',
31
32
  granted_permissions: ['srv_mail.messages:read'],
@@ -376,7 +377,7 @@ describe('TopoloClient.getServiceIdBySlug', () => {
376
377
 
377
378
  it('falls back to /api/services/by-slug/:slug for unknown slugs', async () => {
378
379
  const { impl, calls } = stubFetch([
379
- { body: { success: true, data: { service_id: 'service_abc123', base_url: 'https://x' } } },
380
+ { body: { success: true, data: { service_id: 'service_abc123', api_base_url: 'https://x-api' } } },
380
381
  ]);
381
382
  const client = freshClient(impl, { auth: 'https://auth.example.com' });
382
383
  const id = await client.getServiceIdBySlug('external-service');
@@ -452,7 +453,8 @@ describe('TopoloClient.listServices', () => {
452
453
  app_slug: 'topolo-director',
453
454
  name: 'Topolo Director',
454
455
  description: 'Demo control plane',
455
- base_url: 'https://director.topolo.app',
456
+ api_base_url: 'https://director-api.topolo.app',
457
+ launch_url: 'https://director.topolo.app',
456
458
  status: 'active',
457
459
  surface_kind: 'application',
458
460
  launchable: true,
@@ -476,9 +478,8 @@ describe('TopoloClient.listServices', () => {
476
478
  slug: 'topolo-director',
477
479
  name: 'Topolo Director',
478
480
  description: 'Demo control plane',
479
- baseUrl: 'https://director.topolo.app',
480
- apiBaseUrl: null,
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
- base_url: 'https://mail.topolo.app',
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
- base_url: 'https://crm.topolo.app',
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
- base_url: 'https://crm-api.topolo.app',
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
- base_url: 'https://future-app.topolo.app',
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
- base_url: 'https://warehouse.topolo.app',
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 || !isCallableCatalogEntry(match)) return null;
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>;
@@ -22,7 +22,8 @@ function stubFetch(responses: Array<{ status?: number; body?: unknown }>) {
22
22
  service_id: 'srv_p2p',
23
23
  app_slug: 'topolo-p2p',
24
24
  name: 'Topolo P2P',
25
- base_url: 'https://p2p.topolo.app',
25
+ api_base_url: 'https://p2p.topolo.app',
26
+ launch_url: 'https://p2p.topolo.app',
26
27
  surface_kind: 'api',
27
28
  status: 'active',
28
29
  granted_permissions: ['srv_p2p.directory:read'],
@@ -27,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://one.topolo.app",
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",
@@ -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://one.topolo.app',
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 known split-surface services to the generated callable URL', () => {
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
- baseUrl: 'https://crm.topolo.app',
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('uses a live staging URL instead of a production bundled default', () => {
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
- baseUrl: 'https://crm.stg.topolo.us',
117
- }, { serviceKey: 'crm' })).toBe('https://crm.stg.topolo.us');
117
+ launchUrl: 'https://crm.stg.topolo.us',
118
+ }, { serviceKey: 'crm' })).toBeNull();
118
119
  });
119
120
 
120
- it('prefers explicit catalog API URLs over launcher URLs', () => {
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
- baseUrl: 'https://future.topolo.app',
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
- 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;
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
- }
@@ -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 optional snapshot metadata for configurable TopoloOne datapoints", () => {
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?: TopoloWidgetSnapshotMetadata;
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?: string;
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 when provided`);
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)