@littlebearapps/platform-admin-sdk 1.5.0 → 2.1.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.
Files changed (157) hide show
  1. package/README.md +2 -2
  2. package/dist/templates.d.ts +1 -1
  3. package/dist/templates.js +197 -2
  4. package/package.json +1 -1
  5. package/templates/full/dashboard/src/components/patterns/ActivePatterns.tsx +62 -0
  6. package/templates/full/dashboard/src/components/patterns/PatternTabs.tsx +116 -0
  7. package/templates/full/dashboard/src/components/patterns/SystemPatterns.tsx +52 -0
  8. package/templates/full/dashboard/src/components/patterns/index.ts +3 -0
  9. package/templates/full/dashboard/src/components/reports/DigestStats.tsx +151 -0
  10. package/templates/full/dashboard/src/components/reports/GapDetectionReport.tsx +69 -0
  11. package/templates/full/dashboard/src/components/reports/HealthTrendsReport.tsx +192 -0
  12. package/templates/full/dashboard/src/components/reports/SdkAuditReport.tsx +72 -0
  13. package/templates/full/dashboard/src/components/reports/index.ts +2 -0
  14. package/templates/full/dashboard/src/components/usage/AIModelBreakdown.tsx +364 -0
  15. package/templates/full/dashboard/src/components/usage/unified/Recommendations.tsx +149 -0
  16. package/templates/full/dashboard/src/lib/cloudflare/alerting.ts +486 -0
  17. package/templates/full/dashboard/src/lib/cloudflare/graphql.ts +4785 -0
  18. package/templates/full/dashboard/src/lib/cloudflare/project-registry.ts +451 -0
  19. package/templates/full/dashboard/src/lib/notifications/api.ts +197 -0
  20. package/templates/full/dashboard/src/lib/notifications/types.ts.hbs +97 -0
  21. package/templates/full/dashboard/src/lib/patterns/api.ts +120 -0
  22. package/templates/full/dashboard/src/lib/patterns/types.ts +127 -0
  23. package/templates/full/dashboard/src/lib/reports/types.ts +231 -0
  24. package/templates/full/dashboard/src/lib/search/api.ts +258 -0
  25. package/templates/full/dashboard/src/lib/search/types.ts.hbs +115 -0
  26. package/templates/full/dashboard/src/lib/settings/api.ts.hbs +201 -0
  27. package/templates/full/dashboard/src/lib/settings/types.ts.hbs +104 -0
  28. package/templates/full/dashboard/src/lib/usage/allowance-config.ts.hbs +547 -0
  29. package/templates/full/dashboard/src/lib/usage/providers.ts +331 -0
  30. package/templates/full/dashboard/src/pages/api/notifications/[id]/read.ts +37 -0
  31. package/templates/full/dashboard/src/pages/api/notifications/read-all.ts +28 -0
  32. package/templates/full/dashboard/src/pages/api/patterns/cache-refresh.ts +38 -0
  33. package/templates/full/dashboard/src/pages/api/patterns/discover.ts +36 -0
  34. package/templates/full/dashboard/src/pages/api/patterns/ready-for-review.ts +39 -0
  35. package/templates/full/dashboard/src/pages/api/patterns/stats.ts +39 -0
  36. package/templates/full/dashboard/src/pages/api/patterns/suggestions.ts +43 -0
  37. package/templates/full/dashboard/src/pages/api/reports/audit.ts +45 -0
  38. package/templates/full/dashboard/src/pages/api/reports/usage.ts +52 -0
  39. package/templates/full/dashboard/src/pages/api/search/reindex.ts +28 -0
  40. package/templates/full/dashboard/src/pages/api/search/stats.ts +27 -0
  41. package/templates/full/dashboard/src/pages/api/settings/index.ts +37 -0
  42. package/templates/full/dashboard/src/pages/api/settings/update.ts +41 -0
  43. package/templates/full/dashboard/src/pages/api/topology/index.ts +56 -0
  44. package/templates/full/scripts/ops/universal-backfill.ts +147 -0
  45. package/templates/shared/.github/workflows/contract-check.yml.hbs +42 -0
  46. package/templates/shared/.github/workflows/dashboard-deploy.yml.hbs +39 -0
  47. package/templates/shared/.github/workflows/security.yml +33 -0
  48. package/templates/shared/dashboard/src/components/Nav.astro.hbs +2 -0
  49. package/templates/shared/dashboard/src/components/infrastructure/AlertHistory.tsx +57 -0
  50. package/templates/shared/dashboard/src/components/infrastructure/InfrastructureStats.tsx +73 -0
  51. package/templates/shared/dashboard/src/components/infrastructure/ServiceRegistry.tsx +55 -0
  52. package/templates/shared/dashboard/src/components/infrastructure/UptimeStatus.tsx +56 -0
  53. package/templates/shared/dashboard/src/components/infrastructure/index.ts +4 -0
  54. package/templates/shared/dashboard/src/components/reports/ReportInfoButton.tsx +98 -0
  55. package/templates/shared/dashboard/src/components/ui/Breadcrumbs.tsx +27 -0
  56. package/templates/shared/dashboard/src/components/ui/EmptyState.tsx +26 -0
  57. package/templates/shared/dashboard/src/components/ui/ErrorBoundary.tsx +42 -0
  58. package/templates/shared/dashboard/src/components/ui/LoadingSkeleton.tsx +18 -0
  59. package/templates/shared/dashboard/src/components/ui/PageShell.tsx +26 -0
  60. package/templates/shared/dashboard/src/components/ui/Toast.tsx +44 -0
  61. package/templates/shared/dashboard/src/components/ui/index.ts +6 -0
  62. package/templates/shared/dashboard/src/components/usage/AnomaliesWidget.tsx +68 -0
  63. package/templates/shared/dashboard/src/components/usage/HourlyUsageChart.tsx +55 -0
  64. package/templates/shared/dashboard/src/components/usage/PlanAllowanceDashboard.tsx +67 -0
  65. package/templates/shared/dashboard/src/components/usage/ProjectCostBreakdown.tsx +55 -0
  66. package/templates/shared/dashboard/src/components/usage/index.ts +4 -0
  67. package/templates/shared/dashboard/src/components/usage/react/DashboardShell.tsx +263 -0
  68. package/templates/shared/dashboard/src/components/usage/react/StatusBadge.tsx +77 -0
  69. package/templates/shared/dashboard/src/components/usage/react/UsageChart.tsx +391 -0
  70. package/templates/shared/dashboard/src/components/usage/react/index.ts.hbs +30 -0
  71. package/templates/shared/dashboard/src/components/usage/react/types.ts +137 -0
  72. package/templates/shared/dashboard/src/components/usage/transformers.ts +478 -0
  73. package/templates/shared/dashboard/src/components/usage/unified/AlertBanner.tsx +172 -0
  74. package/templates/shared/dashboard/src/components/usage/unified/HeroCardsRow.tsx +757 -0
  75. package/templates/shared/dashboard/src/components/usage/unified/LiveHeader.tsx +169 -0
  76. package/templates/shared/dashboard/src/components/usage/unified/ProjectsTable.tsx +448 -0
  77. package/templates/shared/dashboard/src/components/usage/unified/ResourceBreakdown.tsx +236 -0
  78. package/templates/shared/dashboard/src/components/usage/unified/Sparkline.tsx +127 -0
  79. package/templates/shared/dashboard/src/components/usage/unified/UnifiedShell.tsx +893 -0
  80. package/templates/shared/dashboard/src/components/usage/unified/index.ts.hbs +50 -0
  81. package/templates/shared/dashboard/src/components/usage/unified/types.ts +416 -0
  82. package/templates/shared/dashboard/src/lib/cloudflare/analytics.ts +310 -0
  83. package/templates/shared/dashboard/src/lib/cloudflare/costs.ts +21 -0
  84. package/templates/shared/dashboard/src/lib/cloudflare/d1.ts +55 -0
  85. package/templates/shared/dashboard/src/lib/cloudflare/index.ts.hbs +120 -0
  86. package/templates/shared/dashboard/src/lib/infrastructure/types.ts +116 -0
  87. package/templates/shared/dashboard/src/lib/usage/fetchWithDedup.ts +101 -0
  88. package/templates/shared/dashboard/src/lib/usage/index.ts.hbs +12 -0
  89. package/templates/shared/dashboard/src/pages/api/costs/overview.ts +65 -0
  90. package/templates/shared/dashboard/src/pages/api/costs/providers.ts +47 -0
  91. package/templates/shared/dashboard/src/pages/api/infrastructure/services.ts +55 -0
  92. package/templates/shared/dashboard/src/pages/api/infrastructure/stats.ts +99 -0
  93. package/templates/shared/dashboard/src/pages/api/usage/allowances.ts +56 -0
  94. package/templates/shared/dashboard/src/pages/api/usage/anomalies.ts +45 -0
  95. package/templates/shared/dashboard/src/pages/api/usage/billing.ts +53 -0
  96. package/templates/shared/dashboard/src/pages/api/usage/granular.ts +50 -0
  97. package/templates/shared/dashboard/src/pages/api/usage/hourly.ts +45 -0
  98. package/templates/shared/dashboard/src/pages/api/usage/projects.ts +51 -0
  99. package/templates/shared/dashboard/src/pages/api/user/identity.ts +11 -0
  100. package/templates/shared/dashboard/src/pages/settings/notifications.astro +34 -0
  101. package/templates/shared/dashboard/src/pages/settings/thresholds.astro +39 -0
  102. package/templates/shared/dashboard/src/pages/settings/usage.astro +28 -0
  103. package/templates/shared/docs/architecture.md +89 -0
  104. package/templates/shared/docs/post-deploy-runbook.md +126 -0
  105. package/templates/shared/docs/troubleshooting.md +91 -0
  106. package/templates/shared/package.json.hbs +5 -0
  107. package/templates/shared/scripts/ops/backfill-cloudflare-daily.ts +145 -0
  108. package/templates/shared/scripts/ops/backfill-monthly-rollups.ts +125 -0
  109. package/templates/shared/scripts/ops/validate-controls.js +141 -0
  110. package/templates/shared/tests/contract/validate-schemas.test.ts +130 -0
  111. package/templates/shared/tests/e2e/usage-api.test.ts +909 -0
  112. package/templates/shared/tests/fixtures/telemetry-envelope-invalid.json +9 -0
  113. package/templates/shared/tests/fixtures/telemetry-envelope-valid.json +27 -0
  114. package/templates/shared/tests/helpers/mock-d1.ts +61 -0
  115. package/templates/shared/tests/helpers/mock-kv.ts +37 -0
  116. package/templates/shared/tests/helpers/mock-storage.ts +166 -0
  117. package/templates/shared/tests/integration/kv-cache.test.ts +252 -0
  118. package/templates/shared/tests/integration/platform-usage.test.ts +956 -0
  119. package/templates/shared/tests/unit/billing.test.ts +331 -0
  120. package/templates/shared/tests/unit/cloudflare/graphql.test.ts +217 -0
  121. package/templates/shared/tests/unit/components/usage-transformers.test.ts +473 -0
  122. package/templates/shared/tests/unit/control.test.ts +226 -0
  123. package/templates/shared/tests/unit/cost-calculator.test.ts +141 -0
  124. package/templates/shared/tests/unit/economics.test.ts +365 -0
  125. package/templates/shared/tests/unit/telemetry-sampling.test.ts +401 -0
  126. package/templates/shared/tests/unit/workers/batch-persistence.test.ts +133 -0
  127. package/templates/shared/tests/unit/workers/budget-enforcement.test.ts +214 -0
  128. package/templates/shared/vitest.config.ts +18 -0
  129. package/templates/standard/dashboard/src/components/health/CircuitBreakerEvents.tsx +69 -0
  130. package/templates/standard/dashboard/src/components/health/CircuitBreakerPanel.tsx +97 -0
  131. package/templates/standard/dashboard/src/components/health/index.ts +2 -0
  132. package/templates/standard/dashboard/src/components/reports/CircuitBreakerReport.tsx +474 -0
  133. package/templates/standard/dashboard/src/components/reports/CostTrendsReport.tsx +229 -0
  134. package/templates/standard/dashboard/src/components/reports/ErrorTrendsReport.tsx +244 -0
  135. package/templates/standard/dashboard/src/components/reports/ProjectHealthTable.tsx +251 -0
  136. package/templates/standard/dashboard/src/components/reports/WarningDigestsTable.tsx +298 -0
  137. package/templates/standard/dashboard/src/components/usage/react/UsageTable.tsx +385 -0
  138. package/templates/standard/dashboard/src/components/usage/unified/CircuitBreakerEvents.tsx +305 -0
  139. package/templates/standard/dashboard/src/components/usage/unified/FeatureBudgets.tsx +472 -0
  140. package/templates/standard/dashboard/src/lib/errors/api.ts +84 -0
  141. package/templates/standard/dashboard/src/lib/errors/types.ts +75 -0
  142. package/templates/standard/dashboard/src/lib/infrastructure/api.ts +141 -0
  143. package/templates/standard/dashboard/src/lib/infrastructure/gatus.ts.hbs +112 -0
  144. package/templates/standard/dashboard/src/lib/services/proxy/index.ts +20 -0
  145. package/templates/standard/dashboard/src/lib/services/proxy/proxy.ts +244 -0
  146. package/templates/standard/dashboard/src/lib/services/proxy/types.ts +81 -0
  147. package/templates/standard/dashboard/src/pages/api/errors/[fingerprint]/mute.ts +49 -0
  148. package/templates/standard/dashboard/src/pages/api/errors/[fingerprint]/resolve.ts +36 -0
  149. package/templates/standard/dashboard/src/pages/api/errors/[fingerprint].ts +55 -0
  150. package/templates/standard/dashboard/src/pages/api/health/audit-history.ts +37 -0
  151. package/templates/standard/dashboard/src/pages/circuit-breakers.astro +13 -0
  152. package/templates/standard/tests/integration/platform-sentinel.test.ts +497 -0
  153. package/templates/standard/tests/unit/cloudflare/alerting.test.ts +480 -0
  154. package/templates/standard/tests/unit/error-collector/capture.test.ts +106 -0
  155. package/templates/standard/tests/unit/error-collector/dedup.test.ts +350 -0
  156. package/templates/standard/tests/unit/error-collector/fingerprint.test.ts +155 -0
  157. package/templates/standard/tests/unit/error-collector/github.test.ts +187 -0
