@topolo/sdk 0.2.5 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -0
- package/dist/index.cjs +166 -3
- package/dist/index.d.cts +111 -2
- package/dist/index.d.ts +111 -2
- package/dist/index.js +155 -2
- package/package.json +1 -1
- package/src/application-requirements.ts +25 -1
- package/src/index.ts +29 -0
- package/src/services.test.ts +4 -0
- package/src/widgets.test.ts +123 -0
- package/src/widgets.ts +288 -0
package/README.md
CHANGED
|
@@ -94,6 +94,29 @@ that contract into catalog-backed conformance reports and migration-queue items.
|
|
|
94
94
|
Catalog-visible evidence is marked directly; implementation details that require
|
|
95
95
|
code or docs inspection are deliberately marked `needs_review`.
|
|
96
96
|
|
|
97
|
+
## Native dashboard widgets
|
|
98
|
+
|
|
99
|
+
Launchable first-party applications populate the TopoloOne live workspace through
|
|
100
|
+
their own `GET /api/widget` endpoint. The SDK owns the shared payload contract:
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
import { createTopoloWidgetResponse } from '@topolo/sdk';
|
|
104
|
+
|
|
105
|
+
return Response.json(createTopoloWidgetResponse({
|
|
106
|
+
serviceId: 'srv_topolo_example',
|
|
107
|
+
serviceName: 'Example',
|
|
108
|
+
widgets: [
|
|
109
|
+
{
|
|
110
|
+
type: 'stats',
|
|
111
|
+
stats: [{ label: 'Open items', value: 3 }],
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
}));
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Use `validateTopoloWidgetResponse()` in endpoint tests to prevent app-local
|
|
118
|
+
payload drift.
|
|
119
|
+
|
|
97
120
|
## Escape hatch
|
|
98
121
|
|
|
99
122
|
```ts
|
package/dist/index.cjs
CHANGED
|
@@ -27,6 +27,8 @@ __export(index_exports, {
|
|
|
27
27
|
DEFAULT_SERVICE_URLS: () => DEFAULT_SERVICE_URLS,
|
|
28
28
|
IdentityModule: () => IdentityModule,
|
|
29
29
|
PLATFORM_SERVICE_IDS: () => PLATFORM_SERVICE_IDS,
|
|
30
|
+
TOPOLO_WIDGET_ENDPOINT: () => TOPOLO_WIDGET_ENDPOINT,
|
|
31
|
+
TOPOLO_WIDGET_REFRESH_INTERVAL_MS: () => TOPOLO_WIDGET_REFRESH_INTERVAL_MS,
|
|
30
32
|
TopoloAuthError: () => TopoloAuthError,
|
|
31
33
|
TopoloClient: () => TopoloClient,
|
|
32
34
|
TopoloHttpError: () => TopoloHttpError,
|
|
@@ -37,8 +39,16 @@ __export(index_exports, {
|
|
|
37
39
|
auditAllApplicationRequirements: () => auditAllApplicationRequirements,
|
|
38
40
|
auditApplicationRequirements: () => auditApplicationRequirements,
|
|
39
41
|
createTopolo: () => createTopolo,
|
|
42
|
+
createTopoloWidgetError: () => createTopoloWidgetError,
|
|
43
|
+
createTopoloWidgetResponse: () => createTopoloWidgetResponse,
|
|
44
|
+
defineTopoloChartWidget: () => defineTopoloChartWidget,
|
|
45
|
+
defineTopoloNotificationsWidget: () => defineTopoloNotificationsWidget,
|
|
46
|
+
defineTopoloQuickActionsWidget: () => defineTopoloQuickActionsWidget,
|
|
47
|
+
defineTopoloRecentActivityWidget: () => defineTopoloRecentActivityWidget,
|
|
48
|
+
defineTopoloStatsWidget: () => defineTopoloStatsWidget,
|
|
40
49
|
requirementsForApplication: () => requirementsForApplication,
|
|
41
|
-
resolveServiceUrl: () => resolveServiceUrl
|
|
50
|
+
resolveServiceUrl: () => resolveServiceUrl,
|
|
51
|
+
validateTopoloWidgetResponse: () => validateTopoloWidgetResponse
|
|
42
52
|
});
|
|
43
53
|
module.exports = __toCommonJS(index_exports);
|
|
44
54
|
|
|
@@ -1219,7 +1229,7 @@ var APPLICATIONS = {
|
|
|
1219
1229
|
};
|
|
1220
1230
|
|
|
1221
1231
|
// src/application-requirements.ts
|
|
1222
|
-
var APPLICATION_REQUIREMENTS_VERSION = "2026-04-
|
|
1232
|
+
var APPLICATION_REQUIREMENTS_VERSION = "2026-04-25.1";
|
|
1223
1233
|
var APPLICATION_REQUIREMENTS = [
|
|
1224
1234
|
{
|
|
1225
1235
|
id: "platform-metadata",
|
|
@@ -1351,6 +1361,22 @@ var APPLICATION_REQUIREMENTS = [
|
|
|
1351
1361
|
"Keep command output JSON-stable for agents and human-readable with --no-json"
|
|
1352
1362
|
]
|
|
1353
1363
|
},
|
|
1364
|
+
{
|
|
1365
|
+
id: "native-dashboard-widget",
|
|
1366
|
+
title: "Expose a native dashboard widget contract",
|
|
1367
|
+
summary: "Launchable first-party applications must populate TopoloOne live workspace through an app-owned /api/widget endpoint using the SDK widget response contract.",
|
|
1368
|
+
appliesTo: ["api"],
|
|
1369
|
+
evidence: [
|
|
1370
|
+
"The app-owned API exposes GET /api/widget",
|
|
1371
|
+
"The endpoint returns TopoloWidgetApiResponse from @topolo/sdk",
|
|
1372
|
+
"The endpoint derives organization/user context from the app auth boundary"
|
|
1373
|
+
],
|
|
1374
|
+
implementation: [
|
|
1375
|
+
"Import createTopoloWidgetResponse from @topolo/sdk in the app backend",
|
|
1376
|
+
"Return native app metrics or action widgets instead of generic launch-card fallbacks",
|
|
1377
|
+
"Add a route-level test that validates the response with validateTopoloWidgetResponse"
|
|
1378
|
+
]
|
|
1379
|
+
},
|
|
1354
1380
|
{
|
|
1355
1381
|
id: "observability-and-audit",
|
|
1356
1382
|
title: "Emit platform audit and debugging signals",
|
|
@@ -1522,6 +1548,13 @@ function evaluateRequirement(application, requirement) {
|
|
|
1522
1548
|
}
|
|
1523
1549
|
return finding(requirement, "missing", "No SDK service or tooling surface is registered.", evidence);
|
|
1524
1550
|
}
|
|
1551
|
+
case "native-dashboard-widget":
|
|
1552
|
+
return finding(
|
|
1553
|
+
requirement,
|
|
1554
|
+
"needs_review",
|
|
1555
|
+
"Catalog metadata cannot verify app-owned /api/widget implementation details.",
|
|
1556
|
+
evidence
|
|
1557
|
+
);
|
|
1525
1558
|
case "observability-and-audit":
|
|
1526
1559
|
return finding(
|
|
1527
1560
|
requirement,
|
|
@@ -1920,6 +1953,126 @@ var TopoloOAuth = class {
|
|
|
1920
1953
|
}
|
|
1921
1954
|
};
|
|
1922
1955
|
|
|
1956
|
+
// src/widgets.ts
|
|
1957
|
+
var TOPOLO_WIDGET_ENDPOINT = "/api/widget";
|
|
1958
|
+
var TOPOLO_WIDGET_REFRESH_INTERVAL_MS = 6e4;
|
|
1959
|
+
function defineTopoloStatsWidget(widget) {
|
|
1960
|
+
return { ...widget, type: "stats" };
|
|
1961
|
+
}
|
|
1962
|
+
function defineTopoloRecentActivityWidget(widget) {
|
|
1963
|
+
return { ...widget, type: "recent-activity" };
|
|
1964
|
+
}
|
|
1965
|
+
function defineTopoloQuickActionsWidget(widget) {
|
|
1966
|
+
return { ...widget, type: "quick-actions" };
|
|
1967
|
+
}
|
|
1968
|
+
function defineTopoloNotificationsWidget(widget) {
|
|
1969
|
+
return { ...widget, type: "notifications" };
|
|
1970
|
+
}
|
|
1971
|
+
function defineTopoloChartWidget(widget) {
|
|
1972
|
+
return { ...widget, type: "chart" };
|
|
1973
|
+
}
|
|
1974
|
+
function createTopoloWidgetResponse({
|
|
1975
|
+
serviceId,
|
|
1976
|
+
serviceName,
|
|
1977
|
+
widgets,
|
|
1978
|
+
now
|
|
1979
|
+
}) {
|
|
1980
|
+
const lastUpdated = toIsoTimestamp(now);
|
|
1981
|
+
return {
|
|
1982
|
+
success: true,
|
|
1983
|
+
widgets: widgets.map((widget) => ({
|
|
1984
|
+
...widget,
|
|
1985
|
+
serviceId: widget.serviceId?.trim() || serviceId,
|
|
1986
|
+
serviceName: widget.serviceName?.trim() || serviceName,
|
|
1987
|
+
lastUpdated: widget.lastUpdated?.trim() || lastUpdated
|
|
1988
|
+
}))
|
|
1989
|
+
};
|
|
1990
|
+
}
|
|
1991
|
+
function createTopoloWidgetError(error) {
|
|
1992
|
+
return {
|
|
1993
|
+
success: false,
|
|
1994
|
+
widgets: [],
|
|
1995
|
+
error
|
|
1996
|
+
};
|
|
1997
|
+
}
|
|
1998
|
+
function validateTopoloWidgetResponse(response) {
|
|
1999
|
+
const errors = [];
|
|
2000
|
+
if (!isRecord(response)) {
|
|
2001
|
+
return { ok: false, errors: ["response must be an object"] };
|
|
2002
|
+
}
|
|
2003
|
+
if (typeof response.success !== "boolean") {
|
|
2004
|
+
errors.push("success must be a boolean");
|
|
2005
|
+
}
|
|
2006
|
+
if (!Array.isArray(response.widgets)) {
|
|
2007
|
+
errors.push("widgets must be an array");
|
|
2008
|
+
return { ok: errors.length === 0, errors };
|
|
2009
|
+
}
|
|
2010
|
+
response.widgets.forEach((widget, index) => {
|
|
2011
|
+
validateWidget(widget, `widgets[${index}]`, errors);
|
|
2012
|
+
});
|
|
2013
|
+
if (response.success === false && response.error !== void 0 && typeof response.error !== "string") {
|
|
2014
|
+
errors.push("error must be a string when provided");
|
|
2015
|
+
}
|
|
2016
|
+
return { ok: errors.length === 0, errors };
|
|
2017
|
+
}
|
|
2018
|
+
function validateWidget(widget, path, errors) {
|
|
2019
|
+
if (!isRecord(widget)) {
|
|
2020
|
+
errors.push(`${path} must be an object`);
|
|
2021
|
+
return;
|
|
2022
|
+
}
|
|
2023
|
+
validateNonEmptyString(widget.serviceId, `${path}.serviceId`, errors);
|
|
2024
|
+
validateNonEmptyString(widget.serviceName, `${path}.serviceName`, errors);
|
|
2025
|
+
validateNonEmptyString(widget.lastUpdated, `${path}.lastUpdated`, errors);
|
|
2026
|
+
switch (widget.type) {
|
|
2027
|
+
case "stats":
|
|
2028
|
+
validateArray(widget.stats, `${path}.stats`, errors);
|
|
2029
|
+
break;
|
|
2030
|
+
case "recent-activity":
|
|
2031
|
+
validateArray(widget.activities, `${path}.activities`, errors);
|
|
2032
|
+
break;
|
|
2033
|
+
case "quick-actions":
|
|
2034
|
+
validateArray(widget.actions, `${path}.actions`, errors);
|
|
2035
|
+
break;
|
|
2036
|
+
case "notifications":
|
|
2037
|
+
validateArray(widget.notifications, `${path}.notifications`, errors);
|
|
2038
|
+
if (typeof widget.unreadCount !== "number") {
|
|
2039
|
+
errors.push(`${path}.unreadCount must be a number`);
|
|
2040
|
+
}
|
|
2041
|
+
break;
|
|
2042
|
+
case "chart":
|
|
2043
|
+
validateNonEmptyString(widget.chartType, `${path}.chartType`, errors);
|
|
2044
|
+
validateNonEmptyString(widget.title, `${path}.title`, errors);
|
|
2045
|
+
if (!isRecord(widget.data)) {
|
|
2046
|
+
errors.push(`${path}.data must be an object`);
|
|
2047
|
+
} else {
|
|
2048
|
+
validateArray(widget.data.labels, `${path}.data.labels`, errors);
|
|
2049
|
+
validateArray(widget.data.values, `${path}.data.values`, errors);
|
|
2050
|
+
}
|
|
2051
|
+
break;
|
|
2052
|
+
default:
|
|
2053
|
+
errors.push(`${path}.type must be a supported Topolo widget type`);
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
function validateNonEmptyString(value, path, errors) {
|
|
2057
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
2058
|
+
errors.push(`${path} must be a non-empty string`);
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
function validateArray(value, path, errors) {
|
|
2062
|
+
if (!Array.isArray(value)) {
|
|
2063
|
+
errors.push(`${path} must be an array`);
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
function isRecord(value) {
|
|
2067
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2068
|
+
}
|
|
2069
|
+
function toIsoTimestamp(input) {
|
|
2070
|
+
if (input instanceof Date) {
|
|
2071
|
+
return input.toISOString();
|
|
2072
|
+
}
|
|
2073
|
+
return input ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
2074
|
+
}
|
|
2075
|
+
|
|
1923
2076
|
// src/index.ts
|
|
1924
2077
|
function createTopolo(options) {
|
|
1925
2078
|
const client = new TopoloClient(options);
|
|
@@ -1938,6 +2091,8 @@ function createTopolo(options) {
|
|
|
1938
2091
|
DEFAULT_SERVICE_URLS,
|
|
1939
2092
|
IdentityModule,
|
|
1940
2093
|
PLATFORM_SERVICE_IDS,
|
|
2094
|
+
TOPOLO_WIDGET_ENDPOINT,
|
|
2095
|
+
TOPOLO_WIDGET_REFRESH_INTERVAL_MS,
|
|
1941
2096
|
TopoloAuthError,
|
|
1942
2097
|
TopoloClient,
|
|
1943
2098
|
TopoloHttpError,
|
|
@@ -1948,6 +2103,14 @@ function createTopolo(options) {
|
|
|
1948
2103
|
auditAllApplicationRequirements,
|
|
1949
2104
|
auditApplicationRequirements,
|
|
1950
2105
|
createTopolo,
|
|
2106
|
+
createTopoloWidgetError,
|
|
2107
|
+
createTopoloWidgetResponse,
|
|
2108
|
+
defineTopoloChartWidget,
|
|
2109
|
+
defineTopoloNotificationsWidget,
|
|
2110
|
+
defineTopoloQuickActionsWidget,
|
|
2111
|
+
defineTopoloRecentActivityWidget,
|
|
2112
|
+
defineTopoloStatsWidget,
|
|
1951
2113
|
requirementsForApplication,
|
|
1952
|
-
resolveServiceUrl
|
|
2114
|
+
resolveServiceUrl,
|
|
2115
|
+
validateTopoloWidgetResponse
|
|
1953
2116
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -847,7 +847,7 @@ type ApplicationId = keyof typeof APPLICATIONS;
|
|
|
847
847
|
type ApplicationCatalogEntry = (typeof APPLICATIONS)[ApplicationId];
|
|
848
848
|
type ApplicationDeployTarget = ApplicationCatalogEntry['deployTargets'][number];
|
|
849
849
|
|
|
850
|
-
declare const APPLICATION_REQUIREMENTS_VERSION = "2026-04-
|
|
850
|
+
declare const APPLICATION_REQUIREMENTS_VERSION = "2026-04-25.1";
|
|
851
851
|
type ApplicationRequirementScope = 'all' | 'browser' | 'api' | 'tooling' | 'agent_surface';
|
|
852
852
|
interface ApplicationRequirement {
|
|
853
853
|
id: string;
|
|
@@ -951,6 +951,13 @@ declare const APPLICATION_REQUIREMENTS: readonly [{
|
|
|
951
951
|
readonly appliesTo: readonly ["api", "tooling", "agent_surface"];
|
|
952
952
|
readonly evidence: readonly ["Read operations have typed SDK/CLI/MCP surfaces once their contract stabilizes", "Mutating generic calls require --confirm or confirm:true", "MCP tools never accept orgId and advertise only appropriate scope-gated tools"];
|
|
953
953
|
readonly implementation: readonly ["Add SDK types first, then CLI commands and MCP tools for durable workflows", "Use generic API passthrough only as a temporary escape hatch", "Keep command output JSON-stable for agents and human-readable with --no-json"];
|
|
954
|
+
}, {
|
|
955
|
+
readonly id: "native-dashboard-widget";
|
|
956
|
+
readonly title: "Expose a native dashboard widget contract";
|
|
957
|
+
readonly summary: "Launchable first-party applications must populate TopoloOne live workspace through an app-owned /api/widget endpoint using the SDK widget response contract.";
|
|
958
|
+
readonly appliesTo: readonly ["api"];
|
|
959
|
+
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"];
|
|
960
|
+
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"];
|
|
954
961
|
}, {
|
|
955
962
|
readonly id: "observability-and-audit";
|
|
956
963
|
readonly title: "Emit platform audit and debugging signals";
|
|
@@ -1235,6 +1242,108 @@ declare class TopoloOAuth {
|
|
|
1235
1242
|
}): Promise<TokenResponse>;
|
|
1236
1243
|
}
|
|
1237
1244
|
|
|
1245
|
+
declare const TOPOLO_WIDGET_ENDPOINT = "/api/widget";
|
|
1246
|
+
declare const TOPOLO_WIDGET_REFRESH_INTERVAL_MS = 60000;
|
|
1247
|
+
type TopoloWidgetType = 'stats' | 'recent-activity' | 'quick-actions' | 'notifications' | 'chart';
|
|
1248
|
+
interface TopoloWidgetBase {
|
|
1249
|
+
serviceId: string;
|
|
1250
|
+
serviceName: string;
|
|
1251
|
+
serviceIcon?: string;
|
|
1252
|
+
type: TopoloWidgetType;
|
|
1253
|
+
lastUpdated: string;
|
|
1254
|
+
}
|
|
1255
|
+
interface TopoloWidgetMetric {
|
|
1256
|
+
label: string;
|
|
1257
|
+
value: string | number;
|
|
1258
|
+
change?: number;
|
|
1259
|
+
changeLabel?: string;
|
|
1260
|
+
icon?: string;
|
|
1261
|
+
}
|
|
1262
|
+
interface TopoloStatsWidget extends TopoloWidgetBase {
|
|
1263
|
+
type: 'stats';
|
|
1264
|
+
stats: TopoloWidgetMetric[];
|
|
1265
|
+
}
|
|
1266
|
+
interface TopoloWidgetActivityItem {
|
|
1267
|
+
id: string;
|
|
1268
|
+
title: string;
|
|
1269
|
+
description?: string;
|
|
1270
|
+
timestamp: string;
|
|
1271
|
+
icon?: string;
|
|
1272
|
+
link?: string;
|
|
1273
|
+
}
|
|
1274
|
+
interface TopoloRecentActivityWidget extends TopoloWidgetBase {
|
|
1275
|
+
type: 'recent-activity';
|
|
1276
|
+
activities: TopoloWidgetActivityItem[];
|
|
1277
|
+
}
|
|
1278
|
+
interface TopoloWidgetActionItem {
|
|
1279
|
+
id: string;
|
|
1280
|
+
label: string;
|
|
1281
|
+
icon?: string;
|
|
1282
|
+
link: string;
|
|
1283
|
+
primary?: boolean;
|
|
1284
|
+
}
|
|
1285
|
+
interface TopoloQuickActionsWidget extends TopoloWidgetBase {
|
|
1286
|
+
type: 'quick-actions';
|
|
1287
|
+
actions: TopoloWidgetActionItem[];
|
|
1288
|
+
}
|
|
1289
|
+
interface TopoloWidgetNotificationItem {
|
|
1290
|
+
id: string;
|
|
1291
|
+
title: string;
|
|
1292
|
+
message: string;
|
|
1293
|
+
type: 'info' | 'warning' | 'error' | 'success';
|
|
1294
|
+
timestamp: string;
|
|
1295
|
+
read: boolean;
|
|
1296
|
+
link?: string;
|
|
1297
|
+
}
|
|
1298
|
+
interface TopoloNotificationsWidget extends TopoloWidgetBase {
|
|
1299
|
+
type: 'notifications';
|
|
1300
|
+
notifications: TopoloWidgetNotificationItem[];
|
|
1301
|
+
unreadCount: number;
|
|
1302
|
+
}
|
|
1303
|
+
interface TopoloChartWidget extends TopoloWidgetBase {
|
|
1304
|
+
type: 'chart';
|
|
1305
|
+
chartType: 'line' | 'bar' | 'pie' | 'donut';
|
|
1306
|
+
title: string;
|
|
1307
|
+
data: {
|
|
1308
|
+
labels: string[];
|
|
1309
|
+
values: number[];
|
|
1310
|
+
};
|
|
1311
|
+
}
|
|
1312
|
+
type TopoloWidget = TopoloStatsWidget | TopoloRecentActivityWidget | TopoloQuickActionsWidget | TopoloNotificationsWidget | TopoloChartWidget;
|
|
1313
|
+
type TopoloWidgetWithOptionalMetadata<T extends TopoloWidget> = Omit<T, 'serviceId' | 'serviceName' | 'lastUpdated'> & Partial<Pick<TopoloWidgetBase, 'serviceId' | 'serviceName' | 'lastUpdated'>>;
|
|
1314
|
+
type TopoloWidgetDraft = TopoloWidget extends infer Widget ? Widget extends TopoloWidget ? TopoloWidgetWithOptionalMetadata<Widget> : never : never;
|
|
1315
|
+
interface TopoloWidgetApiResponse {
|
|
1316
|
+
success: boolean;
|
|
1317
|
+
widgets: TopoloWidget[];
|
|
1318
|
+
error?: string;
|
|
1319
|
+
}
|
|
1320
|
+
interface TopoloWidgetResponseOptions {
|
|
1321
|
+
serviceId: string;
|
|
1322
|
+
serviceName: string;
|
|
1323
|
+
widgets: TopoloWidgetDraft[];
|
|
1324
|
+
now?: Date | string;
|
|
1325
|
+
}
|
|
1326
|
+
interface TopoloWidgetValidationResult {
|
|
1327
|
+
ok: boolean;
|
|
1328
|
+
errors: string[];
|
|
1329
|
+
}
|
|
1330
|
+
interface TopoloWidgetConfig {
|
|
1331
|
+
serviceId: string;
|
|
1332
|
+
serviceName: string;
|
|
1333
|
+
baseUrl: string;
|
|
1334
|
+
widgetEndpoint: string;
|
|
1335
|
+
enabled: boolean;
|
|
1336
|
+
refreshInterval: number;
|
|
1337
|
+
}
|
|
1338
|
+
declare function defineTopoloStatsWidget(widget: Omit<TopoloStatsWidget, 'type'>): TopoloStatsWidget;
|
|
1339
|
+
declare function defineTopoloRecentActivityWidget(widget: Omit<TopoloRecentActivityWidget, 'type'>): TopoloRecentActivityWidget;
|
|
1340
|
+
declare function defineTopoloQuickActionsWidget(widget: Omit<TopoloQuickActionsWidget, 'type'>): TopoloQuickActionsWidget;
|
|
1341
|
+
declare function defineTopoloNotificationsWidget(widget: Omit<TopoloNotificationsWidget, 'type'>): TopoloNotificationsWidget;
|
|
1342
|
+
declare function defineTopoloChartWidget(widget: Omit<TopoloChartWidget, 'type'>): TopoloChartWidget;
|
|
1343
|
+
declare function createTopoloWidgetResponse({ serviceId, serviceName, widgets, now, }: TopoloWidgetResponseOptions): TopoloWidgetApiResponse;
|
|
1344
|
+
declare function createTopoloWidgetError(error: string): TopoloWidgetApiResponse;
|
|
1345
|
+
declare function validateTopoloWidgetResponse(response: unknown): TopoloWidgetValidationResult;
|
|
1346
|
+
|
|
1238
1347
|
/**
|
|
1239
1348
|
* Convenience factory that returns a ready-to-use typed Topolo client plus
|
|
1240
1349
|
* domain module surfaces (identity, crm, ...).
|
|
@@ -1249,4 +1358,4 @@ declare function createTopolo(options: TopoloClientOptions): {
|
|
|
1249
1358
|
};
|
|
1250
1359
|
type Topolo = ReturnType<typeof createTopolo>;
|
|
1251
1360
|
|
|
1252
|
-
export { APPLICATIONS, APPLICATION_REQUIREMENTS, APPLICATION_REQUIREMENTS_VERSION, type AgentIdentity, type ApplicationCatalogEntry, type ApplicationDeployTarget, type ApplicationId, type ApplicationRequirement, type ApplicationRequirementAudit, type ApplicationRequirementFinding, type ApplicationRequirementMigrationItem, type ApplicationRequirementScope, type ApplicationRequirementScore, type ApplicationRequirementStatus, type ApplicationRequirementsAuditReport, type CredentialIntrospection, type CrmContactSummary, CrmModule, DEFAULT_SERVICE_URLS, type DeviceAuthorizationResponse, IdentityModule, type ListContactsOptions, type ListContactsResult, type OAuthHelperOptions, PLATFORM_SERVICE_IDS, type RequestOptions, type ServiceId, type TokenResponse, type Topolo, TopoloAuthError, TopoloClient, type TopoloClientOptions, type TopoloCredential, type TopoloDebugEvent, TopoloHttpError, TopoloOAuth, TopoloPermissionError, TopoloSdkError, applicationRequirementScopes, auditAllApplicationRequirements, auditApplicationRequirements, createTopolo, requirementsForApplication, resolveServiceUrl };
|
|
1361
|
+
export { APPLICATIONS, APPLICATION_REQUIREMENTS, APPLICATION_REQUIREMENTS_VERSION, type AgentIdentity, type ApplicationCatalogEntry, type ApplicationDeployTarget, type ApplicationId, type ApplicationRequirement, type ApplicationRequirementAudit, type ApplicationRequirementFinding, type ApplicationRequirementMigrationItem, type ApplicationRequirementScope, type ApplicationRequirementScore, type ApplicationRequirementStatus, type ApplicationRequirementsAuditReport, type CredentialIntrospection, type CrmContactSummary, CrmModule, DEFAULT_SERVICE_URLS, type DeviceAuthorizationResponse, IdentityModule, type ListContactsOptions, type ListContactsResult, type OAuthHelperOptions, PLATFORM_SERVICE_IDS, type RequestOptions, type ServiceId, TOPOLO_WIDGET_ENDPOINT, TOPOLO_WIDGET_REFRESH_INTERVAL_MS, type TokenResponse, type Topolo, TopoloAuthError, type TopoloChartWidget, TopoloClient, type TopoloClientOptions, type TopoloCredential, type TopoloDebugEvent, TopoloHttpError, type TopoloNotificationsWidget, TopoloOAuth, TopoloPermissionError, type TopoloQuickActionsWidget, type TopoloRecentActivityWidget, TopoloSdkError, type TopoloStatsWidget, type TopoloWidget, type TopoloWidgetActionItem, type TopoloWidgetActivityItem, type TopoloWidgetApiResponse, type TopoloWidgetBase, type TopoloWidgetConfig, type TopoloWidgetDraft, type TopoloWidgetMetric, type TopoloWidgetNotificationItem, type TopoloWidgetResponseOptions, type TopoloWidgetType, type TopoloWidgetValidationResult, applicationRequirementScopes, auditAllApplicationRequirements, auditApplicationRequirements, createTopolo, createTopoloWidgetError, createTopoloWidgetResponse, defineTopoloChartWidget, defineTopoloNotificationsWidget, defineTopoloQuickActionsWidget, defineTopoloRecentActivityWidget, defineTopoloStatsWidget, requirementsForApplication, resolveServiceUrl, validateTopoloWidgetResponse };
|
package/dist/index.d.ts
CHANGED
|
@@ -847,7 +847,7 @@ type ApplicationId = keyof typeof APPLICATIONS;
|
|
|
847
847
|
type ApplicationCatalogEntry = (typeof APPLICATIONS)[ApplicationId];
|
|
848
848
|
type ApplicationDeployTarget = ApplicationCatalogEntry['deployTargets'][number];
|
|
849
849
|
|
|
850
|
-
declare const APPLICATION_REQUIREMENTS_VERSION = "2026-04-
|
|
850
|
+
declare const APPLICATION_REQUIREMENTS_VERSION = "2026-04-25.1";
|
|
851
851
|
type ApplicationRequirementScope = 'all' | 'browser' | 'api' | 'tooling' | 'agent_surface';
|
|
852
852
|
interface ApplicationRequirement {
|
|
853
853
|
id: string;
|
|
@@ -951,6 +951,13 @@ declare const APPLICATION_REQUIREMENTS: readonly [{
|
|
|
951
951
|
readonly appliesTo: readonly ["api", "tooling", "agent_surface"];
|
|
952
952
|
readonly evidence: readonly ["Read operations have typed SDK/CLI/MCP surfaces once their contract stabilizes", "Mutating generic calls require --confirm or confirm:true", "MCP tools never accept orgId and advertise only appropriate scope-gated tools"];
|
|
953
953
|
readonly implementation: readonly ["Add SDK types first, then CLI commands and MCP tools for durable workflows", "Use generic API passthrough only as a temporary escape hatch", "Keep command output JSON-stable for agents and human-readable with --no-json"];
|
|
954
|
+
}, {
|
|
955
|
+
readonly id: "native-dashboard-widget";
|
|
956
|
+
readonly title: "Expose a native dashboard widget contract";
|
|
957
|
+
readonly summary: "Launchable first-party applications must populate TopoloOne live workspace through an app-owned /api/widget endpoint using the SDK widget response contract.";
|
|
958
|
+
readonly appliesTo: readonly ["api"];
|
|
959
|
+
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"];
|
|
960
|
+
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"];
|
|
954
961
|
}, {
|
|
955
962
|
readonly id: "observability-and-audit";
|
|
956
963
|
readonly title: "Emit platform audit and debugging signals";
|
|
@@ -1235,6 +1242,108 @@ declare class TopoloOAuth {
|
|
|
1235
1242
|
}): Promise<TokenResponse>;
|
|
1236
1243
|
}
|
|
1237
1244
|
|
|
1245
|
+
declare const TOPOLO_WIDGET_ENDPOINT = "/api/widget";
|
|
1246
|
+
declare const TOPOLO_WIDGET_REFRESH_INTERVAL_MS = 60000;
|
|
1247
|
+
type TopoloWidgetType = 'stats' | 'recent-activity' | 'quick-actions' | 'notifications' | 'chart';
|
|
1248
|
+
interface TopoloWidgetBase {
|
|
1249
|
+
serviceId: string;
|
|
1250
|
+
serviceName: string;
|
|
1251
|
+
serviceIcon?: string;
|
|
1252
|
+
type: TopoloWidgetType;
|
|
1253
|
+
lastUpdated: string;
|
|
1254
|
+
}
|
|
1255
|
+
interface TopoloWidgetMetric {
|
|
1256
|
+
label: string;
|
|
1257
|
+
value: string | number;
|
|
1258
|
+
change?: number;
|
|
1259
|
+
changeLabel?: string;
|
|
1260
|
+
icon?: string;
|
|
1261
|
+
}
|
|
1262
|
+
interface TopoloStatsWidget extends TopoloWidgetBase {
|
|
1263
|
+
type: 'stats';
|
|
1264
|
+
stats: TopoloWidgetMetric[];
|
|
1265
|
+
}
|
|
1266
|
+
interface TopoloWidgetActivityItem {
|
|
1267
|
+
id: string;
|
|
1268
|
+
title: string;
|
|
1269
|
+
description?: string;
|
|
1270
|
+
timestamp: string;
|
|
1271
|
+
icon?: string;
|
|
1272
|
+
link?: string;
|
|
1273
|
+
}
|
|
1274
|
+
interface TopoloRecentActivityWidget extends TopoloWidgetBase {
|
|
1275
|
+
type: 'recent-activity';
|
|
1276
|
+
activities: TopoloWidgetActivityItem[];
|
|
1277
|
+
}
|
|
1278
|
+
interface TopoloWidgetActionItem {
|
|
1279
|
+
id: string;
|
|
1280
|
+
label: string;
|
|
1281
|
+
icon?: string;
|
|
1282
|
+
link: string;
|
|
1283
|
+
primary?: boolean;
|
|
1284
|
+
}
|
|
1285
|
+
interface TopoloQuickActionsWidget extends TopoloWidgetBase {
|
|
1286
|
+
type: 'quick-actions';
|
|
1287
|
+
actions: TopoloWidgetActionItem[];
|
|
1288
|
+
}
|
|
1289
|
+
interface TopoloWidgetNotificationItem {
|
|
1290
|
+
id: string;
|
|
1291
|
+
title: string;
|
|
1292
|
+
message: string;
|
|
1293
|
+
type: 'info' | 'warning' | 'error' | 'success';
|
|
1294
|
+
timestamp: string;
|
|
1295
|
+
read: boolean;
|
|
1296
|
+
link?: string;
|
|
1297
|
+
}
|
|
1298
|
+
interface TopoloNotificationsWidget extends TopoloWidgetBase {
|
|
1299
|
+
type: 'notifications';
|
|
1300
|
+
notifications: TopoloWidgetNotificationItem[];
|
|
1301
|
+
unreadCount: number;
|
|
1302
|
+
}
|
|
1303
|
+
interface TopoloChartWidget extends TopoloWidgetBase {
|
|
1304
|
+
type: 'chart';
|
|
1305
|
+
chartType: 'line' | 'bar' | 'pie' | 'donut';
|
|
1306
|
+
title: string;
|
|
1307
|
+
data: {
|
|
1308
|
+
labels: string[];
|
|
1309
|
+
values: number[];
|
|
1310
|
+
};
|
|
1311
|
+
}
|
|
1312
|
+
type TopoloWidget = TopoloStatsWidget | TopoloRecentActivityWidget | TopoloQuickActionsWidget | TopoloNotificationsWidget | TopoloChartWidget;
|
|
1313
|
+
type TopoloWidgetWithOptionalMetadata<T extends TopoloWidget> = Omit<T, 'serviceId' | 'serviceName' | 'lastUpdated'> & Partial<Pick<TopoloWidgetBase, 'serviceId' | 'serviceName' | 'lastUpdated'>>;
|
|
1314
|
+
type TopoloWidgetDraft = TopoloWidget extends infer Widget ? Widget extends TopoloWidget ? TopoloWidgetWithOptionalMetadata<Widget> : never : never;
|
|
1315
|
+
interface TopoloWidgetApiResponse {
|
|
1316
|
+
success: boolean;
|
|
1317
|
+
widgets: TopoloWidget[];
|
|
1318
|
+
error?: string;
|
|
1319
|
+
}
|
|
1320
|
+
interface TopoloWidgetResponseOptions {
|
|
1321
|
+
serviceId: string;
|
|
1322
|
+
serviceName: string;
|
|
1323
|
+
widgets: TopoloWidgetDraft[];
|
|
1324
|
+
now?: Date | string;
|
|
1325
|
+
}
|
|
1326
|
+
interface TopoloWidgetValidationResult {
|
|
1327
|
+
ok: boolean;
|
|
1328
|
+
errors: string[];
|
|
1329
|
+
}
|
|
1330
|
+
interface TopoloWidgetConfig {
|
|
1331
|
+
serviceId: string;
|
|
1332
|
+
serviceName: string;
|
|
1333
|
+
baseUrl: string;
|
|
1334
|
+
widgetEndpoint: string;
|
|
1335
|
+
enabled: boolean;
|
|
1336
|
+
refreshInterval: number;
|
|
1337
|
+
}
|
|
1338
|
+
declare function defineTopoloStatsWidget(widget: Omit<TopoloStatsWidget, 'type'>): TopoloStatsWidget;
|
|
1339
|
+
declare function defineTopoloRecentActivityWidget(widget: Omit<TopoloRecentActivityWidget, 'type'>): TopoloRecentActivityWidget;
|
|
1340
|
+
declare function defineTopoloQuickActionsWidget(widget: Omit<TopoloQuickActionsWidget, 'type'>): TopoloQuickActionsWidget;
|
|
1341
|
+
declare function defineTopoloNotificationsWidget(widget: Omit<TopoloNotificationsWidget, 'type'>): TopoloNotificationsWidget;
|
|
1342
|
+
declare function defineTopoloChartWidget(widget: Omit<TopoloChartWidget, 'type'>): TopoloChartWidget;
|
|
1343
|
+
declare function createTopoloWidgetResponse({ serviceId, serviceName, widgets, now, }: TopoloWidgetResponseOptions): TopoloWidgetApiResponse;
|
|
1344
|
+
declare function createTopoloWidgetError(error: string): TopoloWidgetApiResponse;
|
|
1345
|
+
declare function validateTopoloWidgetResponse(response: unknown): TopoloWidgetValidationResult;
|
|
1346
|
+
|
|
1238
1347
|
/**
|
|
1239
1348
|
* Convenience factory that returns a ready-to-use typed Topolo client plus
|
|
1240
1349
|
* domain module surfaces (identity, crm, ...).
|
|
@@ -1249,4 +1358,4 @@ declare function createTopolo(options: TopoloClientOptions): {
|
|
|
1249
1358
|
};
|
|
1250
1359
|
type Topolo = ReturnType<typeof createTopolo>;
|
|
1251
1360
|
|
|
1252
|
-
export { APPLICATIONS, APPLICATION_REQUIREMENTS, APPLICATION_REQUIREMENTS_VERSION, type AgentIdentity, type ApplicationCatalogEntry, type ApplicationDeployTarget, type ApplicationId, type ApplicationRequirement, type ApplicationRequirementAudit, type ApplicationRequirementFinding, type ApplicationRequirementMigrationItem, type ApplicationRequirementScope, type ApplicationRequirementScore, type ApplicationRequirementStatus, type ApplicationRequirementsAuditReport, type CredentialIntrospection, type CrmContactSummary, CrmModule, DEFAULT_SERVICE_URLS, type DeviceAuthorizationResponse, IdentityModule, type ListContactsOptions, type ListContactsResult, type OAuthHelperOptions, PLATFORM_SERVICE_IDS, type RequestOptions, type ServiceId, type TokenResponse, type Topolo, TopoloAuthError, TopoloClient, type TopoloClientOptions, type TopoloCredential, type TopoloDebugEvent, TopoloHttpError, TopoloOAuth, TopoloPermissionError, TopoloSdkError, applicationRequirementScopes, auditAllApplicationRequirements, auditApplicationRequirements, createTopolo, requirementsForApplication, resolveServiceUrl };
|
|
1361
|
+
export { APPLICATIONS, APPLICATION_REQUIREMENTS, APPLICATION_REQUIREMENTS_VERSION, type AgentIdentity, type ApplicationCatalogEntry, type ApplicationDeployTarget, type ApplicationId, type ApplicationRequirement, type ApplicationRequirementAudit, type ApplicationRequirementFinding, type ApplicationRequirementMigrationItem, type ApplicationRequirementScope, type ApplicationRequirementScore, type ApplicationRequirementStatus, type ApplicationRequirementsAuditReport, type CredentialIntrospection, type CrmContactSummary, CrmModule, DEFAULT_SERVICE_URLS, type DeviceAuthorizationResponse, IdentityModule, type ListContactsOptions, type ListContactsResult, type OAuthHelperOptions, PLATFORM_SERVICE_IDS, type RequestOptions, type ServiceId, TOPOLO_WIDGET_ENDPOINT, TOPOLO_WIDGET_REFRESH_INTERVAL_MS, type TokenResponse, type Topolo, TopoloAuthError, type TopoloChartWidget, TopoloClient, type TopoloClientOptions, type TopoloCredential, type TopoloDebugEvent, TopoloHttpError, type TopoloNotificationsWidget, TopoloOAuth, TopoloPermissionError, type TopoloQuickActionsWidget, type TopoloRecentActivityWidget, TopoloSdkError, type TopoloStatsWidget, type TopoloWidget, type TopoloWidgetActionItem, type TopoloWidgetActivityItem, type TopoloWidgetApiResponse, type TopoloWidgetBase, type TopoloWidgetConfig, type TopoloWidgetDraft, type TopoloWidgetMetric, type TopoloWidgetNotificationItem, type TopoloWidgetResponseOptions, type TopoloWidgetType, type TopoloWidgetValidationResult, applicationRequirementScopes, auditAllApplicationRequirements, auditApplicationRequirements, createTopolo, createTopoloWidgetError, createTopoloWidgetResponse, defineTopoloChartWidget, defineTopoloNotificationsWidget, defineTopoloQuickActionsWidget, defineTopoloRecentActivityWidget, defineTopoloStatsWidget, requirementsForApplication, resolveServiceUrl, validateTopoloWidgetResponse };
|
package/dist/index.js
CHANGED
|
@@ -1175,7 +1175,7 @@ var APPLICATIONS = {
|
|
|
1175
1175
|
};
|
|
1176
1176
|
|
|
1177
1177
|
// src/application-requirements.ts
|
|
1178
|
-
var APPLICATION_REQUIREMENTS_VERSION = "2026-04-
|
|
1178
|
+
var APPLICATION_REQUIREMENTS_VERSION = "2026-04-25.1";
|
|
1179
1179
|
var APPLICATION_REQUIREMENTS = [
|
|
1180
1180
|
{
|
|
1181
1181
|
id: "platform-metadata",
|
|
@@ -1307,6 +1307,22 @@ var APPLICATION_REQUIREMENTS = [
|
|
|
1307
1307
|
"Keep command output JSON-stable for agents and human-readable with --no-json"
|
|
1308
1308
|
]
|
|
1309
1309
|
},
|
|
1310
|
+
{
|
|
1311
|
+
id: "native-dashboard-widget",
|
|
1312
|
+
title: "Expose a native dashboard widget contract",
|
|
1313
|
+
summary: "Launchable first-party applications must populate TopoloOne live workspace through an app-owned /api/widget endpoint using the SDK widget response contract.",
|
|
1314
|
+
appliesTo: ["api"],
|
|
1315
|
+
evidence: [
|
|
1316
|
+
"The app-owned API exposes GET /api/widget",
|
|
1317
|
+
"The endpoint returns TopoloWidgetApiResponse from @topolo/sdk",
|
|
1318
|
+
"The endpoint derives organization/user context from the app auth boundary"
|
|
1319
|
+
],
|
|
1320
|
+
implementation: [
|
|
1321
|
+
"Import createTopoloWidgetResponse from @topolo/sdk in the app backend",
|
|
1322
|
+
"Return native app metrics or action widgets instead of generic launch-card fallbacks",
|
|
1323
|
+
"Add a route-level test that validates the response with validateTopoloWidgetResponse"
|
|
1324
|
+
]
|
|
1325
|
+
},
|
|
1310
1326
|
{
|
|
1311
1327
|
id: "observability-and-audit",
|
|
1312
1328
|
title: "Emit platform audit and debugging signals",
|
|
@@ -1478,6 +1494,13 @@ function evaluateRequirement(application, requirement) {
|
|
|
1478
1494
|
}
|
|
1479
1495
|
return finding(requirement, "missing", "No SDK service or tooling surface is registered.", evidence);
|
|
1480
1496
|
}
|
|
1497
|
+
case "native-dashboard-widget":
|
|
1498
|
+
return finding(
|
|
1499
|
+
requirement,
|
|
1500
|
+
"needs_review",
|
|
1501
|
+
"Catalog metadata cannot verify app-owned /api/widget implementation details.",
|
|
1502
|
+
evidence
|
|
1503
|
+
);
|
|
1481
1504
|
case "observability-and-audit":
|
|
1482
1505
|
return finding(
|
|
1483
1506
|
requirement,
|
|
@@ -1876,6 +1899,126 @@ var TopoloOAuth = class {
|
|
|
1876
1899
|
}
|
|
1877
1900
|
};
|
|
1878
1901
|
|
|
1902
|
+
// src/widgets.ts
|
|
1903
|
+
var TOPOLO_WIDGET_ENDPOINT = "/api/widget";
|
|
1904
|
+
var TOPOLO_WIDGET_REFRESH_INTERVAL_MS = 6e4;
|
|
1905
|
+
function defineTopoloStatsWidget(widget) {
|
|
1906
|
+
return { ...widget, type: "stats" };
|
|
1907
|
+
}
|
|
1908
|
+
function defineTopoloRecentActivityWidget(widget) {
|
|
1909
|
+
return { ...widget, type: "recent-activity" };
|
|
1910
|
+
}
|
|
1911
|
+
function defineTopoloQuickActionsWidget(widget) {
|
|
1912
|
+
return { ...widget, type: "quick-actions" };
|
|
1913
|
+
}
|
|
1914
|
+
function defineTopoloNotificationsWidget(widget) {
|
|
1915
|
+
return { ...widget, type: "notifications" };
|
|
1916
|
+
}
|
|
1917
|
+
function defineTopoloChartWidget(widget) {
|
|
1918
|
+
return { ...widget, type: "chart" };
|
|
1919
|
+
}
|
|
1920
|
+
function createTopoloWidgetResponse({
|
|
1921
|
+
serviceId,
|
|
1922
|
+
serviceName,
|
|
1923
|
+
widgets,
|
|
1924
|
+
now
|
|
1925
|
+
}) {
|
|
1926
|
+
const lastUpdated = toIsoTimestamp(now);
|
|
1927
|
+
return {
|
|
1928
|
+
success: true,
|
|
1929
|
+
widgets: widgets.map((widget) => ({
|
|
1930
|
+
...widget,
|
|
1931
|
+
serviceId: widget.serviceId?.trim() || serviceId,
|
|
1932
|
+
serviceName: widget.serviceName?.trim() || serviceName,
|
|
1933
|
+
lastUpdated: widget.lastUpdated?.trim() || lastUpdated
|
|
1934
|
+
}))
|
|
1935
|
+
};
|
|
1936
|
+
}
|
|
1937
|
+
function createTopoloWidgetError(error) {
|
|
1938
|
+
return {
|
|
1939
|
+
success: false,
|
|
1940
|
+
widgets: [],
|
|
1941
|
+
error
|
|
1942
|
+
};
|
|
1943
|
+
}
|
|
1944
|
+
function validateTopoloWidgetResponse(response) {
|
|
1945
|
+
const errors = [];
|
|
1946
|
+
if (!isRecord(response)) {
|
|
1947
|
+
return { ok: false, errors: ["response must be an object"] };
|
|
1948
|
+
}
|
|
1949
|
+
if (typeof response.success !== "boolean") {
|
|
1950
|
+
errors.push("success must be a boolean");
|
|
1951
|
+
}
|
|
1952
|
+
if (!Array.isArray(response.widgets)) {
|
|
1953
|
+
errors.push("widgets must be an array");
|
|
1954
|
+
return { ok: errors.length === 0, errors };
|
|
1955
|
+
}
|
|
1956
|
+
response.widgets.forEach((widget, index) => {
|
|
1957
|
+
validateWidget(widget, `widgets[${index}]`, errors);
|
|
1958
|
+
});
|
|
1959
|
+
if (response.success === false && response.error !== void 0 && typeof response.error !== "string") {
|
|
1960
|
+
errors.push("error must be a string when provided");
|
|
1961
|
+
}
|
|
1962
|
+
return { ok: errors.length === 0, errors };
|
|
1963
|
+
}
|
|
1964
|
+
function validateWidget(widget, path, errors) {
|
|
1965
|
+
if (!isRecord(widget)) {
|
|
1966
|
+
errors.push(`${path} must be an object`);
|
|
1967
|
+
return;
|
|
1968
|
+
}
|
|
1969
|
+
validateNonEmptyString(widget.serviceId, `${path}.serviceId`, errors);
|
|
1970
|
+
validateNonEmptyString(widget.serviceName, `${path}.serviceName`, errors);
|
|
1971
|
+
validateNonEmptyString(widget.lastUpdated, `${path}.lastUpdated`, errors);
|
|
1972
|
+
switch (widget.type) {
|
|
1973
|
+
case "stats":
|
|
1974
|
+
validateArray(widget.stats, `${path}.stats`, errors);
|
|
1975
|
+
break;
|
|
1976
|
+
case "recent-activity":
|
|
1977
|
+
validateArray(widget.activities, `${path}.activities`, errors);
|
|
1978
|
+
break;
|
|
1979
|
+
case "quick-actions":
|
|
1980
|
+
validateArray(widget.actions, `${path}.actions`, errors);
|
|
1981
|
+
break;
|
|
1982
|
+
case "notifications":
|
|
1983
|
+
validateArray(widget.notifications, `${path}.notifications`, errors);
|
|
1984
|
+
if (typeof widget.unreadCount !== "number") {
|
|
1985
|
+
errors.push(`${path}.unreadCount must be a number`);
|
|
1986
|
+
}
|
|
1987
|
+
break;
|
|
1988
|
+
case "chart":
|
|
1989
|
+
validateNonEmptyString(widget.chartType, `${path}.chartType`, errors);
|
|
1990
|
+
validateNonEmptyString(widget.title, `${path}.title`, errors);
|
|
1991
|
+
if (!isRecord(widget.data)) {
|
|
1992
|
+
errors.push(`${path}.data must be an object`);
|
|
1993
|
+
} else {
|
|
1994
|
+
validateArray(widget.data.labels, `${path}.data.labels`, errors);
|
|
1995
|
+
validateArray(widget.data.values, `${path}.data.values`, errors);
|
|
1996
|
+
}
|
|
1997
|
+
break;
|
|
1998
|
+
default:
|
|
1999
|
+
errors.push(`${path}.type must be a supported Topolo widget type`);
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
function validateNonEmptyString(value, path, errors) {
|
|
2003
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
2004
|
+
errors.push(`${path} must be a non-empty string`);
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
function validateArray(value, path, errors) {
|
|
2008
|
+
if (!Array.isArray(value)) {
|
|
2009
|
+
errors.push(`${path} must be an array`);
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
function isRecord(value) {
|
|
2013
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2014
|
+
}
|
|
2015
|
+
function toIsoTimestamp(input) {
|
|
2016
|
+
if (input instanceof Date) {
|
|
2017
|
+
return input.toISOString();
|
|
2018
|
+
}
|
|
2019
|
+
return input ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
2020
|
+
}
|
|
2021
|
+
|
|
1879
2022
|
// src/index.ts
|
|
1880
2023
|
function createTopolo(options) {
|
|
1881
2024
|
const client = new TopoloClient(options);
|
|
@@ -1893,6 +2036,8 @@ export {
|
|
|
1893
2036
|
DEFAULT_SERVICE_URLS,
|
|
1894
2037
|
IdentityModule,
|
|
1895
2038
|
PLATFORM_SERVICE_IDS,
|
|
2039
|
+
TOPOLO_WIDGET_ENDPOINT,
|
|
2040
|
+
TOPOLO_WIDGET_REFRESH_INTERVAL_MS,
|
|
1896
2041
|
TopoloAuthError,
|
|
1897
2042
|
TopoloClient,
|
|
1898
2043
|
TopoloHttpError,
|
|
@@ -1903,6 +2048,14 @@ export {
|
|
|
1903
2048
|
auditAllApplicationRequirements,
|
|
1904
2049
|
auditApplicationRequirements,
|
|
1905
2050
|
createTopolo,
|
|
2051
|
+
createTopoloWidgetError,
|
|
2052
|
+
createTopoloWidgetResponse,
|
|
2053
|
+
defineTopoloChartWidget,
|
|
2054
|
+
defineTopoloNotificationsWidget,
|
|
2055
|
+
defineTopoloQuickActionsWidget,
|
|
2056
|
+
defineTopoloRecentActivityWidget,
|
|
2057
|
+
defineTopoloStatsWidget,
|
|
1906
2058
|
requirementsForApplication,
|
|
1907
|
-
resolveServiceUrl
|
|
2059
|
+
resolveServiceUrl,
|
|
2060
|
+
validateTopoloWidgetResponse
|
|
1908
2061
|
};
|
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-04-
|
|
7
|
+
export const APPLICATION_REQUIREMENTS_VERSION = '2026-04-25.1';
|
|
8
8
|
|
|
9
9
|
export type ApplicationRequirementScope =
|
|
10
10
|
| 'all'
|
|
@@ -205,6 +205,23 @@ export const APPLICATION_REQUIREMENTS = [
|
|
|
205
205
|
'Keep command output JSON-stable for agents and human-readable with --no-json',
|
|
206
206
|
],
|
|
207
207
|
},
|
|
208
|
+
{
|
|
209
|
+
id: 'native-dashboard-widget',
|
|
210
|
+
title: 'Expose a native dashboard widget contract',
|
|
211
|
+
summary:
|
|
212
|
+
'Launchable first-party applications must populate TopoloOne live workspace through an app-owned /api/widget endpoint using the SDK widget response contract.',
|
|
213
|
+
appliesTo: ['api'],
|
|
214
|
+
evidence: [
|
|
215
|
+
'The app-owned API exposes GET /api/widget',
|
|
216
|
+
'The endpoint returns TopoloWidgetApiResponse from @topolo/sdk',
|
|
217
|
+
'The endpoint derives organization/user context from the app auth boundary',
|
|
218
|
+
],
|
|
219
|
+
implementation: [
|
|
220
|
+
'Import createTopoloWidgetResponse from @topolo/sdk in the app backend',
|
|
221
|
+
'Return native app metrics or action widgets instead of generic launch-card fallbacks',
|
|
222
|
+
'Add a route-level test that validates the response with validateTopoloWidgetResponse',
|
|
223
|
+
],
|
|
224
|
+
},
|
|
208
225
|
{
|
|
209
226
|
id: 'observability-and-audit',
|
|
210
227
|
title: 'Emit platform audit and debugging signals',
|
|
@@ -411,6 +428,13 @@ function evaluateRequirement(
|
|
|
411
428
|
}
|
|
412
429
|
return finding(requirement, 'missing', 'No SDK service or tooling surface is registered.', evidence);
|
|
413
430
|
}
|
|
431
|
+
case 'native-dashboard-widget':
|
|
432
|
+
return finding(
|
|
433
|
+
requirement,
|
|
434
|
+
'needs_review',
|
|
435
|
+
'Catalog metadata cannot verify app-owned /api/widget implementation details.',
|
|
436
|
+
evidence,
|
|
437
|
+
);
|
|
414
438
|
case 'observability-and-audit':
|
|
415
439
|
return finding(
|
|
416
440
|
requirement,
|
package/src/index.ts
CHANGED
|
@@ -44,6 +44,35 @@ export {
|
|
|
44
44
|
type DeviceAuthorizationResponse,
|
|
45
45
|
type TokenResponse,
|
|
46
46
|
} from './oauth.js';
|
|
47
|
+
export {
|
|
48
|
+
TOPOLO_WIDGET_ENDPOINT,
|
|
49
|
+
TOPOLO_WIDGET_REFRESH_INTERVAL_MS,
|
|
50
|
+
createTopoloWidgetError,
|
|
51
|
+
createTopoloWidgetResponse,
|
|
52
|
+
defineTopoloChartWidget,
|
|
53
|
+
defineTopoloNotificationsWidget,
|
|
54
|
+
defineTopoloQuickActionsWidget,
|
|
55
|
+
defineTopoloRecentActivityWidget,
|
|
56
|
+
defineTopoloStatsWidget,
|
|
57
|
+
validateTopoloWidgetResponse,
|
|
58
|
+
type TopoloChartWidget,
|
|
59
|
+
type TopoloNotificationsWidget,
|
|
60
|
+
type TopoloQuickActionsWidget,
|
|
61
|
+
type TopoloRecentActivityWidget,
|
|
62
|
+
type TopoloStatsWidget,
|
|
63
|
+
type TopoloWidget,
|
|
64
|
+
type TopoloWidgetActionItem,
|
|
65
|
+
type TopoloWidgetActivityItem,
|
|
66
|
+
type TopoloWidgetApiResponse,
|
|
67
|
+
type TopoloWidgetBase,
|
|
68
|
+
type TopoloWidgetConfig,
|
|
69
|
+
type TopoloWidgetDraft,
|
|
70
|
+
type TopoloWidgetMetric,
|
|
71
|
+
type TopoloWidgetNotificationItem,
|
|
72
|
+
type TopoloWidgetResponseOptions,
|
|
73
|
+
type TopoloWidgetType,
|
|
74
|
+
type TopoloWidgetValidationResult,
|
|
75
|
+
} from './widgets.js';
|
|
47
76
|
|
|
48
77
|
import { TopoloClient, type TopoloClientOptions } from './client.js';
|
|
49
78
|
import { CrmModule } from './modules/crm.js';
|
package/src/services.test.ts
CHANGED
|
@@ -135,6 +135,9 @@ describe('application requirements', () => {
|
|
|
135
135
|
expect(APPLICATION_REQUIREMENTS.map((requirement) => requirement.id)).toContain(
|
|
136
136
|
'shared-auth-boundary',
|
|
137
137
|
);
|
|
138
|
+
expect(APPLICATION_REQUIREMENTS.map((requirement) => requirement.id)).toContain(
|
|
139
|
+
'native-dashboard-widget',
|
|
140
|
+
);
|
|
138
141
|
});
|
|
139
142
|
|
|
140
143
|
it('derives browser and API scopes from app metadata', () => {
|
|
@@ -158,6 +161,7 @@ describe('application requirements', () => {
|
|
|
158
161
|
expect(adminRequirementIds).not.toContain('organization-scoped-data');
|
|
159
162
|
expect(mailRequirementIds).toContain('organization-scoped-data');
|
|
160
163
|
expect(mailRequirementIds).toContain('service-registration-and-scopes');
|
|
164
|
+
expect(mailRequirementIds).toContain('native-dashboard-widget');
|
|
161
165
|
expect(cliRequirementIds).toContain('agent-and-operator-surfaces');
|
|
162
166
|
expect(cliRequirementIds).not.toContain('service-registration-and-scopes');
|
|
163
167
|
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
createTopoloWidgetError,
|
|
4
|
+
createTopoloWidgetResponse,
|
|
5
|
+
TOPOLO_WIDGET_ENDPOINT,
|
|
6
|
+
TOPOLO_WIDGET_REFRESH_INTERVAL_MS,
|
|
7
|
+
validateTopoloWidgetResponse,
|
|
8
|
+
} from './widgets.js';
|
|
9
|
+
|
|
10
|
+
describe('Topolo widget contract', () => {
|
|
11
|
+
it('exports the platform widget endpoint defaults', () => {
|
|
12
|
+
expect(TOPOLO_WIDGET_ENDPOINT).toBe('/api/widget');
|
|
13
|
+
expect(TOPOLO_WIDGET_REFRESH_INTERVAL_MS).toBe(60_000);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('stamps service metadata and timestamp onto widget drafts', () => {
|
|
17
|
+
const response = createTopoloWidgetResponse({
|
|
18
|
+
serviceId: 'srv_bytes',
|
|
19
|
+
serviceName: 'Bytes',
|
|
20
|
+
now: '2026-04-25T00:00:00.000Z',
|
|
21
|
+
widgets: [
|
|
22
|
+
{
|
|
23
|
+
type: 'stats',
|
|
24
|
+
stats: [{ label: 'Files', value: 12 }],
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
expect(response).toEqual({
|
|
30
|
+
success: true,
|
|
31
|
+
widgets: [
|
|
32
|
+
{
|
|
33
|
+
serviceId: 'srv_bytes',
|
|
34
|
+
serviceName: 'Bytes',
|
|
35
|
+
lastUpdated: '2026-04-25T00:00:00.000Z',
|
|
36
|
+
type: 'stats',
|
|
37
|
+
stats: [{ label: 'Files', value: 12 }],
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('keeps explicit widget metadata when an app needs multiple service cards', () => {
|
|
44
|
+
const response = createTopoloWidgetResponse({
|
|
45
|
+
serviceId: 'srv_platform',
|
|
46
|
+
serviceName: 'Platform',
|
|
47
|
+
now: '2026-04-25T00:00:00.000Z',
|
|
48
|
+
widgets: [
|
|
49
|
+
{
|
|
50
|
+
serviceId: 'srv_crm',
|
|
51
|
+
serviceName: 'CRM',
|
|
52
|
+
lastUpdated: '2026-04-24T23:00:00.000Z',
|
|
53
|
+
type: 'stats',
|
|
54
|
+
stats: [{ label: 'Contacts', value: 3 }],
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
expect(response.widgets[0]).toMatchObject({
|
|
60
|
+
serviceId: 'srv_crm',
|
|
61
|
+
serviceName: 'CRM',
|
|
62
|
+
lastUpdated: '2026-04-24T23:00:00.000Z',
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('returns a stable error response shape', () => {
|
|
67
|
+
expect(createTopoloWidgetError('not configured')).toEqual({
|
|
68
|
+
success: false,
|
|
69
|
+
widgets: [],
|
|
70
|
+
error: 'not configured',
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('validates a compliant response', () => {
|
|
75
|
+
const response = createTopoloWidgetResponse({
|
|
76
|
+
serviceId: 'srv_crm',
|
|
77
|
+
serviceName: 'CRM',
|
|
78
|
+
now: '2026-04-25T00:00:00.000Z',
|
|
79
|
+
widgets: [
|
|
80
|
+
{
|
|
81
|
+
type: 'notifications',
|
|
82
|
+
unreadCount: 1,
|
|
83
|
+
notifications: [
|
|
84
|
+
{
|
|
85
|
+
id: 'n_1',
|
|
86
|
+
title: 'New lead',
|
|
87
|
+
message: 'A lead was created.',
|
|
88
|
+
type: 'info',
|
|
89
|
+
timestamp: '2026-04-25T00:00:00.000Z',
|
|
90
|
+
read: false,
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
expect(validateTopoloWidgetResponse(response)).toEqual({
|
|
98
|
+
ok: true,
|
|
99
|
+
errors: [],
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('reports missing metadata and unsupported widget types', () => {
|
|
104
|
+
const result = validateTopoloWidgetResponse({
|
|
105
|
+
success: true,
|
|
106
|
+
widgets: [
|
|
107
|
+
{
|
|
108
|
+
serviceId: '',
|
|
109
|
+
serviceName: 'Bad App',
|
|
110
|
+
lastUpdated: '',
|
|
111
|
+
type: 'calendar',
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
expect(result.ok).toBe(false);
|
|
117
|
+
expect(result.errors).toEqual([
|
|
118
|
+
'widgets[0].serviceId must be a non-empty string',
|
|
119
|
+
'widgets[0].lastUpdated must be a non-empty string',
|
|
120
|
+
'widgets[0].type must be a supported Topolo widget type',
|
|
121
|
+
]);
|
|
122
|
+
});
|
|
123
|
+
});
|
package/src/widgets.ts
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
export const TOPOLO_WIDGET_ENDPOINT = '/api/widget';
|
|
2
|
+
export const TOPOLO_WIDGET_REFRESH_INTERVAL_MS = 60_000;
|
|
3
|
+
|
|
4
|
+
export type TopoloWidgetType =
|
|
5
|
+
| 'stats'
|
|
6
|
+
| 'recent-activity'
|
|
7
|
+
| 'quick-actions'
|
|
8
|
+
| 'notifications'
|
|
9
|
+
| 'chart';
|
|
10
|
+
|
|
11
|
+
export interface TopoloWidgetBase {
|
|
12
|
+
serviceId: string;
|
|
13
|
+
serviceName: string;
|
|
14
|
+
serviceIcon?: string;
|
|
15
|
+
type: TopoloWidgetType;
|
|
16
|
+
lastUpdated: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface TopoloWidgetMetric {
|
|
20
|
+
label: string;
|
|
21
|
+
value: string | number;
|
|
22
|
+
change?: number;
|
|
23
|
+
changeLabel?: string;
|
|
24
|
+
icon?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface TopoloStatsWidget extends TopoloWidgetBase {
|
|
28
|
+
type: 'stats';
|
|
29
|
+
stats: TopoloWidgetMetric[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface TopoloWidgetActivityItem {
|
|
33
|
+
id: string;
|
|
34
|
+
title: string;
|
|
35
|
+
description?: string;
|
|
36
|
+
timestamp: string;
|
|
37
|
+
icon?: string;
|
|
38
|
+
link?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface TopoloRecentActivityWidget extends TopoloWidgetBase {
|
|
42
|
+
type: 'recent-activity';
|
|
43
|
+
activities: TopoloWidgetActivityItem[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface TopoloWidgetActionItem {
|
|
47
|
+
id: string;
|
|
48
|
+
label: string;
|
|
49
|
+
icon?: string;
|
|
50
|
+
link: string;
|
|
51
|
+
primary?: boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface TopoloQuickActionsWidget extends TopoloWidgetBase {
|
|
55
|
+
type: 'quick-actions';
|
|
56
|
+
actions: TopoloWidgetActionItem[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface TopoloWidgetNotificationItem {
|
|
60
|
+
id: string;
|
|
61
|
+
title: string;
|
|
62
|
+
message: string;
|
|
63
|
+
type: 'info' | 'warning' | 'error' | 'success';
|
|
64
|
+
timestamp: string;
|
|
65
|
+
read: boolean;
|
|
66
|
+
link?: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface TopoloNotificationsWidget extends TopoloWidgetBase {
|
|
70
|
+
type: 'notifications';
|
|
71
|
+
notifications: TopoloWidgetNotificationItem[];
|
|
72
|
+
unreadCount: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface TopoloChartWidget extends TopoloWidgetBase {
|
|
76
|
+
type: 'chart';
|
|
77
|
+
chartType: 'line' | 'bar' | 'pie' | 'donut';
|
|
78
|
+
title: string;
|
|
79
|
+
data: {
|
|
80
|
+
labels: string[];
|
|
81
|
+
values: number[];
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export type TopoloWidget =
|
|
86
|
+
| TopoloStatsWidget
|
|
87
|
+
| TopoloRecentActivityWidget
|
|
88
|
+
| TopoloQuickActionsWidget
|
|
89
|
+
| TopoloNotificationsWidget
|
|
90
|
+
| TopoloChartWidget;
|
|
91
|
+
|
|
92
|
+
type TopoloWidgetWithOptionalMetadata<T extends TopoloWidget> = Omit<
|
|
93
|
+
T,
|
|
94
|
+
'serviceId' | 'serviceName' | 'lastUpdated'
|
|
95
|
+
> &
|
|
96
|
+
Partial<Pick<TopoloWidgetBase, 'serviceId' | 'serviceName' | 'lastUpdated'>>;
|
|
97
|
+
|
|
98
|
+
export type TopoloWidgetDraft = TopoloWidget extends infer Widget
|
|
99
|
+
? Widget extends TopoloWidget
|
|
100
|
+
? TopoloWidgetWithOptionalMetadata<Widget>
|
|
101
|
+
: never
|
|
102
|
+
: never;
|
|
103
|
+
|
|
104
|
+
export interface TopoloWidgetApiResponse {
|
|
105
|
+
success: boolean;
|
|
106
|
+
widgets: TopoloWidget[];
|
|
107
|
+
error?: string;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface TopoloWidgetResponseOptions {
|
|
111
|
+
serviceId: string;
|
|
112
|
+
serviceName: string;
|
|
113
|
+
widgets: TopoloWidgetDraft[];
|
|
114
|
+
now?: Date | string;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface TopoloWidgetValidationResult {
|
|
118
|
+
ok: boolean;
|
|
119
|
+
errors: string[];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface TopoloWidgetConfig {
|
|
123
|
+
serviceId: string;
|
|
124
|
+
serviceName: string;
|
|
125
|
+
baseUrl: string;
|
|
126
|
+
widgetEndpoint: string;
|
|
127
|
+
enabled: boolean;
|
|
128
|
+
refreshInterval: number;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function defineTopoloStatsWidget(
|
|
132
|
+
widget: Omit<TopoloStatsWidget, 'type'>
|
|
133
|
+
): TopoloStatsWidget {
|
|
134
|
+
return { ...widget, type: 'stats' };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function defineTopoloRecentActivityWidget(
|
|
138
|
+
widget: Omit<TopoloRecentActivityWidget, 'type'>
|
|
139
|
+
): TopoloRecentActivityWidget {
|
|
140
|
+
return { ...widget, type: 'recent-activity' };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function defineTopoloQuickActionsWidget(
|
|
144
|
+
widget: Omit<TopoloQuickActionsWidget, 'type'>
|
|
145
|
+
): TopoloQuickActionsWidget {
|
|
146
|
+
return { ...widget, type: 'quick-actions' };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function defineTopoloNotificationsWidget(
|
|
150
|
+
widget: Omit<TopoloNotificationsWidget, 'type'>
|
|
151
|
+
): TopoloNotificationsWidget {
|
|
152
|
+
return { ...widget, type: 'notifications' };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function defineTopoloChartWidget(
|
|
156
|
+
widget: Omit<TopoloChartWidget, 'type'>
|
|
157
|
+
): TopoloChartWidget {
|
|
158
|
+
return { ...widget, type: 'chart' };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function createTopoloWidgetResponse({
|
|
162
|
+
serviceId,
|
|
163
|
+
serviceName,
|
|
164
|
+
widgets,
|
|
165
|
+
now,
|
|
166
|
+
}: TopoloWidgetResponseOptions): TopoloWidgetApiResponse {
|
|
167
|
+
const lastUpdated = toIsoTimestamp(now);
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
success: true,
|
|
171
|
+
widgets: widgets.map(widget => ({
|
|
172
|
+
...widget,
|
|
173
|
+
serviceId: widget.serviceId?.trim() || serviceId,
|
|
174
|
+
serviceName: widget.serviceName?.trim() || serviceName,
|
|
175
|
+
lastUpdated: widget.lastUpdated?.trim() || lastUpdated,
|
|
176
|
+
})) as TopoloWidget[],
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function createTopoloWidgetError(error: string): TopoloWidgetApiResponse {
|
|
181
|
+
return {
|
|
182
|
+
success: false,
|
|
183
|
+
widgets: [],
|
|
184
|
+
error,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function validateTopoloWidgetResponse(
|
|
189
|
+
response: unknown
|
|
190
|
+
): TopoloWidgetValidationResult {
|
|
191
|
+
const errors: string[] = [];
|
|
192
|
+
|
|
193
|
+
if (!isRecord(response)) {
|
|
194
|
+
return { ok: false, errors: ['response must be an object'] };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (typeof response.success !== 'boolean') {
|
|
198
|
+
errors.push('success must be a boolean');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (!Array.isArray(response.widgets)) {
|
|
202
|
+
errors.push('widgets must be an array');
|
|
203
|
+
return { ok: errors.length === 0, errors };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
response.widgets.forEach((widget, index) => {
|
|
207
|
+
validateWidget(widget, `widgets[${index}]`, errors);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
if (
|
|
211
|
+
response.success === false &&
|
|
212
|
+
response.error !== undefined &&
|
|
213
|
+
typeof response.error !== 'string'
|
|
214
|
+
) {
|
|
215
|
+
errors.push('error must be a string when provided');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return { ok: errors.length === 0, errors };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function validateWidget(widget: unknown, path: string, errors: string[]) {
|
|
222
|
+
if (!isRecord(widget)) {
|
|
223
|
+
errors.push(`${path} must be an object`);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
validateNonEmptyString(widget.serviceId, `${path}.serviceId`, errors);
|
|
228
|
+
validateNonEmptyString(widget.serviceName, `${path}.serviceName`, errors);
|
|
229
|
+
validateNonEmptyString(widget.lastUpdated, `${path}.lastUpdated`, errors);
|
|
230
|
+
|
|
231
|
+
switch (widget.type) {
|
|
232
|
+
case 'stats':
|
|
233
|
+
validateArray(widget.stats, `${path}.stats`, errors);
|
|
234
|
+
break;
|
|
235
|
+
case 'recent-activity':
|
|
236
|
+
validateArray(widget.activities, `${path}.activities`, errors);
|
|
237
|
+
break;
|
|
238
|
+
case 'quick-actions':
|
|
239
|
+
validateArray(widget.actions, `${path}.actions`, errors);
|
|
240
|
+
break;
|
|
241
|
+
case 'notifications':
|
|
242
|
+
validateArray(widget.notifications, `${path}.notifications`, errors);
|
|
243
|
+
if (typeof widget.unreadCount !== 'number') {
|
|
244
|
+
errors.push(`${path}.unreadCount must be a number`);
|
|
245
|
+
}
|
|
246
|
+
break;
|
|
247
|
+
case 'chart':
|
|
248
|
+
validateNonEmptyString(widget.chartType, `${path}.chartType`, errors);
|
|
249
|
+
validateNonEmptyString(widget.title, `${path}.title`, errors);
|
|
250
|
+
if (!isRecord(widget.data)) {
|
|
251
|
+
errors.push(`${path}.data must be an object`);
|
|
252
|
+
} else {
|
|
253
|
+
validateArray(widget.data.labels, `${path}.data.labels`, errors);
|
|
254
|
+
validateArray(widget.data.values, `${path}.data.values`, errors);
|
|
255
|
+
}
|
|
256
|
+
break;
|
|
257
|
+
default:
|
|
258
|
+
errors.push(`${path}.type must be a supported Topolo widget type`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function validateNonEmptyString(
|
|
263
|
+
value: unknown,
|
|
264
|
+
path: string,
|
|
265
|
+
errors: string[]
|
|
266
|
+
) {
|
|
267
|
+
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
268
|
+
errors.push(`${path} must be a non-empty string`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function validateArray(value: unknown, path: string, errors: string[]) {
|
|
273
|
+
if (!Array.isArray(value)) {
|
|
274
|
+
errors.push(`${path} must be an array`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
279
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function toIsoTimestamp(input: Date | string | undefined): string {
|
|
283
|
+
if (input instanceof Date) {
|
|
284
|
+
return input.toISOString();
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return input ?? new Date().toISOString();
|
|
288
|
+
}
|