@topolo/sdk 0.11.11 → 0.13.0
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 +62 -10
- package/dist/index.cjs +440 -134
- package/dist/index.d.cts +199 -61
- package/dist/index.d.ts +199 -61
- package/dist/index.js +437 -132
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -27,6 +27,7 @@ __export(index_exports, {
|
|
|
27
27
|
P2PModule: () => P2PModule,
|
|
28
28
|
TOPOLO_ENVIRONMENT_SERVICE_URLS: () => TOPOLO_ENVIRONMENT_SERVICE_URLS,
|
|
29
29
|
TOPOLO_RUNTIME_ENVIRONMENTS: () => TOPOLO_RUNTIME_ENVIRONMENTS,
|
|
30
|
+
TOPOLO_WIDGET_CONTRACT_VERSION: () => TOPOLO_WIDGET_CONTRACT_VERSION,
|
|
30
31
|
TOPOLO_WIDGET_ENDPOINT: () => TOPOLO_WIDGET_ENDPOINT,
|
|
31
32
|
TOPOLO_WIDGET_REFRESH_INTERVAL_MS: () => TOPOLO_WIDGET_REFRESH_INTERVAL_MS,
|
|
32
33
|
TopoloAuthError: () => TopoloAuthError,
|
|
@@ -53,10 +54,10 @@ __export(index_exports, {
|
|
|
53
54
|
createTopoloAgentExecutionContext: () => createTopoloAgentExecutionContext,
|
|
54
55
|
createTopoloWidgetError: () => createTopoloWidgetError,
|
|
55
56
|
createTopoloWidgetResponse: () => createTopoloWidgetResponse,
|
|
57
|
+
defineTopoloActionsWidget: () => defineTopoloActionsWidget,
|
|
58
|
+
defineTopoloActivityWidget: () => defineTopoloActivityWidget,
|
|
56
59
|
defineTopoloChartWidget: () => defineTopoloChartWidget,
|
|
57
60
|
defineTopoloNotificationsWidget: () => defineTopoloNotificationsWidget,
|
|
58
|
-
defineTopoloQuickActionsWidget: () => defineTopoloQuickActionsWidget,
|
|
59
|
-
defineTopoloRecentActivityWidget: () => defineTopoloRecentActivityWidget,
|
|
60
61
|
defineTopoloStatsWidget: () => defineTopoloStatsWidget,
|
|
61
62
|
digestTopoloActionCatalog: () => digestTopoloActionCatalog,
|
|
62
63
|
encodeTopoloActionBase64: () => encodeTopoloActionBase64,
|
|
@@ -1228,16 +1229,29 @@ var TopoloClient = class _TopoloClient {
|
|
|
1228
1229
|
clearTimeout(timeoutId);
|
|
1229
1230
|
}
|
|
1230
1231
|
};
|
|
1232
|
+
const fetchWithReadRetry = async () => {
|
|
1233
|
+
const maxAttempts = method === "GET" ? 2 : 1;
|
|
1234
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
1235
|
+
try {
|
|
1236
|
+
return await fetchOnce();
|
|
1237
|
+
} catch (error) {
|
|
1238
|
+
const transportFailure = !(error instanceof TopoloSdkError) && !opts.signal?.aborted;
|
|
1239
|
+
if (!transportFailure || attempt === maxAttempts) throw error;
|
|
1240
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
throw new TopoloSdkError("network_error", "Topolo read retry loop exhausted.");
|
|
1244
|
+
};
|
|
1231
1245
|
let res;
|
|
1232
1246
|
try {
|
|
1233
1247
|
const attemptedCredential = this.credential;
|
|
1234
|
-
res = await
|
|
1248
|
+
res = await fetchWithReadRetry();
|
|
1235
1249
|
if (res.status === 401 && attemptedCredential.kind === "access_token" && this.refreshCredential) {
|
|
1236
1250
|
await res.body?.cancel().catch(() => void 0);
|
|
1237
1251
|
const refreshed = await this.refreshRejectedAccessToken(attemptedCredential.accessToken);
|
|
1238
1252
|
if (refreshed) {
|
|
1239
1253
|
applyAuthHeaders(headers, refreshed);
|
|
1240
|
-
res = await
|
|
1254
|
+
res = await fetchWithReadRetry();
|
|
1241
1255
|
}
|
|
1242
1256
|
}
|
|
1243
1257
|
} catch (err) {
|
|
@@ -1889,26 +1903,29 @@ var TopoloClient = class _TopoloClient {
|
|
|
1889
1903
|
};
|
|
1890
1904
|
function validateActionResource(entry, resource) {
|
|
1891
1905
|
const permissionName = entry.permissionName || entry.requiredPermission.split(".").at(-1) || "";
|
|
1892
|
-
|
|
1906
|
+
const declaredContext = entry.agentContract?.resourceContext;
|
|
1907
|
+
const declaredKind = typeof declaredContext?.kind === "string" ? declaredContext.kind.trim() : "";
|
|
1908
|
+
const expectedType = declaredKind || (permissionName.startsWith("workspace:") ? "workspace" : "");
|
|
1909
|
+
if (!expectedType || declaredContext?.required === false && !permissionName.startsWith("workspace:")) return null;
|
|
1893
1910
|
if (!resource) {
|
|
1894
1911
|
return {
|
|
1895
1912
|
path: "$.resource",
|
|
1896
1913
|
code: "required",
|
|
1897
|
-
message:
|
|
1914
|
+
message: `Select resourceType "${expectedType}" with the application resource catalog before planning or calling this action.`
|
|
1898
1915
|
};
|
|
1899
1916
|
}
|
|
1900
|
-
if (resource.resourceType !==
|
|
1917
|
+
if (resource.resourceType !== expectedType) {
|
|
1901
1918
|
return {
|
|
1902
1919
|
path: "$.resource.resourceType",
|
|
1903
1920
|
code: "const",
|
|
1904
|
-
message:
|
|
1921
|
+
message: `This action requires resourceType "${expectedType}".`
|
|
1905
1922
|
};
|
|
1906
1923
|
}
|
|
1907
1924
|
if (!resource.resourceId.trim()) {
|
|
1908
1925
|
return {
|
|
1909
1926
|
path: "$.resource.resourceId",
|
|
1910
1927
|
code: "required",
|
|
1911
|
-
message:
|
|
1928
|
+
message: `This action requires a ${expectedType} resourceId.`
|
|
1912
1929
|
};
|
|
1913
1930
|
}
|
|
1914
1931
|
return null;
|
|
@@ -2282,7 +2299,8 @@ function compactActionCatalogEntry(action) {
|
|
|
2282
2299
|
agentAccess: action.agentAccess ?? (action.requiresConfirmation ? "confirm" : action.readOnly ? "auto" : "confirm"),
|
|
2283
2300
|
requiredPermission: action.requiredPermission,
|
|
2284
2301
|
requiredInput: required,
|
|
2285
|
-
resourceType: action.agentContract?.
|
|
2302
|
+
resourceType: action.agentContract?.resourceContext?.kind ?? (action.permissionName.startsWith("workspace:") ? "workspace" : null),
|
|
2303
|
+
resultResourceType: action.agentContract?.resource?.kind ?? null,
|
|
2286
2304
|
exampleInput: example.value,
|
|
2287
2305
|
exampleInputAvailable: example.available,
|
|
2288
2306
|
docsUrl: action.agentContract?.docsUrl ?? null
|
|
@@ -3302,23 +3320,387 @@ var TopoloOAuth = class {
|
|
|
3302
3320
|
}
|
|
3303
3321
|
};
|
|
3304
3322
|
|
|
3323
|
+
// src/widget-validation.ts
|
|
3324
|
+
var CONTRACT_VERSION = "2.0.0";
|
|
3325
|
+
var WIDGET_TYPES = ["stats", "chart", "activity", "notifications", "actions"];
|
|
3326
|
+
var CONTEXTS = ["personal", "organization", "workspace", "resource"];
|
|
3327
|
+
var SIZES = ["compact", "medium", "wide", "full"];
|
|
3328
|
+
var STATES = ["ready", "empty", "unavailable", "stale", "error"];
|
|
3329
|
+
var FIELD_TYPES = ["string", "number", "boolean", "select", "multi-select", "date-range"];
|
|
3330
|
+
var SEMANTICS = ["gauge", "counter", "rate", "duration", "currency", "percentage"];
|
|
3331
|
+
var AGGREGATIONS = ["last", "sum", "average", "minimum", "maximum", "count", "distinct_count"];
|
|
3332
|
+
var CHART_TYPES = ["line", "bar", "area", "pie", "donut"];
|
|
3333
|
+
var SEVERITIES = ["info", "warning", "error", "success"];
|
|
3334
|
+
var RECOVERY_MODES = ["not_applicable", "retry", "compensating_action"];
|
|
3335
|
+
var SEMVER = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
3336
|
+
function validateTopoloWidgetResponse(response) {
|
|
3337
|
+
const errors = [];
|
|
3338
|
+
if (!isRecord5(response)) return { ok: false, errors: ["response must be an object"] };
|
|
3339
|
+
exactValue(response.contractVersion, CONTRACT_VERSION, "contractVersion", errors);
|
|
3340
|
+
nonEmptyString(response.appId, "appId", errors);
|
|
3341
|
+
nonEmptyString(response.serviceName, "serviceName", errors);
|
|
3342
|
+
isoTimestamp(response.generatedAt, "generatedAt", errors);
|
|
3343
|
+
booleanValue(response.success, "success", errors);
|
|
3344
|
+
arrayValue(response.widgets, "widgets", errors);
|
|
3345
|
+
if (response.success === true) {
|
|
3346
|
+
if (Array.isArray(response.widgets) && response.widgets.length === 0) {
|
|
3347
|
+
errors.push("widgets must contain at least one complete widget; use widget state empty for intentional no-data");
|
|
3348
|
+
}
|
|
3349
|
+
if (response.error !== void 0) errors.push("error must be omitted when success is true");
|
|
3350
|
+
} else if (response.success === false) {
|
|
3351
|
+
if (Array.isArray(response.widgets) && response.widgets.length !== 0) {
|
|
3352
|
+
errors.push("widgets must be empty when success is false");
|
|
3353
|
+
}
|
|
3354
|
+
validateApiError(response.error, "error", errors);
|
|
3355
|
+
}
|
|
3356
|
+
if (Array.isArray(response.widgets)) {
|
|
3357
|
+
response.widgets.forEach((widget, index) => validateWidget(widget, `widgets[${index}]`, errors));
|
|
3358
|
+
}
|
|
3359
|
+
return { ok: errors.length === 0, errors };
|
|
3360
|
+
}
|
|
3361
|
+
function validateApiError(value, path, errors) {
|
|
3362
|
+
if (!recordValue(value, path, errors)) return;
|
|
3363
|
+
nonEmptyString(value.code, `${path}.code`, errors);
|
|
3364
|
+
nonEmptyString(value.message, `${path}.message`, errors);
|
|
3365
|
+
booleanValue(value.retryable, `${path}.retryable`, errors);
|
|
3366
|
+
optionalIsoTimestamp(value.retryAfter, `${path}.retryAfter`, errors);
|
|
3367
|
+
}
|
|
3368
|
+
function validateWidget(value, path, errors) {
|
|
3369
|
+
if (!recordValue(value, path, errors)) return;
|
|
3370
|
+
nonEmptyString(value.id, `${path}.id`, errors);
|
|
3371
|
+
patternString(value.version, SEMVER, `${path}.version`, errors);
|
|
3372
|
+
nonEmptyString(value.appId, `${path}.appId`, errors);
|
|
3373
|
+
nonEmptyString(value.serviceName, `${path}.serviceName`, errors);
|
|
3374
|
+
enumValue(value.type, WIDGET_TYPES, `${path}.type`, errors);
|
|
3375
|
+
isoTimestamp(value.lastUpdated, `${path}.lastUpdated`, errors);
|
|
3376
|
+
validatePresentation(value.presentation, `${path}.presentation`, errors);
|
|
3377
|
+
validateResource(value.resource, `${path}.resource`, errors);
|
|
3378
|
+
validateState(value.state, `${path}.state`, errors);
|
|
3379
|
+
validateRefresh(value.refresh, `${path}.refresh`, errors);
|
|
3380
|
+
validateConfiguration(value.configuration, `${path}.configuration`, errors);
|
|
3381
|
+
validateDrilldowns(value.drilldowns, `${path}.drilldowns`, errors);
|
|
3382
|
+
validateActions(value.controls, `${path}.controls`, errors);
|
|
3383
|
+
validateLocalization(value.localization, `${path}.localization`, errors);
|
|
3384
|
+
validateAccessibility(value.accessibility, `${path}.accessibility`, errors);
|
|
3385
|
+
switch (value.type) {
|
|
3386
|
+
case "stats":
|
|
3387
|
+
validateMetrics(value.metrics, `${path}.metrics`, errors, requiresContent(value));
|
|
3388
|
+
break;
|
|
3389
|
+
case "chart":
|
|
3390
|
+
enumValue(value.chartType, CHART_TYPES, `${path}.chartType`, errors);
|
|
3391
|
+
validateSeriesCollection(value.series, `${path}.series`, errors, requiresContent(value));
|
|
3392
|
+
break;
|
|
3393
|
+
case "activity":
|
|
3394
|
+
validateActivities(value.activities, `${path}.activities`, errors, requiresContent(value));
|
|
3395
|
+
break;
|
|
3396
|
+
case "notifications":
|
|
3397
|
+
validateNotifications(value.notifications, `${path}.notifications`, errors, requiresContent(value));
|
|
3398
|
+
nonNegativeInteger(value.unreadCount, `${path}.unreadCount`, errors);
|
|
3399
|
+
break;
|
|
3400
|
+
case "actions":
|
|
3401
|
+
validateActions(value.actions, `${path}.actions`, errors, requiresContent(value));
|
|
3402
|
+
break;
|
|
3403
|
+
}
|
|
3404
|
+
}
|
|
3405
|
+
function validatePresentation(value, path, errors) {
|
|
3406
|
+
if (!recordValue(value, path, errors)) return;
|
|
3407
|
+
nonEmptyString(value.title, `${path}.title`, errors);
|
|
3408
|
+
optionalNonEmptyString(value.description, `${path}.description`, errors);
|
|
3409
|
+
enumArray(value.contexts, CONTEXTS, `${path}.contexts`, errors, true);
|
|
3410
|
+
enumArray(value.supportedSizes, SIZES, `${path}.supportedSizes`, errors, true);
|
|
3411
|
+
enumValue(value.defaultSize, SIZES, `${path}.defaultSize`, errors);
|
|
3412
|
+
if (typeof value.defaultSize === "string" && Array.isArray(value.supportedSizes) && !value.supportedSizes.includes(value.defaultSize)) {
|
|
3413
|
+
errors.push(`${path}.defaultSize must be included in supportedSizes`);
|
|
3414
|
+
}
|
|
3415
|
+
}
|
|
3416
|
+
function validateResource(value, path, errors) {
|
|
3417
|
+
if (!recordValue(value, path, errors)) return;
|
|
3418
|
+
nonEmptyString(value.resourceType, `${path}.resourceType`, errors);
|
|
3419
|
+
optionalNonEmptyString(value.resourceId, `${path}.resourceId`, errors);
|
|
3420
|
+
stringArray2(value.requiredPermissions, `${path}.requiredPermissions`, errors, true);
|
|
3421
|
+
}
|
|
3422
|
+
function validateState(value, path, errors) {
|
|
3423
|
+
if (!recordValue(value, path, errors)) return;
|
|
3424
|
+
enumValue(value.status, STATES, `${path}.status`, errors);
|
|
3425
|
+
isoTimestamp(value.observedAt, `${path}.observedAt`, errors);
|
|
3426
|
+
optionalIsoTimestamp(value.lastSuccessfulAt, `${path}.lastSuccessfulAt`, errors);
|
|
3427
|
+
optionalNonEmptyString(value.code, `${path}.code`, errors);
|
|
3428
|
+
optionalNonEmptyString(value.message, `${path}.message`, errors);
|
|
3429
|
+
if (["empty", "unavailable", "error"].includes(String(value.status))) {
|
|
3430
|
+
nonEmptyString(value.message, `${path}.message`, errors);
|
|
3431
|
+
}
|
|
3432
|
+
if (["unavailable", "error"].includes(String(value.status))) {
|
|
3433
|
+
nonEmptyString(value.code, `${path}.code`, errors);
|
|
3434
|
+
}
|
|
3435
|
+
if (value.status === "stale") isoTimestamp(value.lastSuccessfulAt, `${path}.lastSuccessfulAt`, errors);
|
|
3436
|
+
}
|
|
3437
|
+
function validateRefresh(value, path, errors) {
|
|
3438
|
+
if (!recordValue(value, path, errors)) return;
|
|
3439
|
+
positiveInteger(value.intervalSeconds, `${path}.intervalSeconds`, errors);
|
|
3440
|
+
positiveInteger(value.timeoutSeconds, `${path}.timeoutSeconds`, errors);
|
|
3441
|
+
positiveInteger(value.maxAgeSeconds, `${path}.maxAgeSeconds`, errors);
|
|
3442
|
+
nonNegativeInteger(value.staleWhileRevalidateSeconds, `${path}.staleWhileRevalidateSeconds`, errors);
|
|
3443
|
+
if (isPositiveNumber(value.timeoutSeconds) && isPositiveNumber(value.intervalSeconds) && value.timeoutSeconds > value.intervalSeconds) {
|
|
3444
|
+
errors.push(`${path}.timeoutSeconds must not exceed intervalSeconds`);
|
|
3445
|
+
}
|
|
3446
|
+
}
|
|
3447
|
+
function validateConfiguration(value, path, errors) {
|
|
3448
|
+
if (!recordValue(value, path, errors)) return;
|
|
3449
|
+
booleanValue(value.allowMultipleInstances, `${path}.allowMultipleInstances`, errors);
|
|
3450
|
+
recordValue(value.defaults, `${path}.defaults`, errors);
|
|
3451
|
+
if (!arrayValue(value.fields, `${path}.fields`, errors)) return;
|
|
3452
|
+
value.fields.forEach((field, index) => {
|
|
3453
|
+
const fieldPath = `${path}.fields[${index}]`;
|
|
3454
|
+
if (!recordValue(field, fieldPath, errors)) return;
|
|
3455
|
+
nonEmptyString(field.key, `${fieldPath}.key`, errors);
|
|
3456
|
+
nonEmptyString(field.label, `${fieldPath}.label`, errors);
|
|
3457
|
+
enumValue(field.type, FIELD_TYPES, `${fieldPath}.type`, errors);
|
|
3458
|
+
booleanValue(field.required, `${fieldPath}.required`, errors);
|
|
3459
|
+
if (field.type === "select" || field.type === "multi-select") {
|
|
3460
|
+
if (!arrayValue(field.options, `${fieldPath}.options`, errors, true)) return;
|
|
3461
|
+
field.options.forEach((option, optionIndex) => {
|
|
3462
|
+
const optionPath = `${fieldPath}.options[${optionIndex}]`;
|
|
3463
|
+
if (!recordValue(option, optionPath, errors)) return;
|
|
3464
|
+
nonEmptyString(option.value, `${optionPath}.value`, errors);
|
|
3465
|
+
nonEmptyString(option.label, `${optionPath}.label`, errors);
|
|
3466
|
+
});
|
|
3467
|
+
} else if (field.options !== void 0) {
|
|
3468
|
+
errors.push(`${fieldPath}.options is only valid for select fields`);
|
|
3469
|
+
}
|
|
3470
|
+
});
|
|
3471
|
+
}
|
|
3472
|
+
function validateDrilldowns(value, path, errors) {
|
|
3473
|
+
if (!arrayValue(value, path, errors)) return;
|
|
3474
|
+
value.forEach((drilldown, index) => {
|
|
3475
|
+
const itemPath = `${path}[${index}]`;
|
|
3476
|
+
if (!recordValue(drilldown, itemPath, errors)) return;
|
|
3477
|
+
nonEmptyString(drilldown.id, `${itemPath}.id`, errors);
|
|
3478
|
+
nonEmptyString(drilldown.label, `${itemPath}.label`, errors);
|
|
3479
|
+
nonEmptyString(drilldown.href, `${itemPath}.href`, errors);
|
|
3480
|
+
});
|
|
3481
|
+
}
|
|
3482
|
+
function validateActions(value, path, errors, requireOne = false) {
|
|
3483
|
+
if (!arrayValue(value, path, errors, requireOne)) return;
|
|
3484
|
+
value.forEach((action, index) => {
|
|
3485
|
+
const itemPath = `${path}[${index}]`;
|
|
3486
|
+
if (!recordValue(action, itemPath, errors)) return;
|
|
3487
|
+
nonEmptyString(action.id, `${itemPath}.id`, errors);
|
|
3488
|
+
nonEmptyString(action.label, `${itemPath}.label`, errors);
|
|
3489
|
+
nonEmptyString(action.actionId, `${itemPath}.actionId`, errors);
|
|
3490
|
+
nonEmptyString(action.resourceType, `${itemPath}.resourceType`, errors);
|
|
3491
|
+
optionalNonEmptyString(action.resourceId, `${itemPath}.resourceId`, errors);
|
|
3492
|
+
recordValue(action.input, `${itemPath}.input`, errors);
|
|
3493
|
+
booleanValue(action.requiresConfirmation, `${itemPath}.requiresConfirmation`, errors);
|
|
3494
|
+
booleanValue(action.destructive, `${itemPath}.destructive`, errors);
|
|
3495
|
+
stringArray2(action.effects, `${itemPath}.effects`, errors, true);
|
|
3496
|
+
stringArray2(action.verification, `${itemPath}.verification`, errors, true);
|
|
3497
|
+
validateRecovery(action.recovery, `${itemPath}.recovery`, errors);
|
|
3498
|
+
});
|
|
3499
|
+
}
|
|
3500
|
+
function validateRecovery(value, path, errors) {
|
|
3501
|
+
if (!recordValue(value, path, errors)) return;
|
|
3502
|
+
enumValue(value.mode, RECOVERY_MODES, `${path}.mode`, errors);
|
|
3503
|
+
optionalNonEmptyString(value.actionId, `${path}.actionId`, errors);
|
|
3504
|
+
nonEmptyString(value.instructions, `${path}.instructions`, errors);
|
|
3505
|
+
if (value.mode === "compensating_action" && value.actionId === void 0) {
|
|
3506
|
+
errors.push(`${path}.actionId is required for compensating_action`);
|
|
3507
|
+
}
|
|
3508
|
+
}
|
|
3509
|
+
function validateLocalization(value, path, errors) {
|
|
3510
|
+
if (!recordValue(value, path, errors)) return;
|
|
3511
|
+
nonEmptyString(value.locale, `${path}.locale`, errors);
|
|
3512
|
+
nonEmptyString(value.titleKey, `${path}.titleKey`, errors);
|
|
3513
|
+
optionalNonEmptyString(value.descriptionKey, `${path}.descriptionKey`, errors);
|
|
3514
|
+
optionalNonEmptyString(value.title, `${path}.title`, errors);
|
|
3515
|
+
optionalNonEmptyString(value.description, `${path}.description`, errors);
|
|
3516
|
+
}
|
|
3517
|
+
function validateAccessibility(value, path, errors) {
|
|
3518
|
+
if (!recordValue(value, path, errors)) return;
|
|
3519
|
+
nonEmptyString(value.label, `${path}.label`, errors);
|
|
3520
|
+
optionalNonEmptyString(value.description, `${path}.description`, errors);
|
|
3521
|
+
}
|
|
3522
|
+
function validateMetrics(value, path, errors, requireOne) {
|
|
3523
|
+
if (!arrayValue(value, path, errors, requireOne)) return;
|
|
3524
|
+
value.forEach((metric, index) => {
|
|
3525
|
+
const itemPath = `${path}[${index}]`;
|
|
3526
|
+
if (!recordValue(metric, itemPath, errors)) return;
|
|
3527
|
+
nonEmptyString(metric.key, `${itemPath}.key`, errors);
|
|
3528
|
+
nonEmptyString(metric.label, `${itemPath}.label`, errors);
|
|
3529
|
+
finiteNumber(metric.value, `${itemPath}.value`, errors);
|
|
3530
|
+
nonEmptyString(metric.unit, `${itemPath}.unit`, errors);
|
|
3531
|
+
enumValue(metric.semantic, SEMANTICS, `${itemPath}.semantic`, errors);
|
|
3532
|
+
enumValue(metric.aggregation, AGGREGATIONS, `${itemPath}.aggregation`, errors);
|
|
3533
|
+
validatePeriod(metric.period, `${itemPath}.period`, errors);
|
|
3534
|
+
validateDimensions(metric.dimensions, `${itemPath}.dimensions`, errors);
|
|
3535
|
+
if (metric.comparison !== void 0) validateComparison(metric.comparison, `${itemPath}.comparison`, errors);
|
|
3536
|
+
if (metric.series !== void 0) validateSeries(metric.series, `${itemPath}.series`, errors);
|
|
3537
|
+
});
|
|
3538
|
+
}
|
|
3539
|
+
function validatePeriod(value, path, errors) {
|
|
3540
|
+
if (!recordValue(value, path, errors)) return;
|
|
3541
|
+
isoTimestamp(value.start, `${path}.start`, errors);
|
|
3542
|
+
isoTimestamp(value.end, `${path}.end`, errors);
|
|
3543
|
+
nonEmptyString(value.timezone, `${path}.timezone`, errors);
|
|
3544
|
+
if (isIsoTimestamp(value.start) && isIsoTimestamp(value.end) && Date.parse(value.start) > Date.parse(value.end)) {
|
|
3545
|
+
errors.push(`${path}.start must not be after end`);
|
|
3546
|
+
}
|
|
3547
|
+
}
|
|
3548
|
+
function validateDimensions(value, path, errors) {
|
|
3549
|
+
if (!arrayValue(value, path, errors)) return;
|
|
3550
|
+
value.forEach((dimension, index) => {
|
|
3551
|
+
const itemPath = `${path}[${index}]`;
|
|
3552
|
+
if (!recordValue(dimension, itemPath, errors)) return;
|
|
3553
|
+
nonEmptyString(dimension.key, `${itemPath}.key`, errors);
|
|
3554
|
+
nonEmptyString(dimension.label, `${itemPath}.label`, errors);
|
|
3555
|
+
nonEmptyString(dimension.value, `${itemPath}.value`, errors);
|
|
3556
|
+
});
|
|
3557
|
+
}
|
|
3558
|
+
function validateComparison(value, path, errors) {
|
|
3559
|
+
if (!recordValue(value, path, errors)) return;
|
|
3560
|
+
finiteNumber(value.value, `${path}.value`, errors);
|
|
3561
|
+
finiteNumber(value.absoluteChange, `${path}.absoluteChange`, errors);
|
|
3562
|
+
optionalFiniteNumber(value.percentageChange, `${path}.percentageChange`, errors);
|
|
3563
|
+
enumValue(value.direction, ["up", "down", "flat"], `${path}.direction`, errors);
|
|
3564
|
+
nonEmptyString(value.label, `${path}.label`, errors);
|
|
3565
|
+
validatePeriod(value.period, `${path}.period`, errors);
|
|
3566
|
+
}
|
|
3567
|
+
function validateSeriesCollection(value, path, errors, requireOne) {
|
|
3568
|
+
if (!arrayValue(value, path, errors, requireOne)) return;
|
|
3569
|
+
value.forEach((series, index) => validateSeries(series, `${path}[${index}]`, errors));
|
|
3570
|
+
}
|
|
3571
|
+
function validateSeries(value, path, errors) {
|
|
3572
|
+
if (!recordValue(value, path, errors)) return;
|
|
3573
|
+
nonEmptyString(value.id, `${path}.id`, errors);
|
|
3574
|
+
nonEmptyString(value.label, `${path}.label`, errors);
|
|
3575
|
+
nonEmptyString(value.unit, `${path}.unit`, errors);
|
|
3576
|
+
if (!arrayValue(value.points, `${path}.points`, errors, true)) return;
|
|
3577
|
+
value.points.forEach((point, index) => {
|
|
3578
|
+
const itemPath = `${path}.points[${index}]`;
|
|
3579
|
+
if (!recordValue(point, itemPath, errors)) return;
|
|
3580
|
+
isoTimestamp(point.timestamp, `${itemPath}.timestamp`, errors);
|
|
3581
|
+
finiteNumber(point.value, `${itemPath}.value`, errors);
|
|
3582
|
+
validateDimensions(point.dimensions, `${itemPath}.dimensions`, errors);
|
|
3583
|
+
});
|
|
3584
|
+
}
|
|
3585
|
+
function validateActivities(value, path, errors, requireOne) {
|
|
3586
|
+
if (!arrayValue(value, path, errors, requireOne)) return;
|
|
3587
|
+
value.forEach((activity, index) => {
|
|
3588
|
+
const itemPath = `${path}[${index}]`;
|
|
3589
|
+
if (!recordValue(activity, itemPath, errors)) return;
|
|
3590
|
+
nonEmptyString(activity.id, `${itemPath}.id`, errors);
|
|
3591
|
+
nonEmptyString(activity.title, `${itemPath}.title`, errors);
|
|
3592
|
+
optionalNonEmptyString(activity.description, `${itemPath}.description`, errors);
|
|
3593
|
+
isoTimestamp(activity.timestamp, `${itemPath}.timestamp`, errors);
|
|
3594
|
+
optionalNonEmptyString(activity.drilldownId, `${itemPath}.drilldownId`, errors);
|
|
3595
|
+
optionalNonEmptyString(activity.actionId, `${itemPath}.actionId`, errors);
|
|
3596
|
+
});
|
|
3597
|
+
}
|
|
3598
|
+
function validateNotifications(value, path, errors, requireOne) {
|
|
3599
|
+
if (!arrayValue(value, path, errors, requireOne)) return;
|
|
3600
|
+
value.forEach((notification, index) => {
|
|
3601
|
+
const itemPath = `${path}[${index}]`;
|
|
3602
|
+
if (!recordValue(notification, itemPath, errors)) return;
|
|
3603
|
+
nonEmptyString(notification.id, `${itemPath}.id`, errors);
|
|
3604
|
+
nonEmptyString(notification.title, `${itemPath}.title`, errors);
|
|
3605
|
+
nonEmptyString(notification.message, `${itemPath}.message`, errors);
|
|
3606
|
+
enumValue(notification.severity, SEVERITIES, `${itemPath}.severity`, errors);
|
|
3607
|
+
isoTimestamp(notification.timestamp, `${itemPath}.timestamp`, errors);
|
|
3608
|
+
booleanValue(notification.read, `${itemPath}.read`, errors);
|
|
3609
|
+
optionalNonEmptyString(notification.drilldownId, `${itemPath}.drilldownId`, errors);
|
|
3610
|
+
optionalNonEmptyString(notification.actionId, `${itemPath}.actionId`, errors);
|
|
3611
|
+
});
|
|
3612
|
+
}
|
|
3613
|
+
function requiresContent(widget) {
|
|
3614
|
+
return isRecord5(widget.state) && (widget.state.status === "ready" || widget.state.status === "stale");
|
|
3615
|
+
}
|
|
3616
|
+
function recordValue(value, path, errors) {
|
|
3617
|
+
if (!isRecord5(value)) {
|
|
3618
|
+
errors.push(`${path} must be an object`);
|
|
3619
|
+
return false;
|
|
3620
|
+
}
|
|
3621
|
+
return true;
|
|
3622
|
+
}
|
|
3623
|
+
function arrayValue(value, path, errors, requireOne = false) {
|
|
3624
|
+
if (!Array.isArray(value)) {
|
|
3625
|
+
errors.push(`${path} must be an array`);
|
|
3626
|
+
return false;
|
|
3627
|
+
}
|
|
3628
|
+
if (requireOne && value.length === 0) errors.push(`${path} must contain at least one item`);
|
|
3629
|
+
return true;
|
|
3630
|
+
}
|
|
3631
|
+
function stringArray2(value, path, errors, requireOne = false) {
|
|
3632
|
+
if (!arrayValue(value, path, errors, requireOne)) return;
|
|
3633
|
+
value.forEach((item, index) => nonEmptyString(item, `${path}[${index}]`, errors));
|
|
3634
|
+
}
|
|
3635
|
+
function enumArray(value, allowed, path, errors, requireOne) {
|
|
3636
|
+
if (!arrayValue(value, path, errors, requireOne)) return;
|
|
3637
|
+
value.forEach((item, index) => enumValue(item, allowed, `${path}[${index}]`, errors));
|
|
3638
|
+
if (new Set(value).size !== value.length) errors.push(`${path} must not contain duplicates`);
|
|
3639
|
+
}
|
|
3640
|
+
function nonEmptyString(value, path, errors) {
|
|
3641
|
+
if (typeof value !== "string" || value.trim().length === 0) errors.push(`${path} must be a non-empty string`);
|
|
3642
|
+
}
|
|
3643
|
+
function optionalNonEmptyString(value, path, errors) {
|
|
3644
|
+
if (value !== void 0) nonEmptyString(value, path, errors);
|
|
3645
|
+
}
|
|
3646
|
+
function patternString(value, pattern, path, errors) {
|
|
3647
|
+
if (typeof value !== "string" || !pattern.test(value)) errors.push(`${path} must be a semantic version`);
|
|
3648
|
+
}
|
|
3649
|
+
function enumValue(value, allowed, path, errors) {
|
|
3650
|
+
if (typeof value !== "string" || !allowed.includes(value)) errors.push(`${path} must be one of: ${allowed.join(", ")}`);
|
|
3651
|
+
}
|
|
3652
|
+
function exactValue(value, expected, path, errors) {
|
|
3653
|
+
if (value !== expected) errors.push(`${path} must be ${expected}`);
|
|
3654
|
+
}
|
|
3655
|
+
function booleanValue(value, path, errors) {
|
|
3656
|
+
if (typeof value !== "boolean") errors.push(`${path} must be a boolean`);
|
|
3657
|
+
}
|
|
3658
|
+
function finiteNumber(value, path, errors) {
|
|
3659
|
+
if (typeof value !== "number" || !Number.isFinite(value)) errors.push(`${path} must be a finite number`);
|
|
3660
|
+
}
|
|
3661
|
+
function optionalFiniteNumber(value, path, errors) {
|
|
3662
|
+
if (value !== void 0) finiteNumber(value, path, errors);
|
|
3663
|
+
}
|
|
3664
|
+
function positiveInteger(value, path, errors) {
|
|
3665
|
+
if (!Number.isInteger(value) || Number(value) <= 0) errors.push(`${path} must be a positive integer`);
|
|
3666
|
+
}
|
|
3667
|
+
function nonNegativeInteger(value, path, errors) {
|
|
3668
|
+
if (!Number.isInteger(value) || Number(value) < 0) errors.push(`${path} must be a non-negative integer`);
|
|
3669
|
+
}
|
|
3670
|
+
function isoTimestamp(value, path, errors) {
|
|
3671
|
+
if (!isIsoTimestamp(value)) errors.push(`${path} must be an ISO timestamp`);
|
|
3672
|
+
}
|
|
3673
|
+
function optionalIsoTimestamp(value, path, errors) {
|
|
3674
|
+
if (value !== void 0) isoTimestamp(value, path, errors);
|
|
3675
|
+
}
|
|
3676
|
+
function isIsoTimestamp(value) {
|
|
3677
|
+
return typeof value === "string" && value.trim().length > 0 && Number.isFinite(Date.parse(value));
|
|
3678
|
+
}
|
|
3679
|
+
function isPositiveNumber(value) {
|
|
3680
|
+
return typeof value === "number" && value > 0;
|
|
3681
|
+
}
|
|
3682
|
+
function isRecord5(value) {
|
|
3683
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3684
|
+
}
|
|
3685
|
+
|
|
3305
3686
|
// src/widgets.ts
|
|
3306
3687
|
var TOPOLO_WIDGET_ENDPOINT = "/api/widget";
|
|
3307
3688
|
var TOPOLO_WIDGET_REFRESH_INTERVAL_MS = 6e4;
|
|
3689
|
+
var TOPOLO_WIDGET_CONTRACT_VERSION = "2.0.0";
|
|
3308
3690
|
function defineTopoloStatsWidget(widget) {
|
|
3309
3691
|
return { ...widget, type: "stats" };
|
|
3310
3692
|
}
|
|
3311
|
-
function
|
|
3312
|
-
return { ...widget, type: "
|
|
3693
|
+
function defineTopoloChartWidget(widget) {
|
|
3694
|
+
return { ...widget, type: "chart" };
|
|
3313
3695
|
}
|
|
3314
|
-
function
|
|
3315
|
-
return { ...widget, type: "
|
|
3696
|
+
function defineTopoloActivityWidget(widget) {
|
|
3697
|
+
return { ...widget, type: "activity" };
|
|
3316
3698
|
}
|
|
3317
3699
|
function defineTopoloNotificationsWidget(widget) {
|
|
3318
3700
|
return { ...widget, type: "notifications" };
|
|
3319
3701
|
}
|
|
3320
|
-
function
|
|
3321
|
-
return { ...widget, type: "
|
|
3702
|
+
function defineTopoloActionsWidget(widget) {
|
|
3703
|
+
return { ...widget, type: "actions" };
|
|
3322
3704
|
}
|
|
3323
3705
|
function createTopoloWidgetResponse({
|
|
3324
3706
|
appId,
|
|
@@ -3326,136 +3708,59 @@ function createTopoloWidgetResponse({
|
|
|
3326
3708
|
widgets,
|
|
3327
3709
|
now
|
|
3328
3710
|
}) {
|
|
3329
|
-
const
|
|
3330
|
-
|
|
3711
|
+
const generatedAt = toIsoTimestamp(now);
|
|
3712
|
+
const response = {
|
|
3331
3713
|
success: true,
|
|
3714
|
+
contractVersion: TOPOLO_WIDGET_CONTRACT_VERSION,
|
|
3715
|
+
appId,
|
|
3716
|
+
serviceName,
|
|
3717
|
+
generatedAt,
|
|
3332
3718
|
widgets: widgets.map((widget) => ({
|
|
3333
3719
|
...widget,
|
|
3334
3720
|
appId: widget.appId?.trim() || appId,
|
|
3335
3721
|
serviceName: widget.serviceName?.trim() || serviceName,
|
|
3336
|
-
lastUpdated: widget.lastUpdated?.trim() ||
|
|
3722
|
+
lastUpdated: widget.lastUpdated?.trim() || generatedAt
|
|
3337
3723
|
}))
|
|
3338
3724
|
};
|
|
3725
|
+
assertValidTopoloWidgetResponse(response);
|
|
3726
|
+
return response;
|
|
3339
3727
|
}
|
|
3340
|
-
function createTopoloWidgetError(
|
|
3341
|
-
|
|
3728
|
+
function createTopoloWidgetError({
|
|
3729
|
+
appId,
|
|
3730
|
+
serviceName,
|
|
3731
|
+
code,
|
|
3732
|
+
message,
|
|
3733
|
+
retryable,
|
|
3734
|
+
retryAfter,
|
|
3735
|
+
now
|
|
3736
|
+
}) {
|
|
3737
|
+
const response = {
|
|
3342
3738
|
success: false,
|
|
3739
|
+
contractVersion: TOPOLO_WIDGET_CONTRACT_VERSION,
|
|
3740
|
+
appId,
|
|
3741
|
+
serviceName,
|
|
3742
|
+
generatedAt: toIsoTimestamp(now),
|
|
3343
3743
|
widgets: [],
|
|
3344
|
-
error
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
if (!isRecord5(response)) {
|
|
3350
|
-
return { ok: false, errors: ["response must be an object"] };
|
|
3351
|
-
}
|
|
3352
|
-
if (typeof response.success !== "boolean") {
|
|
3353
|
-
errors.push("success must be a boolean");
|
|
3354
|
-
}
|
|
3355
|
-
if (!Array.isArray(response.widgets)) {
|
|
3356
|
-
errors.push("widgets must be an array");
|
|
3357
|
-
return { ok: errors.length === 0, errors };
|
|
3358
|
-
}
|
|
3359
|
-
response.widgets.forEach((widget, index) => {
|
|
3360
|
-
validateWidget(widget, `widgets[${index}]`, errors);
|
|
3361
|
-
});
|
|
3362
|
-
if (response.success === false && response.error !== void 0 && typeof response.error !== "string") {
|
|
3363
|
-
errors.push("error must be a string when provided");
|
|
3364
|
-
}
|
|
3365
|
-
return { ok: errors.length === 0, errors };
|
|
3366
|
-
}
|
|
3367
|
-
function validateWidget(widget, path, errors) {
|
|
3368
|
-
if (!isRecord5(widget)) {
|
|
3369
|
-
errors.push(`${path} must be an object`);
|
|
3370
|
-
return;
|
|
3371
|
-
}
|
|
3372
|
-
validateNonEmptyString(widget.appId, `${path}.appId`, errors);
|
|
3373
|
-
validateNonEmptyString(widget.serviceName, `${path}.serviceName`, errors);
|
|
3374
|
-
validateNonEmptyString(widget.lastUpdated, `${path}.lastUpdated`, errors);
|
|
3375
|
-
validateSnapshotMetadata(widget.snapshot, `${path}.snapshot`, errors);
|
|
3376
|
-
switch (widget.type) {
|
|
3377
|
-
case "stats":
|
|
3378
|
-
validateArray2(widget.stats, `${path}.stats`, errors);
|
|
3379
|
-
break;
|
|
3380
|
-
case "recent-activity":
|
|
3381
|
-
validateArray2(widget.activities, `${path}.activities`, errors);
|
|
3382
|
-
break;
|
|
3383
|
-
case "quick-actions":
|
|
3384
|
-
validateArray2(widget.actions, `${path}.actions`, errors);
|
|
3385
|
-
break;
|
|
3386
|
-
case "notifications":
|
|
3387
|
-
validateArray2(widget.notifications, `${path}.notifications`, errors);
|
|
3388
|
-
if (typeof widget.unreadCount !== "number") {
|
|
3389
|
-
errors.push(`${path}.unreadCount must be a number`);
|
|
3390
|
-
}
|
|
3391
|
-
break;
|
|
3392
|
-
case "chart":
|
|
3393
|
-
validateNonEmptyString(widget.chartType, `${path}.chartType`, errors);
|
|
3394
|
-
validateNonEmptyString(widget.title, `${path}.title`, errors);
|
|
3395
|
-
if (!isRecord5(widget.data)) {
|
|
3396
|
-
errors.push(`${path}.data must be an object`);
|
|
3397
|
-
} else {
|
|
3398
|
-
validateArray2(widget.data.labels, `${path}.data.labels`, errors);
|
|
3399
|
-
validateArray2(widget.data.values, `${path}.data.values`, errors);
|
|
3400
|
-
}
|
|
3401
|
-
break;
|
|
3402
|
-
default:
|
|
3403
|
-
errors.push(`${path}.type must be a supported Topolo widget type`);
|
|
3404
|
-
}
|
|
3405
|
-
}
|
|
3406
|
-
function validateSnapshotMetadata(snapshot, path, errors) {
|
|
3407
|
-
if (snapshot === void 0) {
|
|
3408
|
-
errors.push(`${path} must be an object`);
|
|
3409
|
-
return;
|
|
3410
|
-
}
|
|
3411
|
-
if (!isRecord5(snapshot)) {
|
|
3412
|
-
errors.push(`${path} must be an object`);
|
|
3413
|
-
return;
|
|
3414
|
-
}
|
|
3415
|
-
validateNonEmptyString(snapshot.summary, `${path}.summary`, errors);
|
|
3416
|
-
if (snapshot.defaultMetricKeys !== void 0 && !Array.isArray(snapshot.defaultMetricKeys)) {
|
|
3417
|
-
errors.push(`${path}.defaultMetricKeys must be an array when provided`);
|
|
3418
|
-
}
|
|
3419
|
-
if (snapshot.highlights !== void 0 && !Array.isArray(snapshot.highlights)) {
|
|
3420
|
-
errors.push(`${path}.highlights must be an array when provided`);
|
|
3421
|
-
}
|
|
3422
|
-
if (snapshot.availableMetrics === void 0) {
|
|
3423
|
-
return;
|
|
3424
|
-
}
|
|
3425
|
-
if (!Array.isArray(snapshot.availableMetrics)) {
|
|
3426
|
-
errors.push(`${path}.availableMetrics must be an array when provided`);
|
|
3427
|
-
return;
|
|
3428
|
-
}
|
|
3429
|
-
snapshot.availableMetrics.forEach((metric, index) => {
|
|
3430
|
-
const metricPath = `${path}.availableMetrics[${index}]`;
|
|
3431
|
-
if (!isRecord5(metric)) {
|
|
3432
|
-
errors.push(`${metricPath} must be an object`);
|
|
3433
|
-
return;
|
|
3434
|
-
}
|
|
3435
|
-
validateNonEmptyString(metric.key, `${metricPath}.key`, errors);
|
|
3436
|
-
validateNonEmptyString(metric.label, `${metricPath}.label`, errors);
|
|
3437
|
-
if (typeof metric.value !== "number" || Number.isNaN(metric.value)) {
|
|
3438
|
-
errors.push(`${metricPath}.value must be a number`);
|
|
3744
|
+
error: {
|
|
3745
|
+
code,
|
|
3746
|
+
message,
|
|
3747
|
+
retryable,
|
|
3748
|
+
...retryAfter === void 0 ? {} : { retryAfter: toIsoTimestamp(retryAfter) }
|
|
3439
3749
|
}
|
|
3440
|
-
}
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3750
|
+
};
|
|
3751
|
+
assertValidTopoloWidgetResponse(response);
|
|
3752
|
+
return response;
|
|
3753
|
+
}
|
|
3754
|
+
function assertValidTopoloWidgetResponse(response) {
|
|
3755
|
+
const validation = validateTopoloWidgetResponse(response);
|
|
3756
|
+
if (!validation.ok) {
|
|
3757
|
+
throw new TypeError(
|
|
3758
|
+
`Invalid Topolo widget contract: ${validation.errors.join("; ")}`
|
|
3759
|
+
);
|
|
3450
3760
|
}
|
|
3451
3761
|
}
|
|
3452
|
-
function isRecord5(value) {
|
|
3453
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3454
|
-
}
|
|
3455
3762
|
function toIsoTimestamp(input) {
|
|
3456
|
-
if (input instanceof Date)
|
|
3457
|
-
return input.toISOString();
|
|
3458
|
-
}
|
|
3763
|
+
if (input instanceof Date) return input.toISOString();
|
|
3459
3764
|
return input ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
3460
3765
|
}
|
|
3461
3766
|
|
|
@@ -3478,6 +3783,7 @@ function createTopolo(options) {
|
|
|
3478
3783
|
P2PModule,
|
|
3479
3784
|
TOPOLO_ENVIRONMENT_SERVICE_URLS,
|
|
3480
3785
|
TOPOLO_RUNTIME_ENVIRONMENTS,
|
|
3786
|
+
TOPOLO_WIDGET_CONTRACT_VERSION,
|
|
3481
3787
|
TOPOLO_WIDGET_ENDPOINT,
|
|
3482
3788
|
TOPOLO_WIDGET_REFRESH_INTERVAL_MS,
|
|
3483
3789
|
TopoloAuthError,
|
|
@@ -3504,10 +3810,10 @@ function createTopolo(options) {
|
|
|
3504
3810
|
createTopoloAgentExecutionContext,
|
|
3505
3811
|
createTopoloWidgetError,
|
|
3506
3812
|
createTopoloWidgetResponse,
|
|
3813
|
+
defineTopoloActionsWidget,
|
|
3814
|
+
defineTopoloActivityWidget,
|
|
3507
3815
|
defineTopoloChartWidget,
|
|
3508
3816
|
defineTopoloNotificationsWidget,
|
|
3509
|
-
defineTopoloQuickActionsWidget,
|
|
3510
|
-
defineTopoloRecentActivityWidget,
|
|
3511
3817
|
defineTopoloStatsWidget,
|
|
3512
3818
|
digestTopoloActionCatalog,
|
|
3513
3819
|
encodeTopoloActionBase64,
|