@@ -0,0 +1,125 @@
1
+ #!/usr/bin/env npx tsx
2
+ /**
3
+ * Monthly Rollup Backfill Script
4
+ *
5
+ * Aggregates daily_usage_rollups into monthly_usage_rollups via the D1 REST API.
6
+ * Queries existing daily data and rolls it up to monthly granularity.
7
+ *
8
+ * Prerequisites:
9
+ * CLOUDFLARE_API_TOKEN — API token with D1:Write permissions
10
+ * CLOUDFLARE_ACCOUNT_ID — Your Cloudflare account ID
11
+ * D1_DATABASE_ID — Your platform-metrics D1 database ID
12
+ *
13
+ * Usage:
14
+ * npx tsx scripts/ops/backfill-monthly-rollups.ts
15
+ * npx tsx scripts/ops/backfill-monthly-rollups.ts --dry-run
16
+ * npx tsx scripts/ops/backfill-monthly-rollups.ts --start 2026-01 --end 2026-03
17
+ * npx tsx scripts/ops/backfill-monthly-rollups.ts --limit 12
18
+ */
19
+
20
+ const REST_API_BASE = 'https://api.cloudflare.com/client/v4';
21
+
22
+ interface Args {
23
+ start?: string;
24
+ end?: string;
25
+ dryRun: boolean;
26
+ limit: number;
27
+ }
28
+
29
+ function parseArgs(): Args {
30
+ const args = process.argv.slice(2);
31
+ const result: Args = { dryRun: false, limit: 12 };
32
+
33
+ for (let i = 0; i < args.length; i++) {
34
+ if (args[i] === '--start' && args[i + 1]) result.start = args[++i];
35
+ else if (args[i] === '--end' && args[i + 1]) result.end = args[++i];
36
+ else if (args[i] === '--limit' && args[i + 1]) result.limit = Number(args[++i]);
37
+ else if (args[i] === '--dry-run') result.dryRun = true;
38
+ }
39
+
40
+ if (!result.start) {
41
+ const d = new Date();
42
+ d.setMonth(d.getMonth() - 6);
43
+ result.start = d.toISOString().slice(0, 7);
44
+ }
45
+ if (!result.end) {
46
+ result.end = new Date().toISOString().slice(0, 7);
47
+ }
48
+
49
+ return result;
50
+ }
51
+
52
+ function getEnvOrThrow(key: string): string {
53
+ const val = process.env[key];
54
+ if (!val) throw new Error(`Missing required env var: ${key}`);
55
+ return val;
56
+ }
57
+
58
+ async function d1Query(accountId: string, dbId: string, token: string, sql: string, params: unknown[] = []) {
59
+ const res = await fetch(`${REST_API_BASE}/accounts/${accountId}/d1/database/${dbId}/query`, {
60
+ method: 'POST',
61
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
62
+ body: JSON.stringify({ sql, params }),
63
+ });
64
+ if (!res.ok) {
65
+ const text = await res.text();
66
+ throw new Error(`D1 query failed (${res.status}): ${text}`);
67
+ }
68
+ return res.json() as Promise<{ result: Array<{ results: unknown[] }> }>;
69
+ }
70
+
71
+ async function main() {
72
+ const args = parseArgs();
73
+ const token = getEnvOrThrow('CLOUDFLARE_API_TOKEN');
74
+ const accountId = getEnvOrThrow('CLOUDFLARE_ACCOUNT_ID');
75
+ const dbId = getEnvOrThrow('D1_DATABASE_ID');
76
+
77
+ console.log(`Backfilling monthly rollups: ${args.start} → ${args.end} (limit: ${args.limit}, dry-run: ${args.dryRun})`);
78
+
79
+ // Generate month list
80
+ const months: string[] = [];
81
+ const current = new Date(args.start + '-01');
82
+ const endDate = new Date(args.end + '-01');
83
+ while (current <= endDate && months.length < args.limit) {
84
+ months.push(current.toISOString().slice(0, 7));
85
+ current.setMonth(current.getMonth() + 1);
86
+ }
87
+
88
+ let inserted = 0;
89
+ for (const month of months) {
90
+ const monthStart = `${month}-01`;
91
+ const nextMonth = new Date(monthStart);
92
+ nextMonth.setMonth(nextMonth.getMonth() + 1);
93
+ const monthEnd = nextMonth.toISOString().slice(0, 10);
94
+
95
+ if (args.dryRun) {
96
+ console.log(`[DRY-RUN] Would roll up ${month}`);
97
+ continue;
98
+ }
99
+
100
+ await d1Query(accountId, dbId, token, `
101
+ INSERT OR REPLACE INTO monthly_usage_rollups (
102
+ project, snapshot_month,
103
+ d1_reads, d1_writes, kv_reads, kv_writes,
104
+ r2_reads, r2_writes, worker_requests, total_cost_usd
105
+ )
106
+ SELECT
107
+ project, ? as snapshot_month,
108
+ SUM(d1_reads), SUM(d1_writes), SUM(kv_reads), SUM(kv_writes),
109
+ SUM(r2_reads), SUM(r2_writes), SUM(worker_requests), SUM(total_cost_usd)
110
+ FROM daily_usage_rollups
111
+ WHERE project = 'all' AND snapshot_date >= ? AND snapshot_date < ?
112
+ GROUP BY project
113
+ `, [month, monthStart, monthEnd]);
114
+
115
+ inserted++;
116
+ console.log(` Rolled up ${month} (${inserted}/${months.length})`);
117
+ }
118
+
119
+ console.log(`Done. Inserted ${inserted} monthly rollup rows.`);
120
+ }
121
+
122
+ main().catch((err) => {
123
+ console.error('Fatal error:', err);
124
+ process.exit(1);
125
+ });
@@ -0,0 +1,141 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Validate Controls Script
4
+ *
5
+ * Validates budgets.yaml configuration:
6
+ * - No zero or negative limits
7
+ * - All features have d1_write_limit
8
+ * - Warns on suspiciously high/low values
9
+ * - Checks for orphaned entries (feature keys not in services.yaml)
10
+ *
11
+ * Usage:
12
+ * node scripts/ops/validate-controls.js
13
+ * node scripts/ops/validate-controls.js --strict
14
+ */
15
+
16
+ import { readFileSync } from 'node:fs';
17
+ import { resolve } from 'node:path';
18
+ import { parse } from 'yaml';
19
+
20
+ const ROOT = resolve(import.meta.dirname, '..', '..');
21
+ const strict = process.argv.includes('--strict');
22
+
23
+ let exitCode = 0;
24
+ let warnings = 0;
25
+ let errors = 0;
26
+
27
+ function warn(msg) {
28
+ console.warn(` WARN: ${msg}`);
29
+ warnings++;
30
+ if (strict) exitCode = 1;
31
+ }
32
+
33
+ function error(msg) {
34
+ console.error(` ERROR: ${msg}`);
35
+ errors++;
36
+ exitCode = 1;
37
+ }
38
+
39
+ function normaliseBudgetValue(val) {
40
+ if (typeof val === 'number') return val;
41
+ if (typeof val === 'string') return Number(val.replace(/_/g, ''));
42
+ return NaN;
43
+ }
44
+
45
+ // Load budgets.yaml
46
+ let budgets;
47
+ try {
48
+ const raw = readFileSync(resolve(ROOT, 'platform/config/budgets.yaml'), 'utf-8');
49
+ budgets = parse(raw);
50
+ } catch (err) {
51
+ console.error('Failed to load budgets.yaml:', err.message);
52
+ process.exit(1);
53
+ }
54
+
55
+ // Load services.yaml for cross-reference
56
+ let services;
57
+ try {
58
+ const raw = readFileSync(resolve(ROOT, 'platform/config/services.yaml'), 'utf-8');
59
+ services = parse(raw);
60
+ } catch {
61
+ services = null;
62
+ }
63
+
64
+ console.log('Validating budgets.yaml...\n');
65
+
66
+ // Validate feature budgets
67
+ const featureBudgets = budgets?.features ?? budgets?.feature_budgets ?? {};
68
+ const featureKeys = Object.keys(featureBudgets);
69
+
70
+ console.log(`Found ${featureKeys.length} feature budget entries\n`);
71
+
72
+ for (const key of featureKeys) {
73
+ const budget = featureBudgets[key];
74
+ console.log(` Checking ${key}...`);
75
+
76
+ if (!budget || typeof budget !== 'object') {
77
+ error(`${key}: budget is not an object`);
78
+ continue;
79
+ }
80
+
81
+ // Check for zero limits
82
+ for (const [field, rawValue] of Object.entries(budget)) {
83
+ const value = normaliseBudgetValue(rawValue);
84
+ if (isNaN(value)) {
85
+ error(`${key}.${field}: value "${rawValue}" is not a valid number`);
86
+ } else if (value <= 0) {
87
+ error(`${key}.${field}: value is ${value} (must be positive)`);
88
+ } else if (value < 100 && field.includes('d1')) {
89
+ warn(`${key}.${field}: suspiciously low D1 limit (${value})`);
90
+ } else if (value > 100_000_000_000) {
91
+ warn(`${key}.${field}: suspiciously high limit (${value})`);
92
+ }
93
+ }
94
+
95
+ // Check for d1_write_limit
96
+ const hasD1Write = Object.keys(budget).some(
97
+ (f) => f.includes('d1') && (f.includes('write') || f.includes('written'))
98
+ );
99
+ if (!hasD1Write) {
100
+ warn(`${key}: no d1_write_limit or d1_rows_written field`);
101
+ }
102
+
103
+ // Check feature key format (project:category:feature)
104
+ const parts = key.split(':');
105
+ if (parts.length < 3) {
106
+ warn(`${key}: feature key should have format project:category:feature (has ${parts.length} parts)`);
107
+ }
108
+ }
109
+
110
+ // Cross-reference with services.yaml
111
+ if (services) {
112
+ const registeredProjects = new Set();
113
+ const projects = services?.projects ?? [];
114
+ for (const p of Array.isArray(projects) ? projects : Object.keys(projects)) {
115
+ const name = typeof p === 'string' ? p : p?.name ?? p?.id;
116
+ if (name) registeredProjects.add(name);
117
+ }
118
+
119
+ for (const key of featureKeys) {
120
+ const project = key.split(':')[0];
121
+ if (registeredProjects.size > 0 && !registeredProjects.has(project)) {
122
+ warn(`${key}: project "${project}" not found in services.yaml`);
123
+ }
124
+ }
125
+ }
126
+
127
+ // Global budget check
128
+ const globalBudget = budgets?.global ?? budgets?.global_budget;
129
+ if (!globalBudget) {
130
+ warn('No global budget section found');
131
+ } else {
132
+ for (const [field, rawValue] of Object.entries(globalBudget)) {
133
+ const value = normaliseBudgetValue(rawValue);
134
+ if (isNaN(value) || value <= 0) {
135
+ error(`global.${field}: invalid value "${rawValue}"`);
136
+ }
137
+ }
138
+ }
139
+
140
+ console.log(`\nResults: ${errors} errors, ${warnings} warnings`);
141
+ process.exit(exitCode);
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Contract Tests for JSON Schemas
3
+ *
4
+ * Validates telemetry envelope + error report schemas against fixtures.
5
+ * Ensures contracts remain valid as schemas evolve.
6
+ */
7
+
8
+ import { describe, expect, it } from 'vitest';
9
+ import { readFileSync } from 'node:fs';
10
+ import { resolve } from 'node:path';
11
+
12
+ function loadJson(relativePath: string): unknown {
13
+ const fullPath = resolve(__dirname, '..', '..', relativePath);
14
+ return JSON.parse(readFileSync(fullPath, 'utf-8'));
15
+ }
16
+
17
+ // ===========================================================================
18
+ // Envelope Schema
19
+ // ===========================================================================
20
+
21
+ describe('telemetry envelope schema', () => {
22
+ const schema = loadJson('contracts/schemas/envelope.v1.schema.json') as {
23
+ required?: string[];
24
+ properties?: Record<string, unknown>;
25
+ };
26
+
27
+ it('has required version 1 schema structure', () => {
28
+ expect(schema).toBeDefined();
29
+ expect(schema.required).toBeDefined();
30
+ expect(Array.isArray(schema.required)).toBe(true);
31
+ });
32
+
33
+ it('requires core fields', () => {
34
+ const required = schema.required ?? [];
35
+ expect(required).toContain('version');
36
+ expect(required).toContain('project');
37
+ expect(required).toContain('feature');
38
+ });
39
+
40
+ it('defines resource_usage properties', () => {
41
+ const props = schema.properties ?? {};
42
+ expect(props.resource_usage).toBeDefined();
43
+ });
44
+ });
45
+
46
+ // ===========================================================================
47
+ // Valid Fixture
48
+ // ===========================================================================
49
+
50
+ describe('valid telemetry envelope fixture', () => {
51
+ const fixture = loadJson('tests/fixtures/telemetry-envelope-valid.json') as Record<string, unknown>;
52
+
53
+ it('has version 1', () => {
54
+ expect(fixture.version).toBe(1);
55
+ });
56
+
57
+ it('has valid project and feature format', () => {
58
+ expect(typeof fixture.project).toBe('string');
59
+ expect(typeof fixture.feature).toBe('string');
60
+ const feature = fixture.feature as string;
61
+ expect(feature.split(':').length).toBeGreaterThanOrEqual(3);
62
+ });
63
+
64
+ it('has resource_usage with numeric values', () => {
65
+ const usage = fixture.resource_usage as Record<string, unknown>;
66
+ expect(usage).toBeDefined();
67
+ expect(typeof usage.d1_reads).toBe('number');
68
+ expect(typeof usage.d1_writes).toBe('number');
69
+ expect(usage.d1_reads).toBeGreaterThanOrEqual(0);
70
+ expect(usage.d1_writes).toBeGreaterThanOrEqual(0);
71
+ });
72
+
73
+ it('has valid handler_type', () => {
74
+ expect(['fetch', 'scheduled', 'queue', 'alarm', 'tail']).toContain(fixture.handler_type);
75
+ });
76
+
77
+ it('has valid outcome', () => {
78
+ expect(['ok', 'error', 'exception', 'canceled']).toContain(fixture.outcome);
79
+ });
80
+ });
81
+
82
+ // ===========================================================================
83
+ // Invalid Fixture
84
+ // ===========================================================================
85
+
86
+ describe('invalid telemetry envelope fixture', () => {
87
+ const fixture = loadJson('tests/fixtures/telemetry-envelope-invalid.json') as Record<string, unknown>;
88
+
89
+ it('has wrong version', () => {
90
+ expect(fixture.version).not.toBe(1);
91
+ });
92
+
93
+ it('has empty project', () => {
94
+ expect(fixture.project).toBe('');
95
+ });
96
+
97
+ it('has malformed feature ID (missing colons)', () => {
98
+ const feature = fixture.feature as string;
99
+ expect(feature.split(':').length).toBeLessThan(3);
100
+ });
101
+
102
+ it('has invalid resource_usage values', () => {
103
+ const usage = fixture.resource_usage as Record<string, unknown>;
104
+ const hasInvalid = Object.values(usage).some(
105
+ (v) => typeof v !== 'number' || v < 0
106
+ );
107
+ expect(hasInvalid).toBe(true);
108
+ });
109
+ });
110
+
111
+ // ===========================================================================
112
+ // Error Report Schema
113
+ // ===========================================================================
114
+
115
+ describe('error report schema', () => {
116
+ const schema = loadJson('contracts/schemas/error_report.v1.schema.json') as {
117
+ required?: string[];
118
+ properties?: Record<string, unknown>;
119
+ };
120
+
121
+ it('has required schema structure', () => {
122
+ expect(schema).toBeDefined();
123
+ expect(schema.required).toBeDefined();
124
+ });
125
+
126
+ it('requires error identification fields', () => {
127
+ const required = schema.required ?? [];
128
+ expect(required.length).toBeGreaterThan(0);
129
+ });
130
+ });