@topolo/sdk 0.11.11 → 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/dist/index.js CHANGED
@@ -3219,23 +3219,385 @@ var TopoloOAuth = class {
3219
3219
  }
3220
3220
  };
3221
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
+
3222
3583
  // src/widgets.ts
3223
3584
  var TOPOLO_WIDGET_ENDPOINT = "/api/widget";
3224
3585
  var TOPOLO_WIDGET_REFRESH_INTERVAL_MS = 6e4;
3586
+ var TOPOLO_WIDGET_CONTRACT_VERSION = "2.0.0";
3225
3587
  function defineTopoloStatsWidget(widget) {
3226
3588
  return { ...widget, type: "stats" };
3227
3589
  }
3228
- function defineTopoloRecentActivityWidget(widget) {
3229
- return { ...widget, type: "recent-activity" };
3590
+ function defineTopoloChartWidget(widget) {
3591
+ return { ...widget, type: "chart" };
3230
3592
  }
3231
- function defineTopoloQuickActionsWidget(widget) {
3232
- return { ...widget, type: "quick-actions" };
3593
+ function defineTopoloActivityWidget(widget) {
3594
+ return { ...widget, type: "activity" };
3233
3595
  }
3234
3596
  function defineTopoloNotificationsWidget(widget) {
3235
3597
  return { ...widget, type: "notifications" };
3236
3598
  }
3237
- function defineTopoloChartWidget(widget) {
3238
- return { ...widget, type: "chart" };
3599
+ function defineTopoloActionsWidget(widget) {
3600
+ return { ...widget, type: "actions" };
3239
3601
  }
3240
3602
  function createTopoloWidgetResponse({
3241
3603
  appId,
@@ -3243,136 +3605,59 @@ function createTopoloWidgetResponse({
3243
3605
  widgets,
3244
3606
  now
3245
3607
  }) {
3246
- const lastUpdated = toIsoTimestamp(now);
3247
- return {
3608
+ const generatedAt = toIsoTimestamp(now);
3609
+ const response = {
3248
3610
  success: true,
3611
+ contractVersion: TOPOLO_WIDGET_CONTRACT_VERSION,
3612
+ appId,
3613
+ serviceName,
3614
+ generatedAt,
3249
3615
  widgets: widgets.map((widget) => ({
3250
3616
  ...widget,
3251
3617
  appId: widget.appId?.trim() || appId,
3252
3618
  serviceName: widget.serviceName?.trim() || serviceName,
3253
- lastUpdated: widget.lastUpdated?.trim() || lastUpdated
3619
+ lastUpdated: widget.lastUpdated?.trim() || generatedAt
3254
3620
  }))
3255
3621
  };
3622
+ assertValidTopoloWidgetResponse(response);
3623
+ return response;
3256
3624
  }
3257
- function createTopoloWidgetError(error) {
3258
- return {
3625
+ function createTopoloWidgetError({
3626
+ appId,
3627
+ serviceName,
3628
+ code,
3629
+ message,
3630
+ retryable,
3631
+ retryAfter,
3632
+ now
3633
+ }) {
3634
+ const response = {
3259
3635
  success: false,
3636
+ contractVersion: TOPOLO_WIDGET_CONTRACT_VERSION,
3637
+ appId,
3638
+ serviceName,
3639
+ generatedAt: toIsoTimestamp(now),
3260
3640
  widgets: [],
3261
- error
3262
- };
3263
- }
3264
- function validateTopoloWidgetResponse(response) {
3265
- const errors = [];
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;
3641
+ error: {
3642
+ code,
3643
+ message,
3644
+ retryable,
3645
+ ...retryAfter === void 0 ? {} : { retryAfter: toIsoTimestamp(retryAfter) }
3351
3646
  }
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`);
3356
- }
3357
- });
3358
- }
3359
- function validateNonEmptyString(value, path, errors) {
3360
- if (typeof value !== "string" || value.trim().length === 0) {
3361
- errors.push(`${path} must be a non-empty string`);
3362
- }
3363
- }
3364
- function validateArray2(value, path, errors) {
3365
- if (!Array.isArray(value)) {
3366
- errors.push(`${path} must be an array`);
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
+ );
3367
3657
  }
3368
3658
  }
3369
- function isRecord5(value) {
3370
- return typeof value === "object" && value !== null && !Array.isArray(value);
3371
- }
3372
3659
  function toIsoTimestamp(input) {
3373
- if (input instanceof Date) {
3374
- return input.toISOString();
3375
- }
3660
+ if (input instanceof Date) return input.toISOString();
3376
3661
  return input ?? (/* @__PURE__ */ new Date()).toISOString();
3377
3662
  }
3378
3663
 
@@ -3394,6 +3679,7 @@ export {
3394
3679
  P2PModule,
3395
3680
  TOPOLO_ENVIRONMENT_SERVICE_URLS,
3396
3681
  TOPOLO_RUNTIME_ENVIRONMENTS,
3682
+ TOPOLO_WIDGET_CONTRACT_VERSION,
3397
3683
  TOPOLO_WIDGET_ENDPOINT,
3398
3684
  TOPOLO_WIDGET_REFRESH_INTERVAL_MS,
3399
3685
  TopoloAuthError,
@@ -3420,10 +3706,10 @@ export {
3420
3706
  createTopoloAgentExecutionContext,
3421
3707
  createTopoloWidgetError,
3422
3708
  createTopoloWidgetResponse,
3709
+ defineTopoloActionsWidget,
3710
+ defineTopoloActivityWidget,
3423
3711
  defineTopoloChartWidget,
3424
3712
  defineTopoloNotificationsWidget,
3425
- defineTopoloQuickActionsWidget,
3426
- defineTopoloRecentActivityWidget,
3427
3713
  defineTopoloStatsWidget,
3428
3714
  digestTopoloActionCatalog,
3429
3715
  encodeTopoloActionBase64,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@topolo/sdk",
3
- "version": "0.11.11",
4
- "topoloPublishedVersion": "0.11.11",
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": "4ecfb08c915533c17b3a4e0831241e1e223fd4ead89b60e5be40b749f5f624ee"
53
+ "digest": "2bb3a53e5241cde11d06fe2bd355cac5cd9141b1b5ad5f71ebc2d621c1c33fb0"
54
54
  }
55
55
  }
56
56
  ]