@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.cjs CHANGED
@@ -27,6 +27,7 @@ __export(index_exports, {
27
27
  P2PModule: () => P2PModule,
28
28
  TOPOLO_ENVIRONMENT_SERVICE_URLS: () => TOPOLO_ENVIRONMENT_SERVICE_URLS,
29
29
  TOPOLO_RUNTIME_ENVIRONMENTS: () => TOPOLO_RUNTIME_ENVIRONMENTS,
30
+ TOPOLO_WIDGET_CONTRACT_VERSION: () => TOPOLO_WIDGET_CONTRACT_VERSION,
30
31
  TOPOLO_WIDGET_ENDPOINT: () => TOPOLO_WIDGET_ENDPOINT,
31
32
  TOPOLO_WIDGET_REFRESH_INTERVAL_MS: () => TOPOLO_WIDGET_REFRESH_INTERVAL_MS,
32
33
  TopoloAuthError: () => TopoloAuthError,
@@ -53,10 +54,10 @@ __export(index_exports, {
53
54
  createTopoloAgentExecutionContext: () => createTopoloAgentExecutionContext,
54
55
  createTopoloWidgetError: () => createTopoloWidgetError,
55
56
  createTopoloWidgetResponse: () => createTopoloWidgetResponse,
57
+ defineTopoloActionsWidget: () => defineTopoloActionsWidget,
58
+ defineTopoloActivityWidget: () => defineTopoloActivityWidget,
56
59
  defineTopoloChartWidget: () => defineTopoloChartWidget,
57
60
  defineTopoloNotificationsWidget: () => defineTopoloNotificationsWidget,
58
- defineTopoloQuickActionsWidget: () => defineTopoloQuickActionsWidget,
59
- defineTopoloRecentActivityWidget: () => defineTopoloRecentActivityWidget,
60
61
  defineTopoloStatsWidget: () => defineTopoloStatsWidget,
61
62
  digestTopoloActionCatalog: () => digestTopoloActionCatalog,
62
63
  encodeTopoloActionBase64: () => encodeTopoloActionBase64,
@@ -3302,23 +3303,385 @@ var TopoloOAuth = class {
3302
3303
  }
3303
3304
  };
3304
3305
 
3306
+ // src/widget-validation.ts
3307
+ var CONTRACT_VERSION = "2.0.0";
3308
+ var WIDGET_TYPES = ["stats", "chart", "activity", "notifications", "actions"];
3309
+ var CONTEXTS = ["personal", "organization", "workspace", "resource"];
3310
+ var SIZES = ["compact", "medium", "wide", "full"];
3311
+ var STATES = ["ready", "empty", "unavailable", "stale", "error"];
3312
+ var FIELD_TYPES = ["string", "number", "boolean", "select", "multi-select", "date-range"];
3313
+ var SEMANTICS = ["gauge", "counter", "rate", "duration", "currency", "percentage"];
3314
+ var AGGREGATIONS = ["last", "sum", "average", "minimum", "maximum", "count", "distinct_count"];
3315
+ var CHART_TYPES = ["line", "bar", "area", "pie", "donut"];
3316
+ var SEVERITIES = ["info", "warning", "error", "success"];
3317
+ var RECOVERY_MODES = ["not_applicable", "retry", "compensating_action"];
3318
+ var SEMVER = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
3319
+ function validateTopoloWidgetResponse(response) {
3320
+ const errors = [];
3321
+ if (!isRecord5(response)) return { ok: false, errors: ["response must be an object"] };
3322
+ exactValue(response.contractVersion, CONTRACT_VERSION, "contractVersion", errors);
3323
+ nonEmptyString(response.appId, "appId", errors);
3324
+ nonEmptyString(response.serviceName, "serviceName", errors);
3325
+ isoTimestamp(response.generatedAt, "generatedAt", errors);
3326
+ booleanValue(response.success, "success", errors);
3327
+ arrayValue(response.widgets, "widgets", errors);
3328
+ if (response.success === true) {
3329
+ if (Array.isArray(response.widgets) && response.widgets.length === 0) {
3330
+ errors.push("widgets must contain at least one complete widget; use widget state empty for intentional no-data");
3331
+ }
3332
+ if (response.error !== void 0) errors.push("error must be omitted when success is true");
3333
+ } else if (response.success === false) {
3334
+ if (Array.isArray(response.widgets) && response.widgets.length !== 0) {
3335
+ errors.push("widgets must be empty when success is false");
3336
+ }
3337
+ validateApiError(response.error, "error", errors);
3338
+ }
3339
+ if (Array.isArray(response.widgets)) {
3340
+ response.widgets.forEach((widget, index) => validateWidget(widget, `widgets[${index}]`, errors));
3341
+ }
3342
+ return { ok: errors.length === 0, errors };
3343
+ }
3344
+ function validateApiError(value, path, errors) {
3345
+ if (!recordValue(value, path, errors)) return;
3346
+ nonEmptyString(value.code, `${path}.code`, errors);
3347
+ nonEmptyString(value.message, `${path}.message`, errors);
3348
+ booleanValue(value.retryable, `${path}.retryable`, errors);
3349
+ optionalIsoTimestamp(value.retryAfter, `${path}.retryAfter`, errors);
3350
+ }
3351
+ function validateWidget(value, path, errors) {
3352
+ if (!recordValue(value, path, errors)) return;
3353
+ nonEmptyString(value.id, `${path}.id`, errors);
3354
+ patternString(value.version, SEMVER, `${path}.version`, errors);
3355
+ nonEmptyString(value.appId, `${path}.appId`, errors);
3356
+ nonEmptyString(value.serviceName, `${path}.serviceName`, errors);
3357
+ enumValue(value.type, WIDGET_TYPES, `${path}.type`, errors);
3358
+ isoTimestamp(value.lastUpdated, `${path}.lastUpdated`, errors);
3359
+ validatePresentation(value.presentation, `${path}.presentation`, errors);
3360
+ validateResource(value.resource, `${path}.resource`, errors);
3361
+ validateState(value.state, `${path}.state`, errors);
3362
+ validateRefresh(value.refresh, `${path}.refresh`, errors);
3363
+ validateConfiguration(value.configuration, `${path}.configuration`, errors);
3364
+ validateDrilldowns(value.drilldowns, `${path}.drilldowns`, errors);
3365
+ validateActions(value.controls, `${path}.controls`, errors);
3366
+ validateLocalization(value.localization, `${path}.localization`, errors);
3367
+ validateAccessibility(value.accessibility, `${path}.accessibility`, errors);
3368
+ switch (value.type) {
3369
+ case "stats":
3370
+ validateMetrics(value.metrics, `${path}.metrics`, errors, requiresContent(value));
3371
+ break;
3372
+ case "chart":
3373
+ enumValue(value.chartType, CHART_TYPES, `${path}.chartType`, errors);
3374
+ validateSeriesCollection(value.series, `${path}.series`, errors, requiresContent(value));
3375
+ break;
3376
+ case "activity":
3377
+ validateActivities(value.activities, `${path}.activities`, errors, requiresContent(value));
3378
+ break;
3379
+ case "notifications":
3380
+ validateNotifications(value.notifications, `${path}.notifications`, errors, requiresContent(value));
3381
+ nonNegativeInteger(value.unreadCount, `${path}.unreadCount`, errors);
3382
+ break;
3383
+ case "actions":
3384
+ validateActions(value.actions, `${path}.actions`, errors, requiresContent(value));
3385
+ break;
3386
+ }
3387
+ }
3388
+ function validatePresentation(value, path, errors) {
3389
+ if (!recordValue(value, path, errors)) return;
3390
+ nonEmptyString(value.title, `${path}.title`, errors);
3391
+ optionalNonEmptyString(value.description, `${path}.description`, errors);
3392
+ enumArray(value.contexts, CONTEXTS, `${path}.contexts`, errors, true);
3393
+ enumArray(value.supportedSizes, SIZES, `${path}.supportedSizes`, errors, true);
3394
+ enumValue(value.defaultSize, SIZES, `${path}.defaultSize`, errors);
3395
+ if (typeof value.defaultSize === "string" && Array.isArray(value.supportedSizes) && !value.supportedSizes.includes(value.defaultSize)) {
3396
+ errors.push(`${path}.defaultSize must be included in supportedSizes`);
3397
+ }
3398
+ }
3399
+ function validateResource(value, path, errors) {
3400
+ if (!recordValue(value, path, errors)) return;
3401
+ nonEmptyString(value.resourceType, `${path}.resourceType`, errors);
3402
+ optionalNonEmptyString(value.resourceId, `${path}.resourceId`, errors);
3403
+ stringArray2(value.requiredPermissions, `${path}.requiredPermissions`, errors, true);
3404
+ }
3405
+ function validateState(value, path, errors) {
3406
+ if (!recordValue(value, path, errors)) return;
3407
+ enumValue(value.status, STATES, `${path}.status`, errors);
3408
+ isoTimestamp(value.observedAt, `${path}.observedAt`, errors);
3409
+ optionalIsoTimestamp(value.lastSuccessfulAt, `${path}.lastSuccessfulAt`, errors);
3410
+ optionalNonEmptyString(value.code, `${path}.code`, errors);
3411
+ optionalNonEmptyString(value.message, `${path}.message`, errors);
3412
+ if (["empty", "unavailable", "error"].includes(String(value.status))) {
3413
+ nonEmptyString(value.message, `${path}.message`, errors);
3414
+ }
3415
+ if (["unavailable", "error"].includes(String(value.status))) {
3416
+ nonEmptyString(value.code, `${path}.code`, errors);
3417
+ }
3418
+ if (value.status === "stale") isoTimestamp(value.lastSuccessfulAt, `${path}.lastSuccessfulAt`, errors);
3419
+ }
3420
+ function validateRefresh(value, path, errors) {
3421
+ if (!recordValue(value, path, errors)) return;
3422
+ positiveInteger(value.intervalSeconds, `${path}.intervalSeconds`, errors);
3423
+ positiveInteger(value.timeoutSeconds, `${path}.timeoutSeconds`, errors);
3424
+ positiveInteger(value.maxAgeSeconds, `${path}.maxAgeSeconds`, errors);
3425
+ nonNegativeInteger(value.staleWhileRevalidateSeconds, `${path}.staleWhileRevalidateSeconds`, errors);
3426
+ if (isPositiveNumber(value.timeoutSeconds) && isPositiveNumber(value.intervalSeconds) && value.timeoutSeconds > value.intervalSeconds) {
3427
+ errors.push(`${path}.timeoutSeconds must not exceed intervalSeconds`);
3428
+ }
3429
+ }
3430
+ function validateConfiguration(value, path, errors) {
3431
+ if (!recordValue(value, path, errors)) return;
3432
+ booleanValue(value.allowMultipleInstances, `${path}.allowMultipleInstances`, errors);
3433
+ recordValue(value.defaults, `${path}.defaults`, errors);
3434
+ if (!arrayValue(value.fields, `${path}.fields`, errors)) return;
3435
+ value.fields.forEach((field, index) => {
3436
+ const fieldPath = `${path}.fields[${index}]`;
3437
+ if (!recordValue(field, fieldPath, errors)) return;
3438
+ nonEmptyString(field.key, `${fieldPath}.key`, errors);
3439
+ nonEmptyString(field.label, `${fieldPath}.label`, errors);
3440
+ enumValue(field.type, FIELD_TYPES, `${fieldPath}.type`, errors);
3441
+ booleanValue(field.required, `${fieldPath}.required`, errors);
3442
+ if (field.type === "select" || field.type === "multi-select") {
3443
+ if (!arrayValue(field.options, `${fieldPath}.options`, errors, true)) return;
3444
+ field.options.forEach((option, optionIndex) => {
3445
+ const optionPath = `${fieldPath}.options[${optionIndex}]`;
3446
+ if (!recordValue(option, optionPath, errors)) return;
3447
+ nonEmptyString(option.value, `${optionPath}.value`, errors);
3448
+ nonEmptyString(option.label, `${optionPath}.label`, errors);
3449
+ });
3450
+ } else if (field.options !== void 0) {
3451
+ errors.push(`${fieldPath}.options is only valid for select fields`);
3452
+ }
3453
+ });
3454
+ }
3455
+ function validateDrilldowns(value, path, errors) {
3456
+ if (!arrayValue(value, path, errors)) return;
3457
+ value.forEach((drilldown, index) => {
3458
+ const itemPath = `${path}[${index}]`;
3459
+ if (!recordValue(drilldown, itemPath, errors)) return;
3460
+ nonEmptyString(drilldown.id, `${itemPath}.id`, errors);
3461
+ nonEmptyString(drilldown.label, `${itemPath}.label`, errors);
3462
+ nonEmptyString(drilldown.href, `${itemPath}.href`, errors);
3463
+ });
3464
+ }
3465
+ function validateActions(value, path, errors, requireOne = false) {
3466
+ if (!arrayValue(value, path, errors, requireOne)) return;
3467
+ value.forEach((action, index) => {
3468
+ const itemPath = `${path}[${index}]`;
3469
+ if (!recordValue(action, itemPath, errors)) return;
3470
+ nonEmptyString(action.id, `${itemPath}.id`, errors);
3471
+ nonEmptyString(action.label, `${itemPath}.label`, errors);
3472
+ nonEmptyString(action.actionId, `${itemPath}.actionId`, errors);
3473
+ nonEmptyString(action.resourceType, `${itemPath}.resourceType`, errors);
3474
+ optionalNonEmptyString(action.resourceId, `${itemPath}.resourceId`, errors);
3475
+ recordValue(action.input, `${itemPath}.input`, errors);
3476
+ booleanValue(action.requiresConfirmation, `${itemPath}.requiresConfirmation`, errors);
3477
+ booleanValue(action.destructive, `${itemPath}.destructive`, errors);
3478
+ stringArray2(action.effects, `${itemPath}.effects`, errors, true);
3479
+ stringArray2(action.verification, `${itemPath}.verification`, errors, true);
3480
+ validateRecovery(action.recovery, `${itemPath}.recovery`, errors);
3481
+ });
3482
+ }
3483
+ function validateRecovery(value, path, errors) {
3484
+ if (!recordValue(value, path, errors)) return;
3485
+ enumValue(value.mode, RECOVERY_MODES, `${path}.mode`, errors);
3486
+ optionalNonEmptyString(value.actionId, `${path}.actionId`, errors);
3487
+ nonEmptyString(value.instructions, `${path}.instructions`, errors);
3488
+ if (value.mode === "compensating_action" && value.actionId === void 0) {
3489
+ errors.push(`${path}.actionId is required for compensating_action`);
3490
+ }
3491
+ }
3492
+ function validateLocalization(value, path, errors) {
3493
+ if (!recordValue(value, path, errors)) return;
3494
+ nonEmptyString(value.locale, `${path}.locale`, errors);
3495
+ nonEmptyString(value.titleKey, `${path}.titleKey`, errors);
3496
+ optionalNonEmptyString(value.descriptionKey, `${path}.descriptionKey`, errors);
3497
+ }
3498
+ function validateAccessibility(value, path, errors) {
3499
+ if (!recordValue(value, path, errors)) return;
3500
+ nonEmptyString(value.label, `${path}.label`, errors);
3501
+ optionalNonEmptyString(value.description, `${path}.description`, errors);
3502
+ }
3503
+ function validateMetrics(value, path, errors, requireOne) {
3504
+ if (!arrayValue(value, path, errors, requireOne)) return;
3505
+ value.forEach((metric, index) => {
3506
+ const itemPath = `${path}[${index}]`;
3507
+ if (!recordValue(metric, itemPath, errors)) return;
3508
+ nonEmptyString(metric.key, `${itemPath}.key`, errors);
3509
+ nonEmptyString(metric.label, `${itemPath}.label`, errors);
3510
+ finiteNumber(metric.value, `${itemPath}.value`, errors);
3511
+ nonEmptyString(metric.unit, `${itemPath}.unit`, errors);
3512
+ enumValue(metric.semantic, SEMANTICS, `${itemPath}.semantic`, errors);
3513
+ enumValue(metric.aggregation, AGGREGATIONS, `${itemPath}.aggregation`, errors);
3514
+ validatePeriod(metric.period, `${itemPath}.period`, errors);
3515
+ validateDimensions(metric.dimensions, `${itemPath}.dimensions`, errors);
3516
+ if (metric.comparison !== void 0) validateComparison(metric.comparison, `${itemPath}.comparison`, errors);
3517
+ if (metric.series !== void 0) validateSeries(metric.series, `${itemPath}.series`, errors);
3518
+ });
3519
+ }
3520
+ function validatePeriod(value, path, errors) {
3521
+ if (!recordValue(value, path, errors)) return;
3522
+ isoTimestamp(value.start, `${path}.start`, errors);
3523
+ isoTimestamp(value.end, `${path}.end`, errors);
3524
+ nonEmptyString(value.timezone, `${path}.timezone`, errors);
3525
+ if (isIsoTimestamp(value.start) && isIsoTimestamp(value.end) && Date.parse(value.start) > Date.parse(value.end)) {
3526
+ errors.push(`${path}.start must not be after end`);
3527
+ }
3528
+ }
3529
+ function validateDimensions(value, path, errors) {
3530
+ if (!arrayValue(value, path, errors)) return;
3531
+ value.forEach((dimension, index) => {
3532
+ const itemPath = `${path}[${index}]`;
3533
+ if (!recordValue(dimension, itemPath, errors)) return;
3534
+ nonEmptyString(dimension.key, `${itemPath}.key`, errors);
3535
+ nonEmptyString(dimension.label, `${itemPath}.label`, errors);
3536
+ nonEmptyString(dimension.value, `${itemPath}.value`, errors);
3537
+ });
3538
+ }
3539
+ function validateComparison(value, path, errors) {
3540
+ if (!recordValue(value, path, errors)) return;
3541
+ finiteNumber(value.value, `${path}.value`, errors);
3542
+ finiteNumber(value.absoluteChange, `${path}.absoluteChange`, errors);
3543
+ optionalFiniteNumber(value.percentageChange, `${path}.percentageChange`, errors);
3544
+ enumValue(value.direction, ["up", "down", "flat"], `${path}.direction`, errors);
3545
+ nonEmptyString(value.label, `${path}.label`, errors);
3546
+ validatePeriod(value.period, `${path}.period`, errors);
3547
+ }
3548
+ function validateSeriesCollection(value, path, errors, requireOne) {
3549
+ if (!arrayValue(value, path, errors, requireOne)) return;
3550
+ value.forEach((series, index) => validateSeries(series, `${path}[${index}]`, errors));
3551
+ }
3552
+ function validateSeries(value, path, errors) {
3553
+ if (!recordValue(value, path, errors)) return;
3554
+ nonEmptyString(value.id, `${path}.id`, errors);
3555
+ nonEmptyString(value.label, `${path}.label`, errors);
3556
+ nonEmptyString(value.unit, `${path}.unit`, errors);
3557
+ if (!arrayValue(value.points, `${path}.points`, errors, true)) return;
3558
+ value.points.forEach((point, index) => {
3559
+ const itemPath = `${path}.points[${index}]`;
3560
+ if (!recordValue(point, itemPath, errors)) return;
3561
+ isoTimestamp(point.timestamp, `${itemPath}.timestamp`, errors);
3562
+ finiteNumber(point.value, `${itemPath}.value`, errors);
3563
+ validateDimensions(point.dimensions, `${itemPath}.dimensions`, errors);
3564
+ });
3565
+ }
3566
+ function validateActivities(value, path, errors, requireOne) {
3567
+ if (!arrayValue(value, path, errors, requireOne)) return;
3568
+ value.forEach((activity, index) => {
3569
+ const itemPath = `${path}[${index}]`;
3570
+ if (!recordValue(activity, itemPath, errors)) return;
3571
+ nonEmptyString(activity.id, `${itemPath}.id`, errors);
3572
+ nonEmptyString(activity.title, `${itemPath}.title`, errors);
3573
+ optionalNonEmptyString(activity.description, `${itemPath}.description`, errors);
3574
+ isoTimestamp(activity.timestamp, `${itemPath}.timestamp`, errors);
3575
+ optionalNonEmptyString(activity.drilldownId, `${itemPath}.drilldownId`, errors);
3576
+ optionalNonEmptyString(activity.actionId, `${itemPath}.actionId`, errors);
3577
+ });
3578
+ }
3579
+ function validateNotifications(value, path, errors, requireOne) {
3580
+ if (!arrayValue(value, path, errors, requireOne)) return;
3581
+ value.forEach((notification, index) => {
3582
+ const itemPath = `${path}[${index}]`;
3583
+ if (!recordValue(notification, itemPath, errors)) return;
3584
+ nonEmptyString(notification.id, `${itemPath}.id`, errors);
3585
+ nonEmptyString(notification.title, `${itemPath}.title`, errors);
3586
+ nonEmptyString(notification.message, `${itemPath}.message`, errors);
3587
+ enumValue(notification.severity, SEVERITIES, `${itemPath}.severity`, errors);
3588
+ isoTimestamp(notification.timestamp, `${itemPath}.timestamp`, errors);
3589
+ booleanValue(notification.read, `${itemPath}.read`, errors);
3590
+ optionalNonEmptyString(notification.drilldownId, `${itemPath}.drilldownId`, errors);
3591
+ optionalNonEmptyString(notification.actionId, `${itemPath}.actionId`, errors);
3592
+ });
3593
+ }
3594
+ function requiresContent(widget) {
3595
+ return isRecord5(widget.state) && (widget.state.status === "ready" || widget.state.status === "stale");
3596
+ }
3597
+ function recordValue(value, path, errors) {
3598
+ if (!isRecord5(value)) {
3599
+ errors.push(`${path} must be an object`);
3600
+ return false;
3601
+ }
3602
+ return true;
3603
+ }
3604
+ function arrayValue(value, path, errors, requireOne = false) {
3605
+ if (!Array.isArray(value)) {
3606
+ errors.push(`${path} must be an array`);
3607
+ return false;
3608
+ }
3609
+ if (requireOne && value.length === 0) errors.push(`${path} must contain at least one item`);
3610
+ return true;
3611
+ }
3612
+ function stringArray2(value, path, errors, requireOne = false) {
3613
+ if (!arrayValue(value, path, errors, requireOne)) return;
3614
+ value.forEach((item, index) => nonEmptyString(item, `${path}[${index}]`, errors));
3615
+ }
3616
+ function enumArray(value, allowed, path, errors, requireOne) {
3617
+ if (!arrayValue(value, path, errors, requireOne)) return;
3618
+ value.forEach((item, index) => enumValue(item, allowed, `${path}[${index}]`, errors));
3619
+ if (new Set(value).size !== value.length) errors.push(`${path} must not contain duplicates`);
3620
+ }
3621
+ function nonEmptyString(value, path, errors) {
3622
+ if (typeof value !== "string" || value.trim().length === 0) errors.push(`${path} must be a non-empty string`);
3623
+ }
3624
+ function optionalNonEmptyString(value, path, errors) {
3625
+ if (value !== void 0) nonEmptyString(value, path, errors);
3626
+ }
3627
+ function patternString(value, pattern, path, errors) {
3628
+ if (typeof value !== "string" || !pattern.test(value)) errors.push(`${path} must be a semantic version`);
3629
+ }
3630
+ function enumValue(value, allowed, path, errors) {
3631
+ if (typeof value !== "string" || !allowed.includes(value)) errors.push(`${path} must be one of: ${allowed.join(", ")}`);
3632
+ }
3633
+ function exactValue(value, expected, path, errors) {
3634
+ if (value !== expected) errors.push(`${path} must be ${expected}`);
3635
+ }
3636
+ function booleanValue(value, path, errors) {
3637
+ if (typeof value !== "boolean") errors.push(`${path} must be a boolean`);
3638
+ }
3639
+ function finiteNumber(value, path, errors) {
3640
+ if (typeof value !== "number" || !Number.isFinite(value)) errors.push(`${path} must be a finite number`);
3641
+ }
3642
+ function optionalFiniteNumber(value, path, errors) {
3643
+ if (value !== void 0) finiteNumber(value, path, errors);
3644
+ }
3645
+ function positiveInteger(value, path, errors) {
3646
+ if (!Number.isInteger(value) || Number(value) <= 0) errors.push(`${path} must be a positive integer`);
3647
+ }
3648
+ function nonNegativeInteger(value, path, errors) {
3649
+ if (!Number.isInteger(value) || Number(value) < 0) errors.push(`${path} must be a non-negative integer`);
3650
+ }
3651
+ function isoTimestamp(value, path, errors) {
3652
+ if (!isIsoTimestamp(value)) errors.push(`${path} must be an ISO timestamp`);
3653
+ }
3654
+ function optionalIsoTimestamp(value, path, errors) {
3655
+ if (value !== void 0) isoTimestamp(value, path, errors);
3656
+ }
3657
+ function isIsoTimestamp(value) {
3658
+ return typeof value === "string" && value.trim().length > 0 && Number.isFinite(Date.parse(value));
3659
+ }
3660
+ function isPositiveNumber(value) {
3661
+ return typeof value === "number" && value > 0;
3662
+ }
3663
+ function isRecord5(value) {
3664
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3665
+ }
3666
+
3305
3667
  // src/widgets.ts
3306
3668
  var TOPOLO_WIDGET_ENDPOINT = "/api/widget";
3307
3669
  var TOPOLO_WIDGET_REFRESH_INTERVAL_MS = 6e4;
3670
+ var TOPOLO_WIDGET_CONTRACT_VERSION = "2.0.0";
3308
3671
  function defineTopoloStatsWidget(widget) {
3309
3672
  return { ...widget, type: "stats" };
3310
3673
  }
3311
- function defineTopoloRecentActivityWidget(widget) {
3312
- return { ...widget, type: "recent-activity" };
3674
+ function defineTopoloChartWidget(widget) {
3675
+ return { ...widget, type: "chart" };
3313
3676
  }
3314
- function defineTopoloQuickActionsWidget(widget) {
3315
- return { ...widget, type: "quick-actions" };
3677
+ function defineTopoloActivityWidget(widget) {
3678
+ return { ...widget, type: "activity" };
3316
3679
  }
3317
3680
  function defineTopoloNotificationsWidget(widget) {
3318
3681
  return { ...widget, type: "notifications" };
3319
3682
  }
3320
- function defineTopoloChartWidget(widget) {
3321
- return { ...widget, type: "chart" };
3683
+ function defineTopoloActionsWidget(widget) {
3684
+ return { ...widget, type: "actions" };
3322
3685
  }
3323
3686
  function createTopoloWidgetResponse({
3324
3687
  appId,
@@ -3326,136 +3689,59 @@ function createTopoloWidgetResponse({
3326
3689
  widgets,
3327
3690
  now
3328
3691
  }) {
3329
- const lastUpdated = toIsoTimestamp(now);
3330
- return {
3692
+ const generatedAt = toIsoTimestamp(now);
3693
+ const response = {
3331
3694
  success: true,
3695
+ contractVersion: TOPOLO_WIDGET_CONTRACT_VERSION,
3696
+ appId,
3697
+ serviceName,
3698
+ generatedAt,
3332
3699
  widgets: widgets.map((widget) => ({
3333
3700
  ...widget,
3334
3701
  appId: widget.appId?.trim() || appId,
3335
3702
  serviceName: widget.serviceName?.trim() || serviceName,
3336
- lastUpdated: widget.lastUpdated?.trim() || lastUpdated
3703
+ lastUpdated: widget.lastUpdated?.trim() || generatedAt
3337
3704
  }))
3338
3705
  };
3706
+ assertValidTopoloWidgetResponse(response);
3707
+ return response;
3339
3708
  }
3340
- function createTopoloWidgetError(error) {
3341
- return {
3709
+ function createTopoloWidgetError({
3710
+ appId,
3711
+ serviceName,
3712
+ code,
3713
+ message,
3714
+ retryable,
3715
+ retryAfter,
3716
+ now
3717
+ }) {
3718
+ const response = {
3342
3719
  success: false,
3720
+ contractVersion: TOPOLO_WIDGET_CONTRACT_VERSION,
3721
+ appId,
3722
+ serviceName,
3723
+ generatedAt: toIsoTimestamp(now),
3343
3724
  widgets: [],
3344
- error
3345
- };
3346
- }
3347
- function validateTopoloWidgetResponse(response) {
3348
- const errors = [];
3349
- if (!isRecord5(response)) {
3350
- return { ok: false, errors: ["response must be an object"] };
3351
- }
3352
- if (typeof response.success !== "boolean") {
3353
- errors.push("success must be a boolean");
3354
- }
3355
- if (!Array.isArray(response.widgets)) {
3356
- errors.push("widgets must be an array");
3357
- return { ok: errors.length === 0, errors };
3358
- }
3359
- response.widgets.forEach((widget, index) => {
3360
- validateWidget(widget, `widgets[${index}]`, errors);
3361
- });
3362
- if (response.success === false && response.error !== void 0 && typeof response.error !== "string") {
3363
- errors.push("error must be a string when provided");
3364
- }
3365
- return { ok: errors.length === 0, errors };
3366
- }
3367
- function validateWidget(widget, path, errors) {
3368
- if (!isRecord5(widget)) {
3369
- errors.push(`${path} must be an object`);
3370
- return;
3371
- }
3372
- validateNonEmptyString(widget.appId, `${path}.appId`, errors);
3373
- validateNonEmptyString(widget.serviceName, `${path}.serviceName`, errors);
3374
- validateNonEmptyString(widget.lastUpdated, `${path}.lastUpdated`, errors);
3375
- validateSnapshotMetadata(widget.snapshot, `${path}.snapshot`, errors);
3376
- switch (widget.type) {
3377
- case "stats":
3378
- validateArray2(widget.stats, `${path}.stats`, errors);
3379
- break;
3380
- case "recent-activity":
3381
- validateArray2(widget.activities, `${path}.activities`, errors);
3382
- break;
3383
- case "quick-actions":
3384
- validateArray2(widget.actions, `${path}.actions`, errors);
3385
- break;
3386
- case "notifications":
3387
- validateArray2(widget.notifications, `${path}.notifications`, errors);
3388
- if (typeof widget.unreadCount !== "number") {
3389
- errors.push(`${path}.unreadCount must be a number`);
3390
- }
3391
- break;
3392
- case "chart":
3393
- validateNonEmptyString(widget.chartType, `${path}.chartType`, errors);
3394
- validateNonEmptyString(widget.title, `${path}.title`, errors);
3395
- if (!isRecord5(widget.data)) {
3396
- errors.push(`${path}.data must be an object`);
3397
- } else {
3398
- validateArray2(widget.data.labels, `${path}.data.labels`, errors);
3399
- validateArray2(widget.data.values, `${path}.data.values`, errors);
3400
- }
3401
- break;
3402
- default:
3403
- errors.push(`${path}.type must be a supported Topolo widget type`);
3404
- }
3405
- }
3406
- function validateSnapshotMetadata(snapshot, path, errors) {
3407
- if (snapshot === void 0) {
3408
- errors.push(`${path} must be an object`);
3409
- return;
3410
- }
3411
- if (!isRecord5(snapshot)) {
3412
- errors.push(`${path} must be an object`);
3413
- return;
3414
- }
3415
- validateNonEmptyString(snapshot.summary, `${path}.summary`, errors);
3416
- if (snapshot.defaultMetricKeys !== void 0 && !Array.isArray(snapshot.defaultMetricKeys)) {
3417
- errors.push(`${path}.defaultMetricKeys must be an array when provided`);
3418
- }
3419
- if (snapshot.highlights !== void 0 && !Array.isArray(snapshot.highlights)) {
3420
- errors.push(`${path}.highlights must be an array when provided`);
3421
- }
3422
- if (snapshot.availableMetrics === void 0) {
3423
- return;
3424
- }
3425
- if (!Array.isArray(snapshot.availableMetrics)) {
3426
- errors.push(`${path}.availableMetrics must be an array when provided`);
3427
- return;
3428
- }
3429
- snapshot.availableMetrics.forEach((metric, index) => {
3430
- const metricPath = `${path}.availableMetrics[${index}]`;
3431
- if (!isRecord5(metric)) {
3432
- errors.push(`${metricPath} must be an object`);
3433
- return;
3725
+ error: {
3726
+ code,
3727
+ message,
3728
+ retryable,
3729
+ ...retryAfter === void 0 ? {} : { retryAfter: toIsoTimestamp(retryAfter) }
3434
3730
  }
3435
- validateNonEmptyString(metric.key, `${metricPath}.key`, errors);
3436
- validateNonEmptyString(metric.label, `${metricPath}.label`, errors);
3437
- if (typeof metric.value !== "number" || Number.isNaN(metric.value)) {
3438
- errors.push(`${metricPath}.value must be a number`);
3439
- }
3440
- });
3441
- }
3442
- function validateNonEmptyString(value, path, errors) {
3443
- if (typeof value !== "string" || value.trim().length === 0) {
3444
- errors.push(`${path} must be a non-empty string`);
3445
- }
3446
- }
3447
- function validateArray2(value, path, errors) {
3448
- if (!Array.isArray(value)) {
3449
- errors.push(`${path} must be an array`);
3731
+ };
3732
+ assertValidTopoloWidgetResponse(response);
3733
+ return response;
3734
+ }
3735
+ function assertValidTopoloWidgetResponse(response) {
3736
+ const validation = validateTopoloWidgetResponse(response);
3737
+ if (!validation.ok) {
3738
+ throw new TypeError(
3739
+ `Invalid Topolo widget contract: ${validation.errors.join("; ")}`
3740
+ );
3450
3741
  }
3451
3742
  }
3452
- function isRecord5(value) {
3453
- return typeof value === "object" && value !== null && !Array.isArray(value);
3454
- }
3455
3743
  function toIsoTimestamp(input) {
3456
- if (input instanceof Date) {
3457
- return input.toISOString();
3458
- }
3744
+ if (input instanceof Date) return input.toISOString();
3459
3745
  return input ?? (/* @__PURE__ */ new Date()).toISOString();
3460
3746
  }
3461
3747
 
@@ -3478,6 +3764,7 @@ function createTopolo(options) {
3478
3764
  P2PModule,
3479
3765
  TOPOLO_ENVIRONMENT_SERVICE_URLS,
3480
3766
  TOPOLO_RUNTIME_ENVIRONMENTS,
3767
+ TOPOLO_WIDGET_CONTRACT_VERSION,
3481
3768
  TOPOLO_WIDGET_ENDPOINT,
3482
3769
  TOPOLO_WIDGET_REFRESH_INTERVAL_MS,
3483
3770
  TopoloAuthError,
@@ -3504,10 +3791,10 @@ function createTopolo(options) {
3504
3791
  createTopoloAgentExecutionContext,
3505
3792
  createTopoloWidgetError,
3506
3793
  createTopoloWidgetResponse,
3794
+ defineTopoloActionsWidget,
3795
+ defineTopoloActivityWidget,
3507
3796
  defineTopoloChartWidget,
3508
3797
  defineTopoloNotificationsWidget,
3509
- defineTopoloQuickActionsWidget,
3510
- defineTopoloRecentActivityWidget,
3511
3798
  defineTopoloStatsWidget,
3512
3799
  digestTopoloActionCatalog,
3513
3800
  encodeTopoloActionBase64,