@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.js
CHANGED
|
@@ -1145,16 +1145,29 @@ var TopoloClient = class _TopoloClient {
|
|
|
1145
1145
|
clearTimeout(timeoutId);
|
|
1146
1146
|
}
|
|
1147
1147
|
};
|
|
1148
|
+
const fetchWithReadRetry = async () => {
|
|
1149
|
+
const maxAttempts = method === "GET" ? 2 : 1;
|
|
1150
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
1151
|
+
try {
|
|
1152
|
+
return await fetchOnce();
|
|
1153
|
+
} catch (error) {
|
|
1154
|
+
const transportFailure = !(error instanceof TopoloSdkError) && !opts.signal?.aborted;
|
|
1155
|
+
if (!transportFailure || attempt === maxAttempts) throw error;
|
|
1156
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
throw new TopoloSdkError("network_error", "Topolo read retry loop exhausted.");
|
|
1160
|
+
};
|
|
1148
1161
|
let res;
|
|
1149
1162
|
try {
|
|
1150
1163
|
const attemptedCredential = this.credential;
|
|
1151
|
-
res = await
|
|
1164
|
+
res = await fetchWithReadRetry();
|
|
1152
1165
|
if (res.status === 401 && attemptedCredential.kind === "access_token" && this.refreshCredential) {
|
|
1153
1166
|
await res.body?.cancel().catch(() => void 0);
|
|
1154
1167
|
const refreshed = await this.refreshRejectedAccessToken(attemptedCredential.accessToken);
|
|
1155
1168
|
if (refreshed) {
|
|
1156
1169
|
applyAuthHeaders(headers, refreshed);
|
|
1157
|
-
res = await
|
|
1170
|
+
res = await fetchWithReadRetry();
|
|
1158
1171
|
}
|
|
1159
1172
|
}
|
|
1160
1173
|
} catch (err) {
|
|
@@ -1806,26 +1819,29 @@ var TopoloClient = class _TopoloClient {
|
|
|
1806
1819
|
};
|
|
1807
1820
|
function validateActionResource(entry, resource) {
|
|
1808
1821
|
const permissionName = entry.permissionName || entry.requiredPermission.split(".").at(-1) || "";
|
|
1809
|
-
|
|
1822
|
+
const declaredContext = entry.agentContract?.resourceContext;
|
|
1823
|
+
const declaredKind = typeof declaredContext?.kind === "string" ? declaredContext.kind.trim() : "";
|
|
1824
|
+
const expectedType = declaredKind || (permissionName.startsWith("workspace:") ? "workspace" : "");
|
|
1825
|
+
if (!expectedType || declaredContext?.required === false && !permissionName.startsWith("workspace:")) return null;
|
|
1810
1826
|
if (!resource) {
|
|
1811
1827
|
return {
|
|
1812
1828
|
path: "$.resource",
|
|
1813
1829
|
code: "required",
|
|
1814
|
-
message:
|
|
1830
|
+
message: `Select resourceType "${expectedType}" with the application resource catalog before planning or calling this action.`
|
|
1815
1831
|
};
|
|
1816
1832
|
}
|
|
1817
|
-
if (resource.resourceType !==
|
|
1833
|
+
if (resource.resourceType !== expectedType) {
|
|
1818
1834
|
return {
|
|
1819
1835
|
path: "$.resource.resourceType",
|
|
1820
1836
|
code: "const",
|
|
1821
|
-
message:
|
|
1837
|
+
message: `This action requires resourceType "${expectedType}".`
|
|
1822
1838
|
};
|
|
1823
1839
|
}
|
|
1824
1840
|
if (!resource.resourceId.trim()) {
|
|
1825
1841
|
return {
|
|
1826
1842
|
path: "$.resource.resourceId",
|
|
1827
1843
|
code: "required",
|
|
1828
|
-
message:
|
|
1844
|
+
message: `This action requires a ${expectedType} resourceId.`
|
|
1829
1845
|
};
|
|
1830
1846
|
}
|
|
1831
1847
|
return null;
|
|
@@ -2199,7 +2215,8 @@ function compactActionCatalogEntry(action) {
|
|
|
2199
2215
|
agentAccess: action.agentAccess ?? (action.requiresConfirmation ? "confirm" : action.readOnly ? "auto" : "confirm"),
|
|
2200
2216
|
requiredPermission: action.requiredPermission,
|
|
2201
2217
|
requiredInput: required,
|
|
2202
|
-
resourceType: action.agentContract?.
|
|
2218
|
+
resourceType: action.agentContract?.resourceContext?.kind ?? (action.permissionName.startsWith("workspace:") ? "workspace" : null),
|
|
2219
|
+
resultResourceType: action.agentContract?.resource?.kind ?? null,
|
|
2203
2220
|
exampleInput: example.value,
|
|
2204
2221
|
exampleInputAvailable: example.available,
|
|
2205
2222
|
docsUrl: action.agentContract?.docsUrl ?? null
|
|
@@ -3219,23 +3236,387 @@ var TopoloOAuth = class {
|
|
|
3219
3236
|
}
|
|
3220
3237
|
};
|
|
3221
3238
|
|
|
3239
|
+
// src/widget-validation.ts
|
|
3240
|
+
var CONTRACT_VERSION = "2.0.0";
|
|
3241
|
+
var WIDGET_TYPES = ["stats", "chart", "activity", "notifications", "actions"];
|
|
3242
|
+
var CONTEXTS = ["personal", "organization", "workspace", "resource"];
|
|
3243
|
+
var SIZES = ["compact", "medium", "wide", "full"];
|
|
3244
|
+
var STATES = ["ready", "empty", "unavailable", "stale", "error"];
|
|
3245
|
+
var FIELD_TYPES = ["string", "number", "boolean", "select", "multi-select", "date-range"];
|
|
3246
|
+
var SEMANTICS = ["gauge", "counter", "rate", "duration", "currency", "percentage"];
|
|
3247
|
+
var AGGREGATIONS = ["last", "sum", "average", "minimum", "maximum", "count", "distinct_count"];
|
|
3248
|
+
var CHART_TYPES = ["line", "bar", "area", "pie", "donut"];
|
|
3249
|
+
var SEVERITIES = ["info", "warning", "error", "success"];
|
|
3250
|
+
var RECOVERY_MODES = ["not_applicable", "retry", "compensating_action"];
|
|
3251
|
+
var SEMVER = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
3252
|
+
function validateTopoloWidgetResponse(response) {
|
|
3253
|
+
const errors = [];
|
|
3254
|
+
if (!isRecord5(response)) return { ok: false, errors: ["response must be an object"] };
|
|
3255
|
+
exactValue(response.contractVersion, CONTRACT_VERSION, "contractVersion", errors);
|
|
3256
|
+
nonEmptyString(response.appId, "appId", errors);
|
|
3257
|
+
nonEmptyString(response.serviceName, "serviceName", errors);
|
|
3258
|
+
isoTimestamp(response.generatedAt, "generatedAt", errors);
|
|
3259
|
+
booleanValue(response.success, "success", errors);
|
|
3260
|
+
arrayValue(response.widgets, "widgets", errors);
|
|
3261
|
+
if (response.success === true) {
|
|
3262
|
+
if (Array.isArray(response.widgets) && response.widgets.length === 0) {
|
|
3263
|
+
errors.push("widgets must contain at least one complete widget; use widget state empty for intentional no-data");
|
|
3264
|
+
}
|
|
3265
|
+
if (response.error !== void 0) errors.push("error must be omitted when success is true");
|
|
3266
|
+
} else if (response.success === false) {
|
|
3267
|
+
if (Array.isArray(response.widgets) && response.widgets.length !== 0) {
|
|
3268
|
+
errors.push("widgets must be empty when success is false");
|
|
3269
|
+
}
|
|
3270
|
+
validateApiError(response.error, "error", errors);
|
|
3271
|
+
}
|
|
3272
|
+
if (Array.isArray(response.widgets)) {
|
|
3273
|
+
response.widgets.forEach((widget, index) => validateWidget(widget, `widgets[${index}]`, errors));
|
|
3274
|
+
}
|
|
3275
|
+
return { ok: errors.length === 0, errors };
|
|
3276
|
+
}
|
|
3277
|
+
function validateApiError(value, path, errors) {
|
|
3278
|
+
if (!recordValue(value, path, errors)) return;
|
|
3279
|
+
nonEmptyString(value.code, `${path}.code`, errors);
|
|
3280
|
+
nonEmptyString(value.message, `${path}.message`, errors);
|
|
3281
|
+
booleanValue(value.retryable, `${path}.retryable`, errors);
|
|
3282
|
+
optionalIsoTimestamp(value.retryAfter, `${path}.retryAfter`, errors);
|
|
3283
|
+
}
|
|
3284
|
+
function validateWidget(value, path, errors) {
|
|
3285
|
+
if (!recordValue(value, path, errors)) return;
|
|
3286
|
+
nonEmptyString(value.id, `${path}.id`, errors);
|
|
3287
|
+
patternString(value.version, SEMVER, `${path}.version`, errors);
|
|
3288
|
+
nonEmptyString(value.appId, `${path}.appId`, errors);
|
|
3289
|
+
nonEmptyString(value.serviceName, `${path}.serviceName`, errors);
|
|
3290
|
+
enumValue(value.type, WIDGET_TYPES, `${path}.type`, errors);
|
|
3291
|
+
isoTimestamp(value.lastUpdated, `${path}.lastUpdated`, errors);
|
|
3292
|
+
validatePresentation(value.presentation, `${path}.presentation`, errors);
|
|
3293
|
+
validateResource(value.resource, `${path}.resource`, errors);
|
|
3294
|
+
validateState(value.state, `${path}.state`, errors);
|
|
3295
|
+
validateRefresh(value.refresh, `${path}.refresh`, errors);
|
|
3296
|
+
validateConfiguration(value.configuration, `${path}.configuration`, errors);
|
|
3297
|
+
validateDrilldowns(value.drilldowns, `${path}.drilldowns`, errors);
|
|
3298
|
+
validateActions(value.controls, `${path}.controls`, errors);
|
|
3299
|
+
validateLocalization(value.localization, `${path}.localization`, errors);
|
|
3300
|
+
validateAccessibility(value.accessibility, `${path}.accessibility`, errors);
|
|
3301
|
+
switch (value.type) {
|
|
3302
|
+
case "stats":
|
|
3303
|
+
validateMetrics(value.metrics, `${path}.metrics`, errors, requiresContent(value));
|
|
3304
|
+
break;
|
|
3305
|
+
case "chart":
|
|
3306
|
+
enumValue(value.chartType, CHART_TYPES, `${path}.chartType`, errors);
|
|
3307
|
+
validateSeriesCollection(value.series, `${path}.series`, errors, requiresContent(value));
|
|
3308
|
+
break;
|
|
3309
|
+
case "activity":
|
|
3310
|
+
validateActivities(value.activities, `${path}.activities`, errors, requiresContent(value));
|
|
3311
|
+
break;
|
|
3312
|
+
case "notifications":
|
|
3313
|
+
validateNotifications(value.notifications, `${path}.notifications`, errors, requiresContent(value));
|
|
3314
|
+
nonNegativeInteger(value.unreadCount, `${path}.unreadCount`, errors);
|
|
3315
|
+
break;
|
|
3316
|
+
case "actions":
|
|
3317
|
+
validateActions(value.actions, `${path}.actions`, errors, requiresContent(value));
|
|
3318
|
+
break;
|
|
3319
|
+
}
|
|
3320
|
+
}
|
|
3321
|
+
function validatePresentation(value, path, errors) {
|
|
3322
|
+
if (!recordValue(value, path, errors)) return;
|
|
3323
|
+
nonEmptyString(value.title, `${path}.title`, errors);
|
|
3324
|
+
optionalNonEmptyString(value.description, `${path}.description`, errors);
|
|
3325
|
+
enumArray(value.contexts, CONTEXTS, `${path}.contexts`, errors, true);
|
|
3326
|
+
enumArray(value.supportedSizes, SIZES, `${path}.supportedSizes`, errors, true);
|
|
3327
|
+
enumValue(value.defaultSize, SIZES, `${path}.defaultSize`, errors);
|
|
3328
|
+
if (typeof value.defaultSize === "string" && Array.isArray(value.supportedSizes) && !value.supportedSizes.includes(value.defaultSize)) {
|
|
3329
|
+
errors.push(`${path}.defaultSize must be included in supportedSizes`);
|
|
3330
|
+
}
|
|
3331
|
+
}
|
|
3332
|
+
function validateResource(value, path, errors) {
|
|
3333
|
+
if (!recordValue(value, path, errors)) return;
|
|
3334
|
+
nonEmptyString(value.resourceType, `${path}.resourceType`, errors);
|
|
3335
|
+
optionalNonEmptyString(value.resourceId, `${path}.resourceId`, errors);
|
|
3336
|
+
stringArray2(value.requiredPermissions, `${path}.requiredPermissions`, errors, true);
|
|
3337
|
+
}
|
|
3338
|
+
function validateState(value, path, errors) {
|
|
3339
|
+
if (!recordValue(value, path, errors)) return;
|
|
3340
|
+
enumValue(value.status, STATES, `${path}.status`, errors);
|
|
3341
|
+
isoTimestamp(value.observedAt, `${path}.observedAt`, errors);
|
|
3342
|
+
optionalIsoTimestamp(value.lastSuccessfulAt, `${path}.lastSuccessfulAt`, errors);
|
|
3343
|
+
optionalNonEmptyString(value.code, `${path}.code`, errors);
|
|
3344
|
+
optionalNonEmptyString(value.message, `${path}.message`, errors);
|
|
3345
|
+
if (["empty", "unavailable", "error"].includes(String(value.status))) {
|
|
3346
|
+
nonEmptyString(value.message, `${path}.message`, errors);
|
|
3347
|
+
}
|
|
3348
|
+
if (["unavailable", "error"].includes(String(value.status))) {
|
|
3349
|
+
nonEmptyString(value.code, `${path}.code`, errors);
|
|
3350
|
+
}
|
|
3351
|
+
if (value.status === "stale") isoTimestamp(value.lastSuccessfulAt, `${path}.lastSuccessfulAt`, errors);
|
|
3352
|
+
}
|
|
3353
|
+
function validateRefresh(value, path, errors) {
|
|
3354
|
+
if (!recordValue(value, path, errors)) return;
|
|
3355
|
+
positiveInteger(value.intervalSeconds, `${path}.intervalSeconds`, errors);
|
|
3356
|
+
positiveInteger(value.timeoutSeconds, `${path}.timeoutSeconds`, errors);
|
|
3357
|
+
positiveInteger(value.maxAgeSeconds, `${path}.maxAgeSeconds`, errors);
|
|
3358
|
+
nonNegativeInteger(value.staleWhileRevalidateSeconds, `${path}.staleWhileRevalidateSeconds`, errors);
|
|
3359
|
+
if (isPositiveNumber(value.timeoutSeconds) && isPositiveNumber(value.intervalSeconds) && value.timeoutSeconds > value.intervalSeconds) {
|
|
3360
|
+
errors.push(`${path}.timeoutSeconds must not exceed intervalSeconds`);
|
|
3361
|
+
}
|
|
3362
|
+
}
|
|
3363
|
+
function validateConfiguration(value, path, errors) {
|
|
3364
|
+
if (!recordValue(value, path, errors)) return;
|
|
3365
|
+
booleanValue(value.allowMultipleInstances, `${path}.allowMultipleInstances`, errors);
|
|
3366
|
+
recordValue(value.defaults, `${path}.defaults`, errors);
|
|
3367
|
+
if (!arrayValue(value.fields, `${path}.fields`, errors)) return;
|
|
3368
|
+
value.fields.forEach((field, index) => {
|
|
3369
|
+
const fieldPath = `${path}.fields[${index}]`;
|
|
3370
|
+
if (!recordValue(field, fieldPath, errors)) return;
|
|
3371
|
+
nonEmptyString(field.key, `${fieldPath}.key`, errors);
|
|
3372
|
+
nonEmptyString(field.label, `${fieldPath}.label`, errors);
|
|
3373
|
+
enumValue(field.type, FIELD_TYPES, `${fieldPath}.type`, errors);
|
|
3374
|
+
booleanValue(field.required, `${fieldPath}.required`, errors);
|
|
3375
|
+
if (field.type === "select" || field.type === "multi-select") {
|
|
3376
|
+
if (!arrayValue(field.options, `${fieldPath}.options`, errors, true)) return;
|
|
3377
|
+
field.options.forEach((option, optionIndex) => {
|
|
3378
|
+
const optionPath = `${fieldPath}.options[${optionIndex}]`;
|
|
3379
|
+
if (!recordValue(option, optionPath, errors)) return;
|
|
3380
|
+
nonEmptyString(option.value, `${optionPath}.value`, errors);
|
|
3381
|
+
nonEmptyString(option.label, `${optionPath}.label`, errors);
|
|
3382
|
+
});
|
|
3383
|
+
} else if (field.options !== void 0) {
|
|
3384
|
+
errors.push(`${fieldPath}.options is only valid for select fields`);
|
|
3385
|
+
}
|
|
3386
|
+
});
|
|
3387
|
+
}
|
|
3388
|
+
function validateDrilldowns(value, path, errors) {
|
|
3389
|
+
if (!arrayValue(value, path, errors)) return;
|
|
3390
|
+
value.forEach((drilldown, index) => {
|
|
3391
|
+
const itemPath = `${path}[${index}]`;
|
|
3392
|
+
if (!recordValue(drilldown, itemPath, errors)) return;
|
|
3393
|
+
nonEmptyString(drilldown.id, `${itemPath}.id`, errors);
|
|
3394
|
+
nonEmptyString(drilldown.label, `${itemPath}.label`, errors);
|
|
3395
|
+
nonEmptyString(drilldown.href, `${itemPath}.href`, errors);
|
|
3396
|
+
});
|
|
3397
|
+
}
|
|
3398
|
+
function validateActions(value, path, errors, requireOne = false) {
|
|
3399
|
+
if (!arrayValue(value, path, errors, requireOne)) return;
|
|
3400
|
+
value.forEach((action, index) => {
|
|
3401
|
+
const itemPath = `${path}[${index}]`;
|
|
3402
|
+
if (!recordValue(action, itemPath, errors)) return;
|
|
3403
|
+
nonEmptyString(action.id, `${itemPath}.id`, errors);
|
|
3404
|
+
nonEmptyString(action.label, `${itemPath}.label`, errors);
|
|
3405
|
+
nonEmptyString(action.actionId, `${itemPath}.actionId`, errors);
|
|
3406
|
+
nonEmptyString(action.resourceType, `${itemPath}.resourceType`, errors);
|
|
3407
|
+
optionalNonEmptyString(action.resourceId, `${itemPath}.resourceId`, errors);
|
|
3408
|
+
recordValue(action.input, `${itemPath}.input`, errors);
|
|
3409
|
+
booleanValue(action.requiresConfirmation, `${itemPath}.requiresConfirmation`, errors);
|
|
3410
|
+
booleanValue(action.destructive, `${itemPath}.destructive`, errors);
|
|
3411
|
+
stringArray2(action.effects, `${itemPath}.effects`, errors, true);
|
|
3412
|
+
stringArray2(action.verification, `${itemPath}.verification`, errors, true);
|
|
3413
|
+
validateRecovery(action.recovery, `${itemPath}.recovery`, errors);
|
|
3414
|
+
});
|
|
3415
|
+
}
|
|
3416
|
+
function validateRecovery(value, path, errors) {
|
|
3417
|
+
if (!recordValue(value, path, errors)) return;
|
|
3418
|
+
enumValue(value.mode, RECOVERY_MODES, `${path}.mode`, errors);
|
|
3419
|
+
optionalNonEmptyString(value.actionId, `${path}.actionId`, errors);
|
|
3420
|
+
nonEmptyString(value.instructions, `${path}.instructions`, errors);
|
|
3421
|
+
if (value.mode === "compensating_action" && value.actionId === void 0) {
|
|
3422
|
+
errors.push(`${path}.actionId is required for compensating_action`);
|
|
3423
|
+
}
|
|
3424
|
+
}
|
|
3425
|
+
function validateLocalization(value, path, errors) {
|
|
3426
|
+
if (!recordValue(value, path, errors)) return;
|
|
3427
|
+
nonEmptyString(value.locale, `${path}.locale`, errors);
|
|
3428
|
+
nonEmptyString(value.titleKey, `${path}.titleKey`, errors);
|
|
3429
|
+
optionalNonEmptyString(value.descriptionKey, `${path}.descriptionKey`, errors);
|
|
3430
|
+
optionalNonEmptyString(value.title, `${path}.title`, errors);
|
|
3431
|
+
optionalNonEmptyString(value.description, `${path}.description`, errors);
|
|
3432
|
+
}
|
|
3433
|
+
function validateAccessibility(value, path, errors) {
|
|
3434
|
+
if (!recordValue(value, path, errors)) return;
|
|
3435
|
+
nonEmptyString(value.label, `${path}.label`, errors);
|
|
3436
|
+
optionalNonEmptyString(value.description, `${path}.description`, errors);
|
|
3437
|
+
}
|
|
3438
|
+
function validateMetrics(value, path, errors, requireOne) {
|
|
3439
|
+
if (!arrayValue(value, path, errors, requireOne)) return;
|
|
3440
|
+
value.forEach((metric, index) => {
|
|
3441
|
+
const itemPath = `${path}[${index}]`;
|
|
3442
|
+
if (!recordValue(metric, itemPath, errors)) return;
|
|
3443
|
+
nonEmptyString(metric.key, `${itemPath}.key`, errors);
|
|
3444
|
+
nonEmptyString(metric.label, `${itemPath}.label`, errors);
|
|
3445
|
+
finiteNumber(metric.value, `${itemPath}.value`, errors);
|
|
3446
|
+
nonEmptyString(metric.unit, `${itemPath}.unit`, errors);
|
|
3447
|
+
enumValue(metric.semantic, SEMANTICS, `${itemPath}.semantic`, errors);
|
|
3448
|
+
enumValue(metric.aggregation, AGGREGATIONS, `${itemPath}.aggregation`, errors);
|
|
3449
|
+
validatePeriod(metric.period, `${itemPath}.period`, errors);
|
|
3450
|
+
validateDimensions(metric.dimensions, `${itemPath}.dimensions`, errors);
|
|
3451
|
+
if (metric.comparison !== void 0) validateComparison(metric.comparison, `${itemPath}.comparison`, errors);
|
|
3452
|
+
if (metric.series !== void 0) validateSeries(metric.series, `${itemPath}.series`, errors);
|
|
3453
|
+
});
|
|
3454
|
+
}
|
|
3455
|
+
function validatePeriod(value, path, errors) {
|
|
3456
|
+
if (!recordValue(value, path, errors)) return;
|
|
3457
|
+
isoTimestamp(value.start, `${path}.start`, errors);
|
|
3458
|
+
isoTimestamp(value.end, `${path}.end`, errors);
|
|
3459
|
+
nonEmptyString(value.timezone, `${path}.timezone`, errors);
|
|
3460
|
+
if (isIsoTimestamp(value.start) && isIsoTimestamp(value.end) && Date.parse(value.start) > Date.parse(value.end)) {
|
|
3461
|
+
errors.push(`${path}.start must not be after end`);
|
|
3462
|
+
}
|
|
3463
|
+
}
|
|
3464
|
+
function validateDimensions(value, path, errors) {
|
|
3465
|
+
if (!arrayValue(value, path, errors)) return;
|
|
3466
|
+
value.forEach((dimension, index) => {
|
|
3467
|
+
const itemPath = `${path}[${index}]`;
|
|
3468
|
+
if (!recordValue(dimension, itemPath, errors)) return;
|
|
3469
|
+
nonEmptyString(dimension.key, `${itemPath}.key`, errors);
|
|
3470
|
+
nonEmptyString(dimension.label, `${itemPath}.label`, errors);
|
|
3471
|
+
nonEmptyString(dimension.value, `${itemPath}.value`, errors);
|
|
3472
|
+
});
|
|
3473
|
+
}
|
|
3474
|
+
function validateComparison(value, path, errors) {
|
|
3475
|
+
if (!recordValue(value, path, errors)) return;
|
|
3476
|
+
finiteNumber(value.value, `${path}.value`, errors);
|
|
3477
|
+
finiteNumber(value.absoluteChange, `${path}.absoluteChange`, errors);
|
|
3478
|
+
optionalFiniteNumber(value.percentageChange, `${path}.percentageChange`, errors);
|
|
3479
|
+
enumValue(value.direction, ["up", "down", "flat"], `${path}.direction`, errors);
|
|
3480
|
+
nonEmptyString(value.label, `${path}.label`, errors);
|
|
3481
|
+
validatePeriod(value.period, `${path}.period`, errors);
|
|
3482
|
+
}
|
|
3483
|
+
function validateSeriesCollection(value, path, errors, requireOne) {
|
|
3484
|
+
if (!arrayValue(value, path, errors, requireOne)) return;
|
|
3485
|
+
value.forEach((series, index) => validateSeries(series, `${path}[${index}]`, errors));
|
|
3486
|
+
}
|
|
3487
|
+
function validateSeries(value, path, errors) {
|
|
3488
|
+
if (!recordValue(value, path, errors)) return;
|
|
3489
|
+
nonEmptyString(value.id, `${path}.id`, errors);
|
|
3490
|
+
nonEmptyString(value.label, `${path}.label`, errors);
|
|
3491
|
+
nonEmptyString(value.unit, `${path}.unit`, errors);
|
|
3492
|
+
if (!arrayValue(value.points, `${path}.points`, errors, true)) return;
|
|
3493
|
+
value.points.forEach((point, index) => {
|
|
3494
|
+
const itemPath = `${path}.points[${index}]`;
|
|
3495
|
+
if (!recordValue(point, itemPath, errors)) return;
|
|
3496
|
+
isoTimestamp(point.timestamp, `${itemPath}.timestamp`, errors);
|
|
3497
|
+
finiteNumber(point.value, `${itemPath}.value`, errors);
|
|
3498
|
+
validateDimensions(point.dimensions, `${itemPath}.dimensions`, errors);
|
|
3499
|
+
});
|
|
3500
|
+
}
|
|
3501
|
+
function validateActivities(value, path, errors, requireOne) {
|
|
3502
|
+
if (!arrayValue(value, path, errors, requireOne)) return;
|
|
3503
|
+
value.forEach((activity, index) => {
|
|
3504
|
+
const itemPath = `${path}[${index}]`;
|
|
3505
|
+
if (!recordValue(activity, itemPath, errors)) return;
|
|
3506
|
+
nonEmptyString(activity.id, `${itemPath}.id`, errors);
|
|
3507
|
+
nonEmptyString(activity.title, `${itemPath}.title`, errors);
|
|
3508
|
+
optionalNonEmptyString(activity.description, `${itemPath}.description`, errors);
|
|
3509
|
+
isoTimestamp(activity.timestamp, `${itemPath}.timestamp`, errors);
|
|
3510
|
+
optionalNonEmptyString(activity.drilldownId, `${itemPath}.drilldownId`, errors);
|
|
3511
|
+
optionalNonEmptyString(activity.actionId, `${itemPath}.actionId`, errors);
|
|
3512
|
+
});
|
|
3513
|
+
}
|
|
3514
|
+
function validateNotifications(value, path, errors, requireOne) {
|
|
3515
|
+
if (!arrayValue(value, path, errors, requireOne)) return;
|
|
3516
|
+
value.forEach((notification, index) => {
|
|
3517
|
+
const itemPath = `${path}[${index}]`;
|
|
3518
|
+
if (!recordValue(notification, itemPath, errors)) return;
|
|
3519
|
+
nonEmptyString(notification.id, `${itemPath}.id`, errors);
|
|
3520
|
+
nonEmptyString(notification.title, `${itemPath}.title`, errors);
|
|
3521
|
+
nonEmptyString(notification.message, `${itemPath}.message`, errors);
|
|
3522
|
+
enumValue(notification.severity, SEVERITIES, `${itemPath}.severity`, errors);
|
|
3523
|
+
isoTimestamp(notification.timestamp, `${itemPath}.timestamp`, errors);
|
|
3524
|
+
booleanValue(notification.read, `${itemPath}.read`, errors);
|
|
3525
|
+
optionalNonEmptyString(notification.drilldownId, `${itemPath}.drilldownId`, errors);
|
|
3526
|
+
optionalNonEmptyString(notification.actionId, `${itemPath}.actionId`, errors);
|
|
3527
|
+
});
|
|
3528
|
+
}
|
|
3529
|
+
function requiresContent(widget) {
|
|
3530
|
+
return isRecord5(widget.state) && (widget.state.status === "ready" || widget.state.status === "stale");
|
|
3531
|
+
}
|
|
3532
|
+
function recordValue(value, path, errors) {
|
|
3533
|
+
if (!isRecord5(value)) {
|
|
3534
|
+
errors.push(`${path} must be an object`);
|
|
3535
|
+
return false;
|
|
3536
|
+
}
|
|
3537
|
+
return true;
|
|
3538
|
+
}
|
|
3539
|
+
function arrayValue(value, path, errors, requireOne = false) {
|
|
3540
|
+
if (!Array.isArray(value)) {
|
|
3541
|
+
errors.push(`${path} must be an array`);
|
|
3542
|
+
return false;
|
|
3543
|
+
}
|
|
3544
|
+
if (requireOne && value.length === 0) errors.push(`${path} must contain at least one item`);
|
|
3545
|
+
return true;
|
|
3546
|
+
}
|
|
3547
|
+
function stringArray2(value, path, errors, requireOne = false) {
|
|
3548
|
+
if (!arrayValue(value, path, errors, requireOne)) return;
|
|
3549
|
+
value.forEach((item, index) => nonEmptyString(item, `${path}[${index}]`, errors));
|
|
3550
|
+
}
|
|
3551
|
+
function enumArray(value, allowed, path, errors, requireOne) {
|
|
3552
|
+
if (!arrayValue(value, path, errors, requireOne)) return;
|
|
3553
|
+
value.forEach((item, index) => enumValue(item, allowed, `${path}[${index}]`, errors));
|
|
3554
|
+
if (new Set(value).size !== value.length) errors.push(`${path} must not contain duplicates`);
|
|
3555
|
+
}
|
|
3556
|
+
function nonEmptyString(value, path, errors) {
|
|
3557
|
+
if (typeof value !== "string" || value.trim().length === 0) errors.push(`${path} must be a non-empty string`);
|
|
3558
|
+
}
|
|
3559
|
+
function optionalNonEmptyString(value, path, errors) {
|
|
3560
|
+
if (value !== void 0) nonEmptyString(value, path, errors);
|
|
3561
|
+
}
|
|
3562
|
+
function patternString(value, pattern, path, errors) {
|
|
3563
|
+
if (typeof value !== "string" || !pattern.test(value)) errors.push(`${path} must be a semantic version`);
|
|
3564
|
+
}
|
|
3565
|
+
function enumValue(value, allowed, path, errors) {
|
|
3566
|
+
if (typeof value !== "string" || !allowed.includes(value)) errors.push(`${path} must be one of: ${allowed.join(", ")}`);
|
|
3567
|
+
}
|
|
3568
|
+
function exactValue(value, expected, path, errors) {
|
|
3569
|
+
if (value !== expected) errors.push(`${path} must be ${expected}`);
|
|
3570
|
+
}
|
|
3571
|
+
function booleanValue(value, path, errors) {
|
|
3572
|
+
if (typeof value !== "boolean") errors.push(`${path} must be a boolean`);
|
|
3573
|
+
}
|
|
3574
|
+
function finiteNumber(value, path, errors) {
|
|
3575
|
+
if (typeof value !== "number" || !Number.isFinite(value)) errors.push(`${path} must be a finite number`);
|
|
3576
|
+
}
|
|
3577
|
+
function optionalFiniteNumber(value, path, errors) {
|
|
3578
|
+
if (value !== void 0) finiteNumber(value, path, errors);
|
|
3579
|
+
}
|
|
3580
|
+
function positiveInteger(value, path, errors) {
|
|
3581
|
+
if (!Number.isInteger(value) || Number(value) <= 0) errors.push(`${path} must be a positive integer`);
|
|
3582
|
+
}
|
|
3583
|
+
function nonNegativeInteger(value, path, errors) {
|
|
3584
|
+
if (!Number.isInteger(value) || Number(value) < 0) errors.push(`${path} must be a non-negative integer`);
|
|
3585
|
+
}
|
|
3586
|
+
function isoTimestamp(value, path, errors) {
|
|
3587
|
+
if (!isIsoTimestamp(value)) errors.push(`${path} must be an ISO timestamp`);
|
|
3588
|
+
}
|
|
3589
|
+
function optionalIsoTimestamp(value, path, errors) {
|
|
3590
|
+
if (value !== void 0) isoTimestamp(value, path, errors);
|
|
3591
|
+
}
|
|
3592
|
+
function isIsoTimestamp(value) {
|
|
3593
|
+
return typeof value === "string" && value.trim().length > 0 && Number.isFinite(Date.parse(value));
|
|
3594
|
+
}
|
|
3595
|
+
function isPositiveNumber(value) {
|
|
3596
|
+
return typeof value === "number" && value > 0;
|
|
3597
|
+
}
|
|
3598
|
+
function isRecord5(value) {
|
|
3599
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3600
|
+
}
|
|
3601
|
+
|
|
3222
3602
|
// src/widgets.ts
|
|
3223
3603
|
var TOPOLO_WIDGET_ENDPOINT = "/api/widget";
|
|
3224
3604
|
var TOPOLO_WIDGET_REFRESH_INTERVAL_MS = 6e4;
|
|
3605
|
+
var TOPOLO_WIDGET_CONTRACT_VERSION = "2.0.0";
|
|
3225
3606
|
function defineTopoloStatsWidget(widget) {
|
|
3226
3607
|
return { ...widget, type: "stats" };
|
|
3227
3608
|
}
|
|
3228
|
-
function
|
|
3229
|
-
return { ...widget, type: "
|
|
3609
|
+
function defineTopoloChartWidget(widget) {
|
|
3610
|
+
return { ...widget, type: "chart" };
|
|
3230
3611
|
}
|
|
3231
|
-
function
|
|
3232
|
-
return { ...widget, type: "
|
|
3612
|
+
function defineTopoloActivityWidget(widget) {
|
|
3613
|
+
return { ...widget, type: "activity" };
|
|
3233
3614
|
}
|
|
3234
3615
|
function defineTopoloNotificationsWidget(widget) {
|
|
3235
3616
|
return { ...widget, type: "notifications" };
|
|
3236
3617
|
}
|
|
3237
|
-
function
|
|
3238
|
-
return { ...widget, type: "
|
|
3618
|
+
function defineTopoloActionsWidget(widget) {
|
|
3619
|
+
return { ...widget, type: "actions" };
|
|
3239
3620
|
}
|
|
3240
3621
|
function createTopoloWidgetResponse({
|
|
3241
3622
|
appId,
|
|
@@ -3243,136 +3624,59 @@ function createTopoloWidgetResponse({
|
|
|
3243
3624
|
widgets,
|
|
3244
3625
|
now
|
|
3245
3626
|
}) {
|
|
3246
|
-
const
|
|
3247
|
-
|
|
3627
|
+
const generatedAt = toIsoTimestamp(now);
|
|
3628
|
+
const response = {
|
|
3248
3629
|
success: true,
|
|
3630
|
+
contractVersion: TOPOLO_WIDGET_CONTRACT_VERSION,
|
|
3631
|
+
appId,
|
|
3632
|
+
serviceName,
|
|
3633
|
+
generatedAt,
|
|
3249
3634
|
widgets: widgets.map((widget) => ({
|
|
3250
3635
|
...widget,
|
|
3251
3636
|
appId: widget.appId?.trim() || appId,
|
|
3252
3637
|
serviceName: widget.serviceName?.trim() || serviceName,
|
|
3253
|
-
lastUpdated: widget.lastUpdated?.trim() ||
|
|
3638
|
+
lastUpdated: widget.lastUpdated?.trim() || generatedAt
|
|
3254
3639
|
}))
|
|
3255
3640
|
};
|
|
3641
|
+
assertValidTopoloWidgetResponse(response);
|
|
3642
|
+
return response;
|
|
3256
3643
|
}
|
|
3257
|
-
function createTopoloWidgetError(
|
|
3258
|
-
|
|
3644
|
+
function createTopoloWidgetError({
|
|
3645
|
+
appId,
|
|
3646
|
+
serviceName,
|
|
3647
|
+
code,
|
|
3648
|
+
message,
|
|
3649
|
+
retryable,
|
|
3650
|
+
retryAfter,
|
|
3651
|
+
now
|
|
3652
|
+
}) {
|
|
3653
|
+
const response = {
|
|
3259
3654
|
success: false,
|
|
3655
|
+
contractVersion: TOPOLO_WIDGET_CONTRACT_VERSION,
|
|
3656
|
+
appId,
|
|
3657
|
+
serviceName,
|
|
3658
|
+
generatedAt: toIsoTimestamp(now),
|
|
3260
3659
|
widgets: [],
|
|
3261
|
-
error
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
if (!isRecord5(response)) {
|
|
3267
|
-
return { ok: false, errors: ["response must be an object"] };
|
|
3268
|
-
}
|
|
3269
|
-
if (typeof response.success !== "boolean") {
|
|
3270
|
-
errors.push("success must be a boolean");
|
|
3271
|
-
}
|
|
3272
|
-
if (!Array.isArray(response.widgets)) {
|
|
3273
|
-
errors.push("widgets must be an array");
|
|
3274
|
-
return { ok: errors.length === 0, errors };
|
|
3275
|
-
}
|
|
3276
|
-
response.widgets.forEach((widget, index) => {
|
|
3277
|
-
validateWidget(widget, `widgets[${index}]`, errors);
|
|
3278
|
-
});
|
|
3279
|
-
if (response.success === false && response.error !== void 0 && typeof response.error !== "string") {
|
|
3280
|
-
errors.push("error must be a string when provided");
|
|
3281
|
-
}
|
|
3282
|
-
return { ok: errors.length === 0, errors };
|
|
3283
|
-
}
|
|
3284
|
-
function validateWidget(widget, path, errors) {
|
|
3285
|
-
if (!isRecord5(widget)) {
|
|
3286
|
-
errors.push(`${path} must be an object`);
|
|
3287
|
-
return;
|
|
3288
|
-
}
|
|
3289
|
-
validateNonEmptyString(widget.appId, `${path}.appId`, errors);
|
|
3290
|
-
validateNonEmptyString(widget.serviceName, `${path}.serviceName`, errors);
|
|
3291
|
-
validateNonEmptyString(widget.lastUpdated, `${path}.lastUpdated`, errors);
|
|
3292
|
-
validateSnapshotMetadata(widget.snapshot, `${path}.snapshot`, errors);
|
|
3293
|
-
switch (widget.type) {
|
|
3294
|
-
case "stats":
|
|
3295
|
-
validateArray2(widget.stats, `${path}.stats`, errors);
|
|
3296
|
-
break;
|
|
3297
|
-
case "recent-activity":
|
|
3298
|
-
validateArray2(widget.activities, `${path}.activities`, errors);
|
|
3299
|
-
break;
|
|
3300
|
-
case "quick-actions":
|
|
3301
|
-
validateArray2(widget.actions, `${path}.actions`, errors);
|
|
3302
|
-
break;
|
|
3303
|
-
case "notifications":
|
|
3304
|
-
validateArray2(widget.notifications, `${path}.notifications`, errors);
|
|
3305
|
-
if (typeof widget.unreadCount !== "number") {
|
|
3306
|
-
errors.push(`${path}.unreadCount must be a number`);
|
|
3307
|
-
}
|
|
3308
|
-
break;
|
|
3309
|
-
case "chart":
|
|
3310
|
-
validateNonEmptyString(widget.chartType, `${path}.chartType`, errors);
|
|
3311
|
-
validateNonEmptyString(widget.title, `${path}.title`, errors);
|
|
3312
|
-
if (!isRecord5(widget.data)) {
|
|
3313
|
-
errors.push(`${path}.data must be an object`);
|
|
3314
|
-
} else {
|
|
3315
|
-
validateArray2(widget.data.labels, `${path}.data.labels`, errors);
|
|
3316
|
-
validateArray2(widget.data.values, `${path}.data.values`, errors);
|
|
3317
|
-
}
|
|
3318
|
-
break;
|
|
3319
|
-
default:
|
|
3320
|
-
errors.push(`${path}.type must be a supported Topolo widget type`);
|
|
3321
|
-
}
|
|
3322
|
-
}
|
|
3323
|
-
function validateSnapshotMetadata(snapshot, path, errors) {
|
|
3324
|
-
if (snapshot === void 0) {
|
|
3325
|
-
errors.push(`${path} must be an object`);
|
|
3326
|
-
return;
|
|
3327
|
-
}
|
|
3328
|
-
if (!isRecord5(snapshot)) {
|
|
3329
|
-
errors.push(`${path} must be an object`);
|
|
3330
|
-
return;
|
|
3331
|
-
}
|
|
3332
|
-
validateNonEmptyString(snapshot.summary, `${path}.summary`, errors);
|
|
3333
|
-
if (snapshot.defaultMetricKeys !== void 0 && !Array.isArray(snapshot.defaultMetricKeys)) {
|
|
3334
|
-
errors.push(`${path}.defaultMetricKeys must be an array when provided`);
|
|
3335
|
-
}
|
|
3336
|
-
if (snapshot.highlights !== void 0 && !Array.isArray(snapshot.highlights)) {
|
|
3337
|
-
errors.push(`${path}.highlights must be an array when provided`);
|
|
3338
|
-
}
|
|
3339
|
-
if (snapshot.availableMetrics === void 0) {
|
|
3340
|
-
return;
|
|
3341
|
-
}
|
|
3342
|
-
if (!Array.isArray(snapshot.availableMetrics)) {
|
|
3343
|
-
errors.push(`${path}.availableMetrics must be an array when provided`);
|
|
3344
|
-
return;
|
|
3345
|
-
}
|
|
3346
|
-
snapshot.availableMetrics.forEach((metric, index) => {
|
|
3347
|
-
const metricPath = `${path}.availableMetrics[${index}]`;
|
|
3348
|
-
if (!isRecord5(metric)) {
|
|
3349
|
-
errors.push(`${metricPath} must be an object`);
|
|
3350
|
-
return;
|
|
3351
|
-
}
|
|
3352
|
-
validateNonEmptyString(metric.key, `${metricPath}.key`, errors);
|
|
3353
|
-
validateNonEmptyString(metric.label, `${metricPath}.label`, errors);
|
|
3354
|
-
if (typeof metric.value !== "number" || Number.isNaN(metric.value)) {
|
|
3355
|
-
errors.push(`${metricPath}.value must be a number`);
|
|
3660
|
+
error: {
|
|
3661
|
+
code,
|
|
3662
|
+
message,
|
|
3663
|
+
retryable,
|
|
3664
|
+
...retryAfter === void 0 ? {} : { retryAfter: toIsoTimestamp(retryAfter) }
|
|
3356
3665
|
}
|
|
3357
|
-
}
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3666
|
+
};
|
|
3667
|
+
assertValidTopoloWidgetResponse(response);
|
|
3668
|
+
return response;
|
|
3669
|
+
}
|
|
3670
|
+
function assertValidTopoloWidgetResponse(response) {
|
|
3671
|
+
const validation = validateTopoloWidgetResponse(response);
|
|
3672
|
+
if (!validation.ok) {
|
|
3673
|
+
throw new TypeError(
|
|
3674
|
+
`Invalid Topolo widget contract: ${validation.errors.join("; ")}`
|
|
3675
|
+
);
|
|
3367
3676
|
}
|
|
3368
3677
|
}
|
|
3369
|
-
function isRecord5(value) {
|
|
3370
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3371
|
-
}
|
|
3372
3678
|
function toIsoTimestamp(input) {
|
|
3373
|
-
if (input instanceof Date)
|
|
3374
|
-
return input.toISOString();
|
|
3375
|
-
}
|
|
3679
|
+
if (input instanceof Date) return input.toISOString();
|
|
3376
3680
|
return input ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
3377
3681
|
}
|
|
3378
3682
|
|
|
@@ -3394,6 +3698,7 @@ export {
|
|
|
3394
3698
|
P2PModule,
|
|
3395
3699
|
TOPOLO_ENVIRONMENT_SERVICE_URLS,
|
|
3396
3700
|
TOPOLO_RUNTIME_ENVIRONMENTS,
|
|
3701
|
+
TOPOLO_WIDGET_CONTRACT_VERSION,
|
|
3397
3702
|
TOPOLO_WIDGET_ENDPOINT,
|
|
3398
3703
|
TOPOLO_WIDGET_REFRESH_INTERVAL_MS,
|
|
3399
3704
|
TopoloAuthError,
|
|
@@ -3420,10 +3725,10 @@ export {
|
|
|
3420
3725
|
createTopoloAgentExecutionContext,
|
|
3421
3726
|
createTopoloWidgetError,
|
|
3422
3727
|
createTopoloWidgetResponse,
|
|
3728
|
+
defineTopoloActionsWidget,
|
|
3729
|
+
defineTopoloActivityWidget,
|
|
3423
3730
|
defineTopoloChartWidget,
|
|
3424
3731
|
defineTopoloNotificationsWidget,
|
|
3425
|
-
defineTopoloQuickActionsWidget,
|
|
3426
|
-
defineTopoloRecentActivityWidget,
|
|
3427
3732
|
defineTopoloStatsWidget,
|
|
3428
3733
|
digestTopoloActionCatalog,
|
|
3429
3734
|
encodeTopoloActionBase64,
|