@topolo/sdk 0.11.10 → 0.12.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 +449 -127
- package/dist/index.d.cts +184 -61
- package/dist/index.d.ts +184 -61
- package/dist/index.js +446 -125
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1674,7 +1674,9 @@ var TopoloClient = class _TopoloClient {
|
|
|
1674
1674
|
};
|
|
1675
1675
|
}
|
|
1676
1676
|
planResolvedAction(entry, input, options) {
|
|
1677
|
-
const
|
|
1677
|
+
const inputValidation = validateTopoloActionInput(entry.inputSchema, input);
|
|
1678
|
+
const resourceIssue = validateActionResource(entry, options.resource);
|
|
1679
|
+
const validation = resourceIssue ? { valid: false, errors: [...inputValidation.errors, resourceIssue] } : inputValidation;
|
|
1678
1680
|
const agentAccess = entry.agentAccess ?? (entry.destructive ? "off" : entry.readOnly && !entry.requiresConfirmation ? "auto" : "confirm");
|
|
1679
1681
|
const confirmationRequired = agentAccess === "confirm" || entry.requiresConfirmation || entry.destructive;
|
|
1680
1682
|
let request = null;
|
|
@@ -1733,6 +1735,13 @@ var TopoloClient = class _TopoloClient {
|
|
|
1733
1735
|
`Topolo action "${entry.actionId}" input is invalid: ${validation.errors.map((issue) => `${issue.path} ${issue.message}`).join("; ")}`
|
|
1734
1736
|
);
|
|
1735
1737
|
}
|
|
1738
|
+
const resourceIssue = validateActionResource(entry, options.resource);
|
|
1739
|
+
if (resourceIssue) {
|
|
1740
|
+
throw new TopoloSdkError(
|
|
1741
|
+
"invalid_action_resource",
|
|
1742
|
+
`Topolo action "${entry.actionId}" resource context is invalid: ${resourceIssue.message}`
|
|
1743
|
+
);
|
|
1744
|
+
}
|
|
1736
1745
|
const mapped = mapActionInput(entry, input);
|
|
1737
1746
|
const service = entry.appSlug || entry.appId;
|
|
1738
1747
|
const result = await this.request({
|
|
@@ -1795,6 +1804,32 @@ var TopoloClient = class _TopoloClient {
|
|
|
1795
1804
|
};
|
|
1796
1805
|
}
|
|
1797
1806
|
};
|
|
1807
|
+
function validateActionResource(entry, resource) {
|
|
1808
|
+
const permissionName = entry.permissionName || entry.requiredPermission.split(".").at(-1) || "";
|
|
1809
|
+
if (!permissionName.startsWith("workspace:")) return null;
|
|
1810
|
+
if (!resource) {
|
|
1811
|
+
return {
|
|
1812
|
+
path: "$.resource",
|
|
1813
|
+
code: "required",
|
|
1814
|
+
message: "Select a workspace with the application resource catalog before planning or calling this action."
|
|
1815
|
+
};
|
|
1816
|
+
}
|
|
1817
|
+
if (resource.resourceType !== "workspace") {
|
|
1818
|
+
return {
|
|
1819
|
+
path: "$.resource.resourceType",
|
|
1820
|
+
code: "const",
|
|
1821
|
+
message: 'This action requires resourceType "workspace".'
|
|
1822
|
+
};
|
|
1823
|
+
}
|
|
1824
|
+
if (!resource.resourceId.trim()) {
|
|
1825
|
+
return {
|
|
1826
|
+
path: "$.resource.resourceId",
|
|
1827
|
+
code: "required",
|
|
1828
|
+
message: "This action requires a workspace resourceId."
|
|
1829
|
+
};
|
|
1830
|
+
}
|
|
1831
|
+
return null;
|
|
1832
|
+
}
|
|
1798
1833
|
function resolveConfiguredAppId(slug, appIds) {
|
|
1799
1834
|
for (const serviceKey of serviceKeyCandidates2(slug)) {
|
|
1800
1835
|
const configured = appIds?.[serviceKey];
|
|
@@ -3184,23 +3219,385 @@ var TopoloOAuth = class {
|
|
|
3184
3219
|
}
|
|
3185
3220
|
};
|
|
3186
3221
|
|
|
3222
|
+
// src/widget-validation.ts
|
|
3223
|
+
var CONTRACT_VERSION = "2.0.0";
|
|
3224
|
+
var WIDGET_TYPES = ["stats", "chart", "activity", "notifications", "actions"];
|
|
3225
|
+
var CONTEXTS = ["personal", "organization", "workspace", "resource"];
|
|
3226
|
+
var SIZES = ["compact", "medium", "wide", "full"];
|
|
3227
|
+
var STATES = ["ready", "empty", "unavailable", "stale", "error"];
|
|
3228
|
+
var FIELD_TYPES = ["string", "number", "boolean", "select", "multi-select", "date-range"];
|
|
3229
|
+
var SEMANTICS = ["gauge", "counter", "rate", "duration", "currency", "percentage"];
|
|
3230
|
+
var AGGREGATIONS = ["last", "sum", "average", "minimum", "maximum", "count", "distinct_count"];
|
|
3231
|
+
var CHART_TYPES = ["line", "bar", "area", "pie", "donut"];
|
|
3232
|
+
var SEVERITIES = ["info", "warning", "error", "success"];
|
|
3233
|
+
var RECOVERY_MODES = ["not_applicable", "retry", "compensating_action"];
|
|
3234
|
+
var SEMVER = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
3235
|
+
function validateTopoloWidgetResponse(response) {
|
|
3236
|
+
const errors = [];
|
|
3237
|
+
if (!isRecord5(response)) return { ok: false, errors: ["response must be an object"] };
|
|
3238
|
+
exactValue(response.contractVersion, CONTRACT_VERSION, "contractVersion", errors);
|
|
3239
|
+
nonEmptyString(response.appId, "appId", errors);
|
|
3240
|
+
nonEmptyString(response.serviceName, "serviceName", errors);
|
|
3241
|
+
isoTimestamp(response.generatedAt, "generatedAt", errors);
|
|
3242
|
+
booleanValue(response.success, "success", errors);
|
|
3243
|
+
arrayValue(response.widgets, "widgets", errors);
|
|
3244
|
+
if (response.success === true) {
|
|
3245
|
+
if (Array.isArray(response.widgets) && response.widgets.length === 0) {
|
|
3246
|
+
errors.push("widgets must contain at least one complete widget; use widget state empty for intentional no-data");
|
|
3247
|
+
}
|
|
3248
|
+
if (response.error !== void 0) errors.push("error must be omitted when success is true");
|
|
3249
|
+
} else if (response.success === false) {
|
|
3250
|
+
if (Array.isArray(response.widgets) && response.widgets.length !== 0) {
|
|
3251
|
+
errors.push("widgets must be empty when success is false");
|
|
3252
|
+
}
|
|
3253
|
+
validateApiError(response.error, "error", errors);
|
|
3254
|
+
}
|
|
3255
|
+
if (Array.isArray(response.widgets)) {
|
|
3256
|
+
response.widgets.forEach((widget, index) => validateWidget(widget, `widgets[${index}]`, errors));
|
|
3257
|
+
}
|
|
3258
|
+
return { ok: errors.length === 0, errors };
|
|
3259
|
+
}
|
|
3260
|
+
function validateApiError(value, path, errors) {
|
|
3261
|
+
if (!recordValue(value, path, errors)) return;
|
|
3262
|
+
nonEmptyString(value.code, `${path}.code`, errors);
|
|
3263
|
+
nonEmptyString(value.message, `${path}.message`, errors);
|
|
3264
|
+
booleanValue(value.retryable, `${path}.retryable`, errors);
|
|
3265
|
+
optionalIsoTimestamp(value.retryAfter, `${path}.retryAfter`, errors);
|
|
3266
|
+
}
|
|
3267
|
+
function validateWidget(value, path, errors) {
|
|
3268
|
+
if (!recordValue(value, path, errors)) return;
|
|
3269
|
+
nonEmptyString(value.id, `${path}.id`, errors);
|
|
3270
|
+
patternString(value.version, SEMVER, `${path}.version`, errors);
|
|
3271
|
+
nonEmptyString(value.appId, `${path}.appId`, errors);
|
|
3272
|
+
nonEmptyString(value.serviceName, `${path}.serviceName`, errors);
|
|
3273
|
+
enumValue(value.type, WIDGET_TYPES, `${path}.type`, errors);
|
|
3274
|
+
isoTimestamp(value.lastUpdated, `${path}.lastUpdated`, errors);
|
|
3275
|
+
validatePresentation(value.presentation, `${path}.presentation`, errors);
|
|
3276
|
+
validateResource(value.resource, `${path}.resource`, errors);
|
|
3277
|
+
validateState(value.state, `${path}.state`, errors);
|
|
3278
|
+
validateRefresh(value.refresh, `${path}.refresh`, errors);
|
|
3279
|
+
validateConfiguration(value.configuration, `${path}.configuration`, errors);
|
|
3280
|
+
validateDrilldowns(value.drilldowns, `${path}.drilldowns`, errors);
|
|
3281
|
+
validateActions(value.controls, `${path}.controls`, errors);
|
|
3282
|
+
validateLocalization(value.localization, `${path}.localization`, errors);
|
|
3283
|
+
validateAccessibility(value.accessibility, `${path}.accessibility`, errors);
|
|
3284
|
+
switch (value.type) {
|
|
3285
|
+
case "stats":
|
|
3286
|
+
validateMetrics(value.metrics, `${path}.metrics`, errors, requiresContent(value));
|
|
3287
|
+
break;
|
|
3288
|
+
case "chart":
|
|
3289
|
+
enumValue(value.chartType, CHART_TYPES, `${path}.chartType`, errors);
|
|
3290
|
+
validateSeriesCollection(value.series, `${path}.series`, errors, requiresContent(value));
|
|
3291
|
+
break;
|
|
3292
|
+
case "activity":
|
|
3293
|
+
validateActivities(value.activities, `${path}.activities`, errors, requiresContent(value));
|
|
3294
|
+
break;
|
|
3295
|
+
case "notifications":
|
|
3296
|
+
validateNotifications(value.notifications, `${path}.notifications`, errors, requiresContent(value));
|
|
3297
|
+
nonNegativeInteger(value.unreadCount, `${path}.unreadCount`, errors);
|
|
3298
|
+
break;
|
|
3299
|
+
case "actions":
|
|
3300
|
+
validateActions(value.actions, `${path}.actions`, errors, requiresContent(value));
|
|
3301
|
+
break;
|
|
3302
|
+
}
|
|
3303
|
+
}
|
|
3304
|
+
function validatePresentation(value, path, errors) {
|
|
3305
|
+
if (!recordValue(value, path, errors)) return;
|
|
3306
|
+
nonEmptyString(value.title, `${path}.title`, errors);
|
|
3307
|
+
optionalNonEmptyString(value.description, `${path}.description`, errors);
|
|
3308
|
+
enumArray(value.contexts, CONTEXTS, `${path}.contexts`, errors, true);
|
|
3309
|
+
enumArray(value.supportedSizes, SIZES, `${path}.supportedSizes`, errors, true);
|
|
3310
|
+
enumValue(value.defaultSize, SIZES, `${path}.defaultSize`, errors);
|
|
3311
|
+
if (typeof value.defaultSize === "string" && Array.isArray(value.supportedSizes) && !value.supportedSizes.includes(value.defaultSize)) {
|
|
3312
|
+
errors.push(`${path}.defaultSize must be included in supportedSizes`);
|
|
3313
|
+
}
|
|
3314
|
+
}
|
|
3315
|
+
function validateResource(value, path, errors) {
|
|
3316
|
+
if (!recordValue(value, path, errors)) return;
|
|
3317
|
+
nonEmptyString(value.resourceType, `${path}.resourceType`, errors);
|
|
3318
|
+
optionalNonEmptyString(value.resourceId, `${path}.resourceId`, errors);
|
|
3319
|
+
stringArray2(value.requiredPermissions, `${path}.requiredPermissions`, errors, true);
|
|
3320
|
+
}
|
|
3321
|
+
function validateState(value, path, errors) {
|
|
3322
|
+
if (!recordValue(value, path, errors)) return;
|
|
3323
|
+
enumValue(value.status, STATES, `${path}.status`, errors);
|
|
3324
|
+
isoTimestamp(value.observedAt, `${path}.observedAt`, errors);
|
|
3325
|
+
optionalIsoTimestamp(value.lastSuccessfulAt, `${path}.lastSuccessfulAt`, errors);
|
|
3326
|
+
optionalNonEmptyString(value.code, `${path}.code`, errors);
|
|
3327
|
+
optionalNonEmptyString(value.message, `${path}.message`, errors);
|
|
3328
|
+
if (["empty", "unavailable", "error"].includes(String(value.status))) {
|
|
3329
|
+
nonEmptyString(value.message, `${path}.message`, errors);
|
|
3330
|
+
}
|
|
3331
|
+
if (["unavailable", "error"].includes(String(value.status))) {
|
|
3332
|
+
nonEmptyString(value.code, `${path}.code`, errors);
|
|
3333
|
+
}
|
|
3334
|
+
if (value.status === "stale") isoTimestamp(value.lastSuccessfulAt, `${path}.lastSuccessfulAt`, errors);
|
|
3335
|
+
}
|
|
3336
|
+
function validateRefresh(value, path, errors) {
|
|
3337
|
+
if (!recordValue(value, path, errors)) return;
|
|
3338
|
+
positiveInteger(value.intervalSeconds, `${path}.intervalSeconds`, errors);
|
|
3339
|
+
positiveInteger(value.timeoutSeconds, `${path}.timeoutSeconds`, errors);
|
|
3340
|
+
positiveInteger(value.maxAgeSeconds, `${path}.maxAgeSeconds`, errors);
|
|
3341
|
+
nonNegativeInteger(value.staleWhileRevalidateSeconds, `${path}.staleWhileRevalidateSeconds`, errors);
|
|
3342
|
+
if (isPositiveNumber(value.timeoutSeconds) && isPositiveNumber(value.intervalSeconds) && value.timeoutSeconds > value.intervalSeconds) {
|
|
3343
|
+
errors.push(`${path}.timeoutSeconds must not exceed intervalSeconds`);
|
|
3344
|
+
}
|
|
3345
|
+
}
|
|
3346
|
+
function validateConfiguration(value, path, errors) {
|
|
3347
|
+
if (!recordValue(value, path, errors)) return;
|
|
3348
|
+
booleanValue(value.allowMultipleInstances, `${path}.allowMultipleInstances`, errors);
|
|
3349
|
+
recordValue(value.defaults, `${path}.defaults`, errors);
|
|
3350
|
+
if (!arrayValue(value.fields, `${path}.fields`, errors)) return;
|
|
3351
|
+
value.fields.forEach((field, index) => {
|
|
3352
|
+
const fieldPath = `${path}.fields[${index}]`;
|
|
3353
|
+
if (!recordValue(field, fieldPath, errors)) return;
|
|
3354
|
+
nonEmptyString(field.key, `${fieldPath}.key`, errors);
|
|
3355
|
+
nonEmptyString(field.label, `${fieldPath}.label`, errors);
|
|
3356
|
+
enumValue(field.type, FIELD_TYPES, `${fieldPath}.type`, errors);
|
|
3357
|
+
booleanValue(field.required, `${fieldPath}.required`, errors);
|
|
3358
|
+
if (field.type === "select" || field.type === "multi-select") {
|
|
3359
|
+
if (!arrayValue(field.options, `${fieldPath}.options`, errors, true)) return;
|
|
3360
|
+
field.options.forEach((option, optionIndex) => {
|
|
3361
|
+
const optionPath = `${fieldPath}.options[${optionIndex}]`;
|
|
3362
|
+
if (!recordValue(option, optionPath, errors)) return;
|
|
3363
|
+
nonEmptyString(option.value, `${optionPath}.value`, errors);
|
|
3364
|
+
nonEmptyString(option.label, `${optionPath}.label`, errors);
|
|
3365
|
+
});
|
|
3366
|
+
} else if (field.options !== void 0) {
|
|
3367
|
+
errors.push(`${fieldPath}.options is only valid for select fields`);
|
|
3368
|
+
}
|
|
3369
|
+
});
|
|
3370
|
+
}
|
|
3371
|
+
function validateDrilldowns(value, path, errors) {
|
|
3372
|
+
if (!arrayValue(value, path, errors)) return;
|
|
3373
|
+
value.forEach((drilldown, index) => {
|
|
3374
|
+
const itemPath = `${path}[${index}]`;
|
|
3375
|
+
if (!recordValue(drilldown, itemPath, errors)) return;
|
|
3376
|
+
nonEmptyString(drilldown.id, `${itemPath}.id`, errors);
|
|
3377
|
+
nonEmptyString(drilldown.label, `${itemPath}.label`, errors);
|
|
3378
|
+
nonEmptyString(drilldown.href, `${itemPath}.href`, errors);
|
|
3379
|
+
});
|
|
3380
|
+
}
|
|
3381
|
+
function validateActions(value, path, errors, requireOne = false) {
|
|
3382
|
+
if (!arrayValue(value, path, errors, requireOne)) return;
|
|
3383
|
+
value.forEach((action, index) => {
|
|
3384
|
+
const itemPath = `${path}[${index}]`;
|
|
3385
|
+
if (!recordValue(action, itemPath, errors)) return;
|
|
3386
|
+
nonEmptyString(action.id, `${itemPath}.id`, errors);
|
|
3387
|
+
nonEmptyString(action.label, `${itemPath}.label`, errors);
|
|
3388
|
+
nonEmptyString(action.actionId, `${itemPath}.actionId`, errors);
|
|
3389
|
+
nonEmptyString(action.resourceType, `${itemPath}.resourceType`, errors);
|
|
3390
|
+
optionalNonEmptyString(action.resourceId, `${itemPath}.resourceId`, errors);
|
|
3391
|
+
recordValue(action.input, `${itemPath}.input`, errors);
|
|
3392
|
+
booleanValue(action.requiresConfirmation, `${itemPath}.requiresConfirmation`, errors);
|
|
3393
|
+
booleanValue(action.destructive, `${itemPath}.destructive`, errors);
|
|
3394
|
+
stringArray2(action.effects, `${itemPath}.effects`, errors, true);
|
|
3395
|
+
stringArray2(action.verification, `${itemPath}.verification`, errors, true);
|
|
3396
|
+
validateRecovery(action.recovery, `${itemPath}.recovery`, errors);
|
|
3397
|
+
});
|
|
3398
|
+
}
|
|
3399
|
+
function validateRecovery(value, path, errors) {
|
|
3400
|
+
if (!recordValue(value, path, errors)) return;
|
|
3401
|
+
enumValue(value.mode, RECOVERY_MODES, `${path}.mode`, errors);
|
|
3402
|
+
optionalNonEmptyString(value.actionId, `${path}.actionId`, errors);
|
|
3403
|
+
nonEmptyString(value.instructions, `${path}.instructions`, errors);
|
|
3404
|
+
if (value.mode === "compensating_action" && value.actionId === void 0) {
|
|
3405
|
+
errors.push(`${path}.actionId is required for compensating_action`);
|
|
3406
|
+
}
|
|
3407
|
+
}
|
|
3408
|
+
function validateLocalization(value, path, errors) {
|
|
3409
|
+
if (!recordValue(value, path, errors)) return;
|
|
3410
|
+
nonEmptyString(value.locale, `${path}.locale`, errors);
|
|
3411
|
+
nonEmptyString(value.titleKey, `${path}.titleKey`, errors);
|
|
3412
|
+
optionalNonEmptyString(value.descriptionKey, `${path}.descriptionKey`, errors);
|
|
3413
|
+
}
|
|
3414
|
+
function validateAccessibility(value, path, errors) {
|
|
3415
|
+
if (!recordValue(value, path, errors)) return;
|
|
3416
|
+
nonEmptyString(value.label, `${path}.label`, errors);
|
|
3417
|
+
optionalNonEmptyString(value.description, `${path}.description`, errors);
|
|
3418
|
+
}
|
|
3419
|
+
function validateMetrics(value, path, errors, requireOne) {
|
|
3420
|
+
if (!arrayValue(value, path, errors, requireOne)) return;
|
|
3421
|
+
value.forEach((metric, index) => {
|
|
3422
|
+
const itemPath = `${path}[${index}]`;
|
|
3423
|
+
if (!recordValue(metric, itemPath, errors)) return;
|
|
3424
|
+
nonEmptyString(metric.key, `${itemPath}.key`, errors);
|
|
3425
|
+
nonEmptyString(metric.label, `${itemPath}.label`, errors);
|
|
3426
|
+
finiteNumber(metric.value, `${itemPath}.value`, errors);
|
|
3427
|
+
nonEmptyString(metric.unit, `${itemPath}.unit`, errors);
|
|
3428
|
+
enumValue(metric.semantic, SEMANTICS, `${itemPath}.semantic`, errors);
|
|
3429
|
+
enumValue(metric.aggregation, AGGREGATIONS, `${itemPath}.aggregation`, errors);
|
|
3430
|
+
validatePeriod(metric.period, `${itemPath}.period`, errors);
|
|
3431
|
+
validateDimensions(metric.dimensions, `${itemPath}.dimensions`, errors);
|
|
3432
|
+
if (metric.comparison !== void 0) validateComparison(metric.comparison, `${itemPath}.comparison`, errors);
|
|
3433
|
+
if (metric.series !== void 0) validateSeries(metric.series, `${itemPath}.series`, errors);
|
|
3434
|
+
});
|
|
3435
|
+
}
|
|
3436
|
+
function validatePeriod(value, path, errors) {
|
|
3437
|
+
if (!recordValue(value, path, errors)) return;
|
|
3438
|
+
isoTimestamp(value.start, `${path}.start`, errors);
|
|
3439
|
+
isoTimestamp(value.end, `${path}.end`, errors);
|
|
3440
|
+
nonEmptyString(value.timezone, `${path}.timezone`, errors);
|
|
3441
|
+
if (isIsoTimestamp(value.start) && isIsoTimestamp(value.end) && Date.parse(value.start) > Date.parse(value.end)) {
|
|
3442
|
+
errors.push(`${path}.start must not be after end`);
|
|
3443
|
+
}
|
|
3444
|
+
}
|
|
3445
|
+
function validateDimensions(value, path, errors) {
|
|
3446
|
+
if (!arrayValue(value, path, errors)) return;
|
|
3447
|
+
value.forEach((dimension, index) => {
|
|
3448
|
+
const itemPath = `${path}[${index}]`;
|
|
3449
|
+
if (!recordValue(dimension, itemPath, errors)) return;
|
|
3450
|
+
nonEmptyString(dimension.key, `${itemPath}.key`, errors);
|
|
3451
|
+
nonEmptyString(dimension.label, `${itemPath}.label`, errors);
|
|
3452
|
+
nonEmptyString(dimension.value, `${itemPath}.value`, errors);
|
|
3453
|
+
});
|
|
3454
|
+
}
|
|
3455
|
+
function validateComparison(value, path, errors) {
|
|
3456
|
+
if (!recordValue(value, path, errors)) return;
|
|
3457
|
+
finiteNumber(value.value, `${path}.value`, errors);
|
|
3458
|
+
finiteNumber(value.absoluteChange, `${path}.absoluteChange`, errors);
|
|
3459
|
+
optionalFiniteNumber(value.percentageChange, `${path}.percentageChange`, errors);
|
|
3460
|
+
enumValue(value.direction, ["up", "down", "flat"], `${path}.direction`, errors);
|
|
3461
|
+
nonEmptyString(value.label, `${path}.label`, errors);
|
|
3462
|
+
validatePeriod(value.period, `${path}.period`, errors);
|
|
3463
|
+
}
|
|
3464
|
+
function validateSeriesCollection(value, path, errors, requireOne) {
|
|
3465
|
+
if (!arrayValue(value, path, errors, requireOne)) return;
|
|
3466
|
+
value.forEach((series, index) => validateSeries(series, `${path}[${index}]`, errors));
|
|
3467
|
+
}
|
|
3468
|
+
function validateSeries(value, path, errors) {
|
|
3469
|
+
if (!recordValue(value, path, errors)) return;
|
|
3470
|
+
nonEmptyString(value.id, `${path}.id`, errors);
|
|
3471
|
+
nonEmptyString(value.label, `${path}.label`, errors);
|
|
3472
|
+
nonEmptyString(value.unit, `${path}.unit`, errors);
|
|
3473
|
+
if (!arrayValue(value.points, `${path}.points`, errors, true)) return;
|
|
3474
|
+
value.points.forEach((point, index) => {
|
|
3475
|
+
const itemPath = `${path}.points[${index}]`;
|
|
3476
|
+
if (!recordValue(point, itemPath, errors)) return;
|
|
3477
|
+
isoTimestamp(point.timestamp, `${itemPath}.timestamp`, errors);
|
|
3478
|
+
finiteNumber(point.value, `${itemPath}.value`, errors);
|
|
3479
|
+
validateDimensions(point.dimensions, `${itemPath}.dimensions`, errors);
|
|
3480
|
+
});
|
|
3481
|
+
}
|
|
3482
|
+
function validateActivities(value, path, errors, requireOne) {
|
|
3483
|
+
if (!arrayValue(value, path, errors, requireOne)) return;
|
|
3484
|
+
value.forEach((activity, index) => {
|
|
3485
|
+
const itemPath = `${path}[${index}]`;
|
|
3486
|
+
if (!recordValue(activity, itemPath, errors)) return;
|
|
3487
|
+
nonEmptyString(activity.id, `${itemPath}.id`, errors);
|
|
3488
|
+
nonEmptyString(activity.title, `${itemPath}.title`, errors);
|
|
3489
|
+
optionalNonEmptyString(activity.description, `${itemPath}.description`, errors);
|
|
3490
|
+
isoTimestamp(activity.timestamp, `${itemPath}.timestamp`, errors);
|
|
3491
|
+
optionalNonEmptyString(activity.drilldownId, `${itemPath}.drilldownId`, errors);
|
|
3492
|
+
optionalNonEmptyString(activity.actionId, `${itemPath}.actionId`, errors);
|
|
3493
|
+
});
|
|
3494
|
+
}
|
|
3495
|
+
function validateNotifications(value, path, errors, requireOne) {
|
|
3496
|
+
if (!arrayValue(value, path, errors, requireOne)) return;
|
|
3497
|
+
value.forEach((notification, index) => {
|
|
3498
|
+
const itemPath = `${path}[${index}]`;
|
|
3499
|
+
if (!recordValue(notification, itemPath, errors)) return;
|
|
3500
|
+
nonEmptyString(notification.id, `${itemPath}.id`, errors);
|
|
3501
|
+
nonEmptyString(notification.title, `${itemPath}.title`, errors);
|
|
3502
|
+
nonEmptyString(notification.message, `${itemPath}.message`, errors);
|
|
3503
|
+
enumValue(notification.severity, SEVERITIES, `${itemPath}.severity`, errors);
|
|
3504
|
+
isoTimestamp(notification.timestamp, `${itemPath}.timestamp`, errors);
|
|
3505
|
+
booleanValue(notification.read, `${itemPath}.read`, errors);
|
|
3506
|
+
optionalNonEmptyString(notification.drilldownId, `${itemPath}.drilldownId`, errors);
|
|
3507
|
+
optionalNonEmptyString(notification.actionId, `${itemPath}.actionId`, errors);
|
|
3508
|
+
});
|
|
3509
|
+
}
|
|
3510
|
+
function requiresContent(widget) {
|
|
3511
|
+
return isRecord5(widget.state) && (widget.state.status === "ready" || widget.state.status === "stale");
|
|
3512
|
+
}
|
|
3513
|
+
function recordValue(value, path, errors) {
|
|
3514
|
+
if (!isRecord5(value)) {
|
|
3515
|
+
errors.push(`${path} must be an object`);
|
|
3516
|
+
return false;
|
|
3517
|
+
}
|
|
3518
|
+
return true;
|
|
3519
|
+
}
|
|
3520
|
+
function arrayValue(value, path, errors, requireOne = false) {
|
|
3521
|
+
if (!Array.isArray(value)) {
|
|
3522
|
+
errors.push(`${path} must be an array`);
|
|
3523
|
+
return false;
|
|
3524
|
+
}
|
|
3525
|
+
if (requireOne && value.length === 0) errors.push(`${path} must contain at least one item`);
|
|
3526
|
+
return true;
|
|
3527
|
+
}
|
|
3528
|
+
function stringArray2(value, path, errors, requireOne = false) {
|
|
3529
|
+
if (!arrayValue(value, path, errors, requireOne)) return;
|
|
3530
|
+
value.forEach((item, index) => nonEmptyString(item, `${path}[${index}]`, errors));
|
|
3531
|
+
}
|
|
3532
|
+
function enumArray(value, allowed, path, errors, requireOne) {
|
|
3533
|
+
if (!arrayValue(value, path, errors, requireOne)) return;
|
|
3534
|
+
value.forEach((item, index) => enumValue(item, allowed, `${path}[${index}]`, errors));
|
|
3535
|
+
if (new Set(value).size !== value.length) errors.push(`${path} must not contain duplicates`);
|
|
3536
|
+
}
|
|
3537
|
+
function nonEmptyString(value, path, errors) {
|
|
3538
|
+
if (typeof value !== "string" || value.trim().length === 0) errors.push(`${path} must be a non-empty string`);
|
|
3539
|
+
}
|
|
3540
|
+
function optionalNonEmptyString(value, path, errors) {
|
|
3541
|
+
if (value !== void 0) nonEmptyString(value, path, errors);
|
|
3542
|
+
}
|
|
3543
|
+
function patternString(value, pattern, path, errors) {
|
|
3544
|
+
if (typeof value !== "string" || !pattern.test(value)) errors.push(`${path} must be a semantic version`);
|
|
3545
|
+
}
|
|
3546
|
+
function enumValue(value, allowed, path, errors) {
|
|
3547
|
+
if (typeof value !== "string" || !allowed.includes(value)) errors.push(`${path} must be one of: ${allowed.join(", ")}`);
|
|
3548
|
+
}
|
|
3549
|
+
function exactValue(value, expected, path, errors) {
|
|
3550
|
+
if (value !== expected) errors.push(`${path} must be ${expected}`);
|
|
3551
|
+
}
|
|
3552
|
+
function booleanValue(value, path, errors) {
|
|
3553
|
+
if (typeof value !== "boolean") errors.push(`${path} must be a boolean`);
|
|
3554
|
+
}
|
|
3555
|
+
function finiteNumber(value, path, errors) {
|
|
3556
|
+
if (typeof value !== "number" || !Number.isFinite(value)) errors.push(`${path} must be a finite number`);
|
|
3557
|
+
}
|
|
3558
|
+
function optionalFiniteNumber(value, path, errors) {
|
|
3559
|
+
if (value !== void 0) finiteNumber(value, path, errors);
|
|
3560
|
+
}
|
|
3561
|
+
function positiveInteger(value, path, errors) {
|
|
3562
|
+
if (!Number.isInteger(value) || Number(value) <= 0) errors.push(`${path} must be a positive integer`);
|
|
3563
|
+
}
|
|
3564
|
+
function nonNegativeInteger(value, path, errors) {
|
|
3565
|
+
if (!Number.isInteger(value) || Number(value) < 0) errors.push(`${path} must be a non-negative integer`);
|
|
3566
|
+
}
|
|
3567
|
+
function isoTimestamp(value, path, errors) {
|
|
3568
|
+
if (!isIsoTimestamp(value)) errors.push(`${path} must be an ISO timestamp`);
|
|
3569
|
+
}
|
|
3570
|
+
function optionalIsoTimestamp(value, path, errors) {
|
|
3571
|
+
if (value !== void 0) isoTimestamp(value, path, errors);
|
|
3572
|
+
}
|
|
3573
|
+
function isIsoTimestamp(value) {
|
|
3574
|
+
return typeof value === "string" && value.trim().length > 0 && Number.isFinite(Date.parse(value));
|
|
3575
|
+
}
|
|
3576
|
+
function isPositiveNumber(value) {
|
|
3577
|
+
return typeof value === "number" && value > 0;
|
|
3578
|
+
}
|
|
3579
|
+
function isRecord5(value) {
|
|
3580
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3581
|
+
}
|
|
3582
|
+
|
|
3187
3583
|
// src/widgets.ts
|
|
3188
3584
|
var TOPOLO_WIDGET_ENDPOINT = "/api/widget";
|
|
3189
3585
|
var TOPOLO_WIDGET_REFRESH_INTERVAL_MS = 6e4;
|
|
3586
|
+
var TOPOLO_WIDGET_CONTRACT_VERSION = "2.0.0";
|
|
3190
3587
|
function defineTopoloStatsWidget(widget) {
|
|
3191
3588
|
return { ...widget, type: "stats" };
|
|
3192
3589
|
}
|
|
3193
|
-
function
|
|
3194
|
-
return { ...widget, type: "
|
|
3590
|
+
function defineTopoloChartWidget(widget) {
|
|
3591
|
+
return { ...widget, type: "chart" };
|
|
3195
3592
|
}
|
|
3196
|
-
function
|
|
3197
|
-
return { ...widget, type: "
|
|
3593
|
+
function defineTopoloActivityWidget(widget) {
|
|
3594
|
+
return { ...widget, type: "activity" };
|
|
3198
3595
|
}
|
|
3199
3596
|
function defineTopoloNotificationsWidget(widget) {
|
|
3200
3597
|
return { ...widget, type: "notifications" };
|
|
3201
3598
|
}
|
|
3202
|
-
function
|
|
3203
|
-
return { ...widget, type: "
|
|
3599
|
+
function defineTopoloActionsWidget(widget) {
|
|
3600
|
+
return { ...widget, type: "actions" };
|
|
3204
3601
|
}
|
|
3205
3602
|
function createTopoloWidgetResponse({
|
|
3206
3603
|
appId,
|
|
@@ -3208,136 +3605,59 @@ function createTopoloWidgetResponse({
|
|
|
3208
3605
|
widgets,
|
|
3209
3606
|
now
|
|
3210
3607
|
}) {
|
|
3211
|
-
const
|
|
3212
|
-
|
|
3608
|
+
const generatedAt = toIsoTimestamp(now);
|
|
3609
|
+
const response = {
|
|
3213
3610
|
success: true,
|
|
3611
|
+
contractVersion: TOPOLO_WIDGET_CONTRACT_VERSION,
|
|
3612
|
+
appId,
|
|
3613
|
+
serviceName,
|
|
3614
|
+
generatedAt,
|
|
3214
3615
|
widgets: widgets.map((widget) => ({
|
|
3215
3616
|
...widget,
|
|
3216
3617
|
appId: widget.appId?.trim() || appId,
|
|
3217
3618
|
serviceName: widget.serviceName?.trim() || serviceName,
|
|
3218
|
-
lastUpdated: widget.lastUpdated?.trim() ||
|
|
3619
|
+
lastUpdated: widget.lastUpdated?.trim() || generatedAt
|
|
3219
3620
|
}))
|
|
3220
3621
|
};
|
|
3622
|
+
assertValidTopoloWidgetResponse(response);
|
|
3623
|
+
return response;
|
|
3221
3624
|
}
|
|
3222
|
-
function createTopoloWidgetError(
|
|
3223
|
-
|
|
3625
|
+
function createTopoloWidgetError({
|
|
3626
|
+
appId,
|
|
3627
|
+
serviceName,
|
|
3628
|
+
code,
|
|
3629
|
+
message,
|
|
3630
|
+
retryable,
|
|
3631
|
+
retryAfter,
|
|
3632
|
+
now
|
|
3633
|
+
}) {
|
|
3634
|
+
const response = {
|
|
3224
3635
|
success: false,
|
|
3636
|
+
contractVersion: TOPOLO_WIDGET_CONTRACT_VERSION,
|
|
3637
|
+
appId,
|
|
3638
|
+
serviceName,
|
|
3639
|
+
generatedAt: toIsoTimestamp(now),
|
|
3225
3640
|
widgets: [],
|
|
3226
|
-
error
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
if (!isRecord5(response)) {
|
|
3232
|
-
return { ok: false, errors: ["response must be an object"] };
|
|
3233
|
-
}
|
|
3234
|
-
if (typeof response.success !== "boolean") {
|
|
3235
|
-
errors.push("success must be a boolean");
|
|
3236
|
-
}
|
|
3237
|
-
if (!Array.isArray(response.widgets)) {
|
|
3238
|
-
errors.push("widgets must be an array");
|
|
3239
|
-
return { ok: errors.length === 0, errors };
|
|
3240
|
-
}
|
|
3241
|
-
response.widgets.forEach((widget, index) => {
|
|
3242
|
-
validateWidget(widget, `widgets[${index}]`, errors);
|
|
3243
|
-
});
|
|
3244
|
-
if (response.success === false && response.error !== void 0 && typeof response.error !== "string") {
|
|
3245
|
-
errors.push("error must be a string when provided");
|
|
3246
|
-
}
|
|
3247
|
-
return { ok: errors.length === 0, errors };
|
|
3248
|
-
}
|
|
3249
|
-
function validateWidget(widget, path, errors) {
|
|
3250
|
-
if (!isRecord5(widget)) {
|
|
3251
|
-
errors.push(`${path} must be an object`);
|
|
3252
|
-
return;
|
|
3253
|
-
}
|
|
3254
|
-
validateNonEmptyString(widget.appId, `${path}.appId`, errors);
|
|
3255
|
-
validateNonEmptyString(widget.serviceName, `${path}.serviceName`, errors);
|
|
3256
|
-
validateNonEmptyString(widget.lastUpdated, `${path}.lastUpdated`, errors);
|
|
3257
|
-
validateSnapshotMetadata(widget.snapshot, `${path}.snapshot`, errors);
|
|
3258
|
-
switch (widget.type) {
|
|
3259
|
-
case "stats":
|
|
3260
|
-
validateArray2(widget.stats, `${path}.stats`, errors);
|
|
3261
|
-
break;
|
|
3262
|
-
case "recent-activity":
|
|
3263
|
-
validateArray2(widget.activities, `${path}.activities`, errors);
|
|
3264
|
-
break;
|
|
3265
|
-
case "quick-actions":
|
|
3266
|
-
validateArray2(widget.actions, `${path}.actions`, errors);
|
|
3267
|
-
break;
|
|
3268
|
-
case "notifications":
|
|
3269
|
-
validateArray2(widget.notifications, `${path}.notifications`, errors);
|
|
3270
|
-
if (typeof widget.unreadCount !== "number") {
|
|
3271
|
-
errors.push(`${path}.unreadCount must be a number`);
|
|
3272
|
-
}
|
|
3273
|
-
break;
|
|
3274
|
-
case "chart":
|
|
3275
|
-
validateNonEmptyString(widget.chartType, `${path}.chartType`, errors);
|
|
3276
|
-
validateNonEmptyString(widget.title, `${path}.title`, errors);
|
|
3277
|
-
if (!isRecord5(widget.data)) {
|
|
3278
|
-
errors.push(`${path}.data must be an object`);
|
|
3279
|
-
} else {
|
|
3280
|
-
validateArray2(widget.data.labels, `${path}.data.labels`, errors);
|
|
3281
|
-
validateArray2(widget.data.values, `${path}.data.values`, errors);
|
|
3282
|
-
}
|
|
3283
|
-
break;
|
|
3284
|
-
default:
|
|
3285
|
-
errors.push(`${path}.type must be a supported Topolo widget type`);
|
|
3286
|
-
}
|
|
3287
|
-
}
|
|
3288
|
-
function validateSnapshotMetadata(snapshot, path, errors) {
|
|
3289
|
-
if (snapshot === void 0) {
|
|
3290
|
-
errors.push(`${path} must be an object`);
|
|
3291
|
-
return;
|
|
3292
|
-
}
|
|
3293
|
-
if (!isRecord5(snapshot)) {
|
|
3294
|
-
errors.push(`${path} must be an object`);
|
|
3295
|
-
return;
|
|
3296
|
-
}
|
|
3297
|
-
validateNonEmptyString(snapshot.summary, `${path}.summary`, errors);
|
|
3298
|
-
if (snapshot.defaultMetricKeys !== void 0 && !Array.isArray(snapshot.defaultMetricKeys)) {
|
|
3299
|
-
errors.push(`${path}.defaultMetricKeys must be an array when provided`);
|
|
3300
|
-
}
|
|
3301
|
-
if (snapshot.highlights !== void 0 && !Array.isArray(snapshot.highlights)) {
|
|
3302
|
-
errors.push(`${path}.highlights must be an array when provided`);
|
|
3303
|
-
}
|
|
3304
|
-
if (snapshot.availableMetrics === void 0) {
|
|
3305
|
-
return;
|
|
3306
|
-
}
|
|
3307
|
-
if (!Array.isArray(snapshot.availableMetrics)) {
|
|
3308
|
-
errors.push(`${path}.availableMetrics must be an array when provided`);
|
|
3309
|
-
return;
|
|
3310
|
-
}
|
|
3311
|
-
snapshot.availableMetrics.forEach((metric, index) => {
|
|
3312
|
-
const metricPath = `${path}.availableMetrics[${index}]`;
|
|
3313
|
-
if (!isRecord5(metric)) {
|
|
3314
|
-
errors.push(`${metricPath} must be an object`);
|
|
3315
|
-
return;
|
|
3316
|
-
}
|
|
3317
|
-
validateNonEmptyString(metric.key, `${metricPath}.key`, errors);
|
|
3318
|
-
validateNonEmptyString(metric.label, `${metricPath}.label`, errors);
|
|
3319
|
-
if (typeof metric.value !== "number" || Number.isNaN(metric.value)) {
|
|
3320
|
-
errors.push(`${metricPath}.value must be a number`);
|
|
3641
|
+
error: {
|
|
3642
|
+
code,
|
|
3643
|
+
message,
|
|
3644
|
+
retryable,
|
|
3645
|
+
...retryAfter === void 0 ? {} : { retryAfter: toIsoTimestamp(retryAfter) }
|
|
3321
3646
|
}
|
|
3322
|
-
}
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3647
|
+
};
|
|
3648
|
+
assertValidTopoloWidgetResponse(response);
|
|
3649
|
+
return response;
|
|
3650
|
+
}
|
|
3651
|
+
function assertValidTopoloWidgetResponse(response) {
|
|
3652
|
+
const validation = validateTopoloWidgetResponse(response);
|
|
3653
|
+
if (!validation.ok) {
|
|
3654
|
+
throw new TypeError(
|
|
3655
|
+
`Invalid Topolo widget contract: ${validation.errors.join("; ")}`
|
|
3656
|
+
);
|
|
3332
3657
|
}
|
|
3333
3658
|
}
|
|
3334
|
-
function isRecord5(value) {
|
|
3335
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3336
|
-
}
|
|
3337
3659
|
function toIsoTimestamp(input) {
|
|
3338
|
-
if (input instanceof Date)
|
|
3339
|
-
return input.toISOString();
|
|
3340
|
-
}
|
|
3660
|
+
if (input instanceof Date) return input.toISOString();
|
|
3341
3661
|
return input ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
3342
3662
|
}
|
|
3343
3663
|
|
|
@@ -3359,6 +3679,7 @@ export {
|
|
|
3359
3679
|
P2PModule,
|
|
3360
3680
|
TOPOLO_ENVIRONMENT_SERVICE_URLS,
|
|
3361
3681
|
TOPOLO_RUNTIME_ENVIRONMENTS,
|
|
3682
|
+
TOPOLO_WIDGET_CONTRACT_VERSION,
|
|
3362
3683
|
TOPOLO_WIDGET_ENDPOINT,
|
|
3363
3684
|
TOPOLO_WIDGET_REFRESH_INTERVAL_MS,
|
|
3364
3685
|
TopoloAuthError,
|
|
@@ -3385,10 +3706,10 @@ export {
|
|
|
3385
3706
|
createTopoloAgentExecutionContext,
|
|
3386
3707
|
createTopoloWidgetError,
|
|
3387
3708
|
createTopoloWidgetResponse,
|
|
3709
|
+
defineTopoloActionsWidget,
|
|
3710
|
+
defineTopoloActivityWidget,
|
|
3388
3711
|
defineTopoloChartWidget,
|
|
3389
3712
|
defineTopoloNotificationsWidget,
|
|
3390
|
-
defineTopoloQuickActionsWidget,
|
|
3391
|
-
defineTopoloRecentActivityWidget,
|
|
3392
3713
|
defineTopoloStatsWidget,
|
|
3393
3714
|
digestTopoloActionCatalog,
|
|
3394
3715
|
encodeTopoloActionBase64,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@topolo/sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"topoloPublishedVersion": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
|
+
"topoloPublishedVersion": "0.12.0",
|
|
5
5
|
"description": "Typed client SDK for the Topolo platform. Used by TopoloCli, TopoloMCP, and third-party agents.",
|
|
6
6
|
"license": "UNLICENSED",
|
|
7
7
|
"author": "Topolo",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"tsconfig.json",
|
|
51
51
|
"vitest.config.ts"
|
|
52
52
|
],
|
|
53
|
-
"digest": "
|
|
53
|
+
"digest": "2bb3a53e5241cde11d06fe2bd355cac5cd9141b1b5ad5f71ebc2d621c1c33fb0"
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
]
|