@littlebearapps/platform-admin-sdk 1.4.2 → 2.0.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 (189) hide show
  1. package/dist/templates.d.ts +1 -1
  2. package/dist/templates.js +232 -2
  3. package/package.json +1 -1
  4. package/templates/full/config/audit-targets.yaml +72 -0
  5. package/templates/full/dashboard/src/components/notifications/NotificationBell.tsx +30 -0
  6. package/templates/full/dashboard/src/components/notifications/NotificationList.tsx +116 -0
  7. package/templates/full/dashboard/src/components/notifications/index.ts +2 -0
  8. package/templates/full/dashboard/src/components/patterns/ActivePatterns.tsx +62 -0
  9. package/templates/full/dashboard/src/components/patterns/PatternStats.tsx +60 -0
  10. package/templates/full/dashboard/src/components/patterns/PatternTabs.tsx +116 -0
  11. package/templates/full/dashboard/src/components/patterns/SuggestionsQueue.tsx +115 -0
  12. package/templates/full/dashboard/src/components/patterns/SystemPatterns.tsx +52 -0
  13. package/templates/full/dashboard/src/components/patterns/index.ts +5 -0
  14. package/templates/full/dashboard/src/components/reports/GapDetectionReport.tsx +69 -0
  15. package/templates/full/dashboard/src/components/reports/SdkAuditReport.tsx +72 -0
  16. package/templates/full/dashboard/src/components/reports/index.ts +2 -0
  17. package/templates/full/dashboard/src/components/search/SearchModal.tsx +108 -0
  18. package/templates/full/dashboard/src/pages/api/notifications/[id]/read.ts +37 -0
  19. package/templates/full/dashboard/src/pages/api/notifications/index.ts +47 -0
  20. package/templates/full/dashboard/src/pages/api/notifications/read-all.ts +28 -0
  21. package/templates/full/dashboard/src/pages/api/notifications/unread-count.ts +31 -0
  22. package/templates/full/dashboard/src/pages/api/patterns/approve.ts +55 -0
  23. package/templates/full/dashboard/src/pages/api/patterns/cache-refresh.ts +38 -0
  24. package/templates/full/dashboard/src/pages/api/patterns/discover.ts +36 -0
  25. package/templates/full/dashboard/src/pages/api/patterns/index.ts +36 -0
  26. package/templates/full/dashboard/src/pages/api/patterns/ready-for-review.ts +39 -0
  27. package/templates/full/dashboard/src/pages/api/patterns/reject.ts +54 -0
  28. package/templates/full/dashboard/src/pages/api/patterns/stats.ts +39 -0
  29. package/templates/full/dashboard/src/pages/api/patterns/suggestions.ts +43 -0
  30. package/templates/full/dashboard/src/pages/api/reports/audit.ts +45 -0
  31. package/templates/full/dashboard/src/pages/api/reports/usage.ts +52 -0
  32. package/templates/full/dashboard/src/pages/api/search/index.ts +74 -0
  33. package/templates/full/dashboard/src/pages/api/search/reindex.ts +28 -0
  34. package/templates/full/dashboard/src/pages/api/search/stats.ts +27 -0
  35. package/templates/full/dashboard/src/pages/api/settings/index.ts +37 -0
  36. package/templates/full/dashboard/src/pages/api/settings/update.ts +41 -0
  37. package/templates/full/dashboard/src/pages/api/topology/index.ts +56 -0
  38. package/templates/full/dashboard/src/pages/notifications.astro +11 -0
  39. package/templates/full/migrations/008_auditor.sql +99 -0
  40. package/templates/full/migrations/010_pricing_versions.sql +110 -0
  41. package/templates/full/migrations/011_multi_account.sql +51 -0
  42. package/templates/full/scripts/ops/set-kv-pricing.ts +182 -0
  43. package/templates/full/scripts/ops/universal-backfill.ts +147 -0
  44. package/templates/full/workers/lib/ai-judge-schema.ts +181 -0
  45. package/templates/full/workers/lib/auditor/comprehensive-report.ts +407 -0
  46. package/templates/full/workers/lib/auditor/feature-coverage.ts +348 -0
  47. package/templates/full/workers/lib/auditor/index.ts +9 -0
  48. package/templates/full/workers/lib/auditor/types.ts +167 -0
  49. package/templates/full/workers/platform-auditor.ts +1071 -0
  50. package/templates/full/wrangler.auditor.jsonc.hbs +75 -0
  51. package/templates/shared/.github/workflows/contract-check.yml.hbs +42 -0
  52. package/templates/shared/.github/workflows/dashboard-deploy.yml.hbs +39 -0
  53. package/templates/shared/.github/workflows/platform-check.yml.hbs +28 -0
  54. package/templates/shared/.github/workflows/security.yml +33 -0
  55. package/templates/shared/config/observability.yaml.hbs +276 -0
  56. package/templates/shared/contracts/schemas/envelope.v1.schema.json +64 -0
  57. package/templates/shared/contracts/schemas/error_report.v1.schema.json +65 -0
  58. package/templates/shared/contracts/types/telemetry-envelope.types.ts +139 -0
  59. package/templates/shared/dashboard/astro.config.mjs +21 -0
  60. package/templates/shared/dashboard/package.json.hbs +29 -0
  61. package/templates/shared/dashboard/src/components/Header.astro +29 -0
  62. package/templates/shared/dashboard/src/components/Nav.astro.hbs +59 -0
  63. package/templates/shared/dashboard/src/components/infrastructure/AlertHistory.tsx +57 -0
  64. package/templates/shared/dashboard/src/components/infrastructure/InfrastructureStats.tsx +73 -0
  65. package/templates/shared/dashboard/src/components/infrastructure/ServiceRegistry.tsx +55 -0
  66. package/templates/shared/dashboard/src/components/infrastructure/UptimeStatus.tsx +56 -0
  67. package/templates/shared/dashboard/src/components/infrastructure/index.ts +4 -0
  68. package/templates/shared/dashboard/src/components/overview/ActivityFeed.tsx +134 -0
  69. package/templates/shared/dashboard/src/components/overview/CostQuadrant.tsx +131 -0
  70. package/templates/shared/dashboard/src/components/overview/ErrorsQuadrant.tsx +113 -0
  71. package/templates/shared/dashboard/src/components/overview/HealthQuadrant.tsx +87 -0
  72. package/templates/shared/dashboard/src/components/overview/MissionControl.tsx +139 -0
  73. package/templates/shared/dashboard/src/components/resources/AllowanceStatus.tsx +44 -0
  74. package/templates/shared/dashboard/src/components/resources/CostCentreOverview.tsx +42 -0
  75. package/templates/shared/dashboard/src/components/resources/ResourceTabs.tsx +69 -0
  76. package/templates/shared/dashboard/src/components/resources/index.ts +3 -0
  77. package/templates/shared/dashboard/src/components/settings/SettingsCard.tsx +21 -0
  78. package/templates/shared/dashboard/src/components/settings/index.ts +1 -0
  79. package/templates/shared/dashboard/src/components/ui/AlertBanner.tsx +39 -0
  80. package/templates/shared/dashboard/src/components/ui/Breadcrumbs.tsx +27 -0
  81. package/templates/shared/dashboard/src/components/ui/EmptyState.tsx +26 -0
  82. package/templates/shared/dashboard/src/components/ui/ErrorBoundary.tsx +42 -0
  83. package/templates/shared/dashboard/src/components/ui/LoadingSkeleton.tsx +18 -0
  84. package/templates/shared/dashboard/src/components/ui/PageShell.tsx +26 -0
  85. package/templates/shared/dashboard/src/components/ui/Sparkline.tsx +127 -0
  86. package/templates/shared/dashboard/src/components/ui/StatusDot.tsx +21 -0
  87. package/templates/shared/dashboard/src/components/ui/Toast.tsx +44 -0
  88. package/templates/shared/dashboard/src/components/ui/index.ts +9 -0
  89. package/templates/shared/dashboard/src/components/usage/AnomaliesWidget.tsx +68 -0
  90. package/templates/shared/dashboard/src/components/usage/HourlyUsageChart.tsx +55 -0
  91. package/templates/shared/dashboard/src/components/usage/PlanAllowanceDashboard.tsx +67 -0
  92. package/templates/shared/dashboard/src/components/usage/ProjectCostBreakdown.tsx +55 -0
  93. package/templates/shared/dashboard/src/components/usage/index.ts +4 -0
  94. package/templates/shared/dashboard/src/env.d.ts.hbs +34 -0
  95. package/templates/shared/dashboard/src/layouts/DashboardLayout.astro +37 -0
  96. package/templates/shared/dashboard/src/lib/cloudflare/costs.ts +21 -0
  97. package/templates/shared/dashboard/src/lib/fetch.ts +29 -0
  98. package/templates/shared/dashboard/src/lib/types.ts +72 -0
  99. package/templates/shared/dashboard/src/middleware/auth.ts +100 -0
  100. package/templates/shared/dashboard/src/middleware/index.ts +1 -0
  101. package/templates/shared/dashboard/src/pages/api/costs/overview.ts +65 -0
  102. package/templates/shared/dashboard/src/pages/api/costs/providers.ts +47 -0
  103. package/templates/shared/dashboard/src/pages/api/infrastructure/services.ts +55 -0
  104. package/templates/shared/dashboard/src/pages/api/infrastructure/stats.ts +99 -0
  105. package/templates/shared/dashboard/src/pages/api/overview/summary.ts +311 -0
  106. package/templates/shared/dashboard/src/pages/api/usage/allowances.ts +56 -0
  107. package/templates/shared/dashboard/src/pages/api/usage/anomalies.ts +45 -0
  108. package/templates/shared/dashboard/src/pages/api/usage/billing.ts +53 -0
  109. package/templates/shared/dashboard/src/pages/api/usage/circuit-breakers.ts +44 -0
  110. package/templates/shared/dashboard/src/pages/api/usage/granular.ts +50 -0
  111. package/templates/shared/dashboard/src/pages/api/usage/hourly.ts +45 -0
  112. package/templates/shared/dashboard/src/pages/api/usage/projects.ts +51 -0
  113. package/templates/shared/dashboard/src/pages/api/usage/status.ts +42 -0
  114. package/templates/shared/dashboard/src/pages/api/user/identity.ts +11 -0
  115. package/templates/shared/dashboard/src/pages/dashboard.astro +11 -0
  116. package/templates/shared/dashboard/src/pages/index.astro +3 -0
  117. package/templates/shared/dashboard/src/pages/resources.astro +11 -0
  118. package/templates/shared/dashboard/src/pages/settings/index.astro +28 -0
  119. package/templates/shared/dashboard/src/pages/settings/notifications.astro +34 -0
  120. package/templates/shared/dashboard/src/pages/settings/thresholds.astro +39 -0
  121. package/templates/shared/dashboard/src/pages/settings/usage.astro +28 -0
  122. package/templates/shared/dashboard/src/styles/global.css +29 -0
  123. package/templates/shared/dashboard/tailwind.config.mjs +9 -0
  124. package/templates/shared/dashboard/tsconfig.json +9 -0
  125. package/templates/shared/dashboard/wrangler.json.hbs +47 -0
  126. package/templates/shared/docs/architecture.md +89 -0
  127. package/templates/shared/docs/post-deploy-runbook.md +126 -0
  128. package/templates/shared/docs/troubleshooting.md +91 -0
  129. package/templates/shared/package.json.hbs +17 -1
  130. package/templates/shared/scripts/ops/backfill-cloudflare-daily.ts +145 -0
  131. package/templates/shared/scripts/ops/backfill-cloudflare-hourly.ts +473 -0
  132. package/templates/shared/scripts/ops/backfill-monthly-rollups.ts +125 -0
  133. package/templates/shared/scripts/ops/discover-graphql-datasets.ts +482 -0
  134. package/templates/shared/scripts/ops/reset-budget-state.ts +279 -0
  135. package/templates/shared/scripts/ops/validate-controls.js +141 -0
  136. package/templates/shared/scripts/ops/validate-pipeline.ts +237 -0
  137. package/templates/shared/scripts/ops/verify-account-completeness.ts +236 -0
  138. package/templates/shared/scripts/validate-schemas.js +61 -0
  139. package/templates/shared/tests/contract/validate-schemas.test.ts +130 -0
  140. package/templates/shared/tests/fixtures/telemetry-envelope-invalid.json +9 -0
  141. package/templates/shared/tests/fixtures/telemetry-envelope-valid.json +27 -0
  142. package/templates/shared/tests/helpers/mock-d1.ts +61 -0
  143. package/templates/shared/tests/helpers/mock-kv.ts +37 -0
  144. package/templates/shared/tests/unit/workers/batch-persistence.test.ts +133 -0
  145. package/templates/shared/tests/unit/workers/budget-enforcement.test.ts +214 -0
  146. package/templates/shared/vitest.config.ts +18 -0
  147. package/templates/shared/workers/lib/usage/collectors/anthropic.ts +114 -0
  148. package/templates/shared/workers/lib/usage/collectors/apify.ts +96 -0
  149. package/templates/shared/workers/lib/usage/collectors/custom-http.ts +151 -0
  150. package/templates/shared/workers/lib/usage/collectors/deepseek.ts +92 -0
  151. package/templates/shared/workers/lib/usage/collectors/gemini.ts +263 -0
  152. package/templates/shared/workers/lib/usage/collectors/github.ts +362 -0
  153. package/templates/shared/workers/lib/usage/collectors/index.ts +31 -15
  154. package/templates/shared/workers/lib/usage/collectors/minimax.ts +106 -0
  155. package/templates/shared/workers/lib/usage/collectors/openai.ts +171 -0
  156. package/templates/shared/workers/lib/usage/collectors/resend.ts +79 -0
  157. package/templates/shared/workers/lib/usage/collectors/stripe.ts +192 -0
  158. package/templates/shared/workers/lib/usage/shared/types.ts +46 -0
  159. package/templates/shared/workers/platform-usage.ts +98 -8
  160. package/templates/standard/dashboard/src/components/errors/ErrorStats.tsx +53 -0
  161. package/templates/standard/dashboard/src/components/errors/ErrorsTable.tsx +133 -0
  162. package/templates/standard/dashboard/src/components/errors/index.ts +2 -0
  163. package/templates/standard/dashboard/src/components/health/CircuitBreakerEvents.tsx +69 -0
  164. package/templates/standard/dashboard/src/components/health/CircuitBreakerPanel.tsx +97 -0
  165. package/templates/standard/dashboard/src/components/health/DlqStatusCard.tsx +52 -0
  166. package/templates/standard/dashboard/src/components/health/HealthTabs.tsx +86 -0
  167. package/templates/standard/dashboard/src/components/health/index.ts +4 -0
  168. package/templates/standard/dashboard/src/lib/errors.ts +28 -0
  169. package/templates/standard/dashboard/src/pages/api/errors/[fingerprint]/mute.ts +49 -0
  170. package/templates/standard/dashboard/src/pages/api/errors/[fingerprint]/resolve.ts +36 -0
  171. package/templates/standard/dashboard/src/pages/api/errors/[fingerprint].ts +55 -0
  172. package/templates/standard/dashboard/src/pages/api/errors/index.ts +58 -0
  173. package/templates/standard/dashboard/src/pages/api/errors/stats.ts +55 -0
  174. package/templates/standard/dashboard/src/pages/api/health/audit-history.ts +37 -0
  175. package/templates/standard/dashboard/src/pages/api/health/dlq.ts +43 -0
  176. package/templates/standard/dashboard/src/pages/circuit-breakers.astro +13 -0
  177. package/templates/standard/dashboard/src/pages/errors.astro +13 -0
  178. package/templates/standard/dashboard/src/pages/health.astro +11 -0
  179. package/templates/standard/migrations/009_topology_mapper.sql +65 -0
  180. package/templates/standard/tests/unit/error-collector/capture.test.ts +106 -0
  181. package/templates/standard/tests/unit/error-collector/fingerprint.test.ts +155 -0
  182. package/templates/standard/workers/lib/error-collector/email-health-alerts.ts +37 -3
  183. package/templates/standard/workers/lib/error-collector/gap-alerts.ts +32 -1
  184. package/templates/standard/workers/lib/mapper/attribution-check.ts +339 -0
  185. package/templates/standard/workers/lib/mapper/index.ts +7 -0
  186. package/templates/standard/workers/platform-mapper.ts +482 -0
  187. package/templates/standard/workers/platform-sdk-test-client.ts +125 -0
  188. package/templates/standard/wrangler.mapper.jsonc.hbs +85 -0
  189. package/templates/standard/wrangler.sdk-test-client.jsonc.hbs +62 -0
@@ -0,0 +1,482 @@
1
+ /**
2
+ * Platform Mapper Worker
3
+ *
4
+ * Auto-discovers Cloudflare infrastructure every 15 minutes:
5
+ * - Cloudflare Workers (via REST API)
6
+ * - Merges with Service Registry (from KV)
7
+ * - Stores topology in D1 + KV
8
+ * - Runs attribution checks (which resources belong to which projects)
9
+ *
10
+ * Cron: Every 15 minutes
11
+ * Cost: $0/month (within free tier for typical usage)
12
+ *
13
+ * Extension points (uncomment when ready):
14
+ * - GitHub deployment discovery
15
+ * - Health endpoint monitoring
16
+ * - Custom resource discovery
17
+ */
18
+
19
+ import {
20
+ withCronBudget,
21
+ withFeatureBudget,
22
+ CircuitBreakerError,
23
+ completeTracking,
24
+ createLogger,
25
+ createLoggerFromRequest,
26
+ health,
27
+ pingHeartbeat,
28
+ type Logger,
29
+ } from '@littlebearapps/platform-consumer-sdk';
30
+ import {
31
+ checkAttribution,
32
+ storeAttributionReport,
33
+ alertUnattributedResources,
34
+ type DiscoveredResource,
35
+ } from './lib/mapper';
36
+
37
+ // TODO: Update this feature ID to match your project slug
38
+ const FEATURE_ID = 'platform:discovery:topology';
39
+ const HEARTBEAT_ID = 'platform:health:mapper';
40
+
41
+ interface Env {
42
+ PLATFORM_DB: D1Database;
43
+ PLATFORM_CACHE: KVNamespace;
44
+ SERVICE_REGISTRY: KVNamespace;
45
+ PLATFORM_TELEMETRY: Queue;
46
+ CLOUDFLARE_API_TOKEN: string;
47
+ CLOUDFLARE_ACCOUNT_ID: string;
48
+ GATUS_HEARTBEAT_URL?: string;
49
+ GATUS_TOKEN?: string;
50
+ SLACK_WEBHOOK_URL?: string;
51
+ }
52
+
53
+ interface Service {
54
+ id: string;
55
+ name: string;
56
+ type: string;
57
+ tier: number;
58
+ status: string;
59
+ version?: string;
60
+ health_endpoint?: string;
61
+ health_status?: string;
62
+ last_seen?: string;
63
+ metadata?: Record<string, unknown>;
64
+ }
65
+
66
+ interface Connection {
67
+ from_service: string;
68
+ to_service: string;
69
+ connection_type: string;
70
+ protocol?: string;
71
+ status: string;
72
+ metadata?: Record<string, unknown>;
73
+ }
74
+
75
+ interface Topology {
76
+ services: Service[];
77
+ connections: Connection[];
78
+ timestamp: string;
79
+ }
80
+
81
+ interface ServiceRegistry {
82
+ metadata?: {
83
+ version: string;
84
+ lastUpdated: string;
85
+ autoDiscovery: boolean;
86
+ };
87
+ services: Service[];
88
+ connections: Connection[];
89
+ }
90
+
91
+ interface CloudflareWorkerScript {
92
+ id: string;
93
+ created_on: string;
94
+ modified_on: string;
95
+ }
96
+
97
+ interface CloudflareAPIResponse {
98
+ result: CloudflareWorkerScript[];
99
+ success: boolean;
100
+ }
101
+
102
+ export default {
103
+ async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext): Promise<void> {
104
+ const log = createLogger({ worker: 'platform-mapper', featureId: FEATURE_ID });
105
+ log.info('Starting infrastructure mapping');
106
+
107
+ try {
108
+ const trackedEnv = withCronBudget(env, FEATURE_ID, {
109
+ ctx,
110
+ cronExpression: '*/15 * * * *',
111
+ });
112
+
113
+ const topology = await discoverInfrastructure(trackedEnv, log);
114
+
115
+ // Store in D1 (historical)
116
+ await storeSnapshot(trackedEnv.PLATFORM_DB, topology, log);
117
+
118
+ // Cache in KV (latest)
119
+ await cacheTopology(trackedEnv.PLATFORM_CACHE, topology, log);
120
+
121
+ // Run attribution check
122
+ const resources = topology.services.map(
123
+ (s): DiscoveredResource => ({
124
+ resourceType: s.type,
125
+ resourceId: s.id,
126
+ resourceName: s.name,
127
+ metadata: s.metadata,
128
+ })
129
+ );
130
+ const attributionReport = checkAttribution(resources);
131
+ await storeAttributionReport(trackedEnv, attributionReport, log);
132
+ if (attributionReport.unattributedCount > 0) {
133
+ await alertUnattributedResources(trackedEnv, attributionReport, log);
134
+ }
135
+ log.debug('Attribution check complete', {
136
+ total: attributionReport.totalResources,
137
+ attributed: attributionReport.attributedCount,
138
+ unattributed: attributionReport.unattributedCount,
139
+ });
140
+
141
+ await completeTracking(trackedEnv);
142
+
143
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
144
+ await health(HEARTBEAT_ID, env.PLATFORM_CACHE as any, env.PLATFORM_TELEMETRY, ctx);
145
+
146
+ log.info('Infrastructure mapping complete', {
147
+ services: topology.services.length,
148
+ connections: topology.connections.length,
149
+ });
150
+
151
+ pingHeartbeat(ctx, env.GATUS_HEARTBEAT_URL, env.GATUS_TOKEN, true);
152
+ } catch (error) {
153
+ if (error instanceof CircuitBreakerError) {
154
+ log.warn('Circuit breaker STOP', error, { reason: error.reason });
155
+ return;
156
+ }
157
+
158
+ log.error('Infrastructure mapping failed', error);
159
+ pingHeartbeat(ctx, env.GATUS_HEARTBEAT_URL, env.GATUS_TOKEN, false);
160
+ throw error;
161
+ }
162
+ },
163
+
164
+ // Manual trigger endpoint for testing
165
+ async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
166
+ const url = new URL(request.url);
167
+
168
+ if (url.pathname === '/health') {
169
+ return new Response('OK', { status: 200 });
170
+ }
171
+
172
+ const log = createLoggerFromRequest(request, env, 'platform-mapper', FEATURE_ID);
173
+
174
+ try {
175
+ const trackedEnv = withFeatureBudget(env, FEATURE_ID, { ctx });
176
+
177
+ if (url.pathname === '/trigger') {
178
+ try {
179
+ const topology = await discoverInfrastructure(trackedEnv, log);
180
+ await storeSnapshot(trackedEnv.PLATFORM_DB, topology, log);
181
+ await cacheTopology(trackedEnv.PLATFORM_CACHE, topology, log);
182
+ await completeTracking(trackedEnv);
183
+
184
+ return new Response(JSON.stringify(topology, null, 2), {
185
+ headers: { 'Content-Type': 'application/json' },
186
+ });
187
+ } catch (discoverError) {
188
+ const err =
189
+ discoverError instanceof Error ? discoverError : new Error(String(discoverError));
190
+ log.error('Discovery failed', err, { stack: err.stack });
191
+ return new Response(JSON.stringify({ error: err.message }), {
192
+ status: 500,
193
+ headers: { 'Content-Type': 'application/json' },
194
+ });
195
+ }
196
+ }
197
+
198
+ await completeTracking(trackedEnv);
199
+ return new Response('Not Found', { status: 404 });
200
+ } catch (error) {
201
+ if (error instanceof CircuitBreakerError) {
202
+ log.warn('Circuit breaker STOP', error, { reason: error.reason });
203
+ return new Response(JSON.stringify({ error: 'Service temporarily unavailable' }), {
204
+ status: 503,
205
+ headers: { 'Content-Type': 'application/json' },
206
+ });
207
+ }
208
+ const err = error instanceof Error ? error : new Error(String(error));
209
+ return new Response(JSON.stringify({ error: err.message }), {
210
+ status: 500,
211
+ headers: { 'Content-Type': 'application/json' },
212
+ });
213
+ }
214
+ },
215
+ };
216
+
217
+ async function discoverInfrastructure(env: Env, log: Logger): Promise<Topology> {
218
+ const topology: Topology = {
219
+ services: [],
220
+ connections: [],
221
+ timestamp: new Date().toISOString(),
222
+ };
223
+
224
+ // 1. Load base registry (static config)
225
+ const baseRegistry = await loadServiceRegistry(env, log);
226
+
227
+ // 2. Discover Cloudflare Workers via REST API
228
+ const cfWorkers = await discoverCloudflareWorkers(env, log);
229
+
230
+ // =============================================================================
231
+ // EXTENSION POINTS
232
+ // Uncomment and implement when ready:
233
+ //
234
+ // 3. Discover GitHub deployments
235
+ // const githubDeployments = await discoverGitHubDeployments(env, log);
236
+ //
237
+ // 4. Fetch health status from monitoring
238
+ // const healthStatuses = await fetchHealthStatuses(env, log);
239
+ // =============================================================================
240
+
241
+ // Merge all sources (priority: health > deployments > CF API > base config)
242
+ topology.services = mergeServiceData(baseRegistry.services, cfWorkers);
243
+
244
+ // Use connections from base registry (defensive filter)
245
+ topology.connections = (baseRegistry.connections ?? []).filter(
246
+ (conn): conn is Connection =>
247
+ conn != null &&
248
+ typeof conn.from_service === 'string' &&
249
+ typeof conn.to_service === 'string' &&
250
+ typeof conn.connection_type === 'string'
251
+ );
252
+
253
+ return topology;
254
+ }
255
+
256
+ async function loadServiceRegistry(env: Env, log: Logger): Promise<ServiceRegistry> {
257
+ const registryJSON = await env.SERVICE_REGISTRY.get('registry:latest');
258
+
259
+ if (!registryJSON) {
260
+ log.warn('Service registry not found in KV, using empty registry');
261
+ return { services: [], connections: [] };
262
+ }
263
+
264
+ return JSON.parse(registryJSON) as ServiceRegistry;
265
+ }
266
+
267
+ async function discoverCloudflareWorkers(env: Env, log: Logger): Promise<Service[]> {
268
+ const maxRetries = 2;
269
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
270
+ try {
271
+ const response = await fetch(
272
+ `https://api.cloudflare.com/client/v4/accounts/${env.CLOUDFLARE_ACCOUNT_ID}/workers/scripts`,
273
+ {
274
+ headers: {
275
+ Authorization: `Bearer ${env.CLOUDFLARE_API_TOKEN}`,
276
+ 'Content-Type': 'application/json',
277
+ },
278
+ }
279
+ );
280
+
281
+ if (!response.ok) {
282
+ // Consume response body to avoid stalled connections
283
+ await response.text().catch(() => {});
284
+ if (response.status >= 500 && attempt < maxRetries) {
285
+ log.warn('Cloudflare API 5xx, retrying', undefined, { status: response.status, attempt });
286
+ await new Promise((r) => setTimeout(r, 1000 * Math.pow(2, attempt)));
287
+ continue;
288
+ }
289
+ log.error('Cloudflare API error', undefined, { status: response.status });
290
+ return [];
291
+ }
292
+
293
+ const data = (await response.json()) as CloudflareAPIResponse;
294
+
295
+ return data.result.map((script) => ({
296
+ id: `cloudflare-worker-${script.id}`,
297
+ name: script.id,
298
+ type: 'cloudflare-worker',
299
+ tier: 1,
300
+ status: 'deployed',
301
+ version: 'unknown',
302
+ health_status: 'unknown',
303
+ last_seen: new Date().toISOString(),
304
+ metadata: {
305
+ created_on: script.created_on,
306
+ modified_on: script.modified_on,
307
+ },
308
+ }));
309
+ } catch (error) {
310
+ if (attempt < maxRetries) {
311
+ log.warn(
312
+ 'Cloudflare API fetch error, retrying',
313
+ error instanceof Error ? error : undefined,
314
+ { attempt }
315
+ );
316
+ await new Promise((r) => setTimeout(r, 1000 * Math.pow(2, attempt)));
317
+ continue;
318
+ }
319
+ log.error('Failed to discover Cloudflare Workers', error);
320
+ return [];
321
+ }
322
+ }
323
+ return [];
324
+ }
325
+
326
+ function mergeServiceData(...sources: (Service[] | Partial<Service>[])[]): Service[] {
327
+ const merged: Record<string, Partial<Service>> = {};
328
+
329
+ for (const source of sources) {
330
+ if (!source || !Array.isArray(source)) continue;
331
+
332
+ for (const service of source) {
333
+ if (!service || !service.id) continue;
334
+
335
+ if (!merged[service.id]) {
336
+ merged[service.id] = { ...service };
337
+ } else {
338
+ merged[service.id] = {
339
+ ...merged[service.id],
340
+ ...service,
341
+ health_status: service.health_status || merged[service.id].health_status,
342
+ };
343
+ }
344
+ }
345
+ }
346
+
347
+ return Object.values(merged)
348
+ .filter((service): service is Service => !!service.id)
349
+ .map((service) => ({
350
+ id: service.id,
351
+ name: service.name ?? service.id,
352
+ type: service.type ?? 'unknown',
353
+ tier: service.tier ?? 2,
354
+ status: service.status ?? 'unknown',
355
+ version: service.version,
356
+ health_endpoint: service.health_endpoint,
357
+ health_status: service.health_status ?? 'unknown',
358
+ last_seen: service.last_seen ?? new Date().toISOString(),
359
+ metadata: service.metadata,
360
+ }));
361
+ }
362
+
363
+ async function storeSnapshot(db: D1Database, topology: Topology, log: Logger): Promise<void> {
364
+ const snapshotData = JSON.stringify(topology);
365
+ const serviceCount = topology.services.length;
366
+ const connCount = topology.connections.length;
367
+
368
+ // Batch all D1 writes to minimise row-level overhead (cost rule #1)
369
+ const stmts: D1PreparedStatement[] = [];
370
+
371
+ // Snapshot insert
372
+ stmts.push(
373
+ db
374
+ .prepare(
375
+ 'INSERT INTO topology_snapshots (data, change_summary, service_count, connection_count) VALUES (?, ?, ?, ?)'
376
+ )
377
+ .bind(snapshotData, '', serviceCount, connCount)
378
+ );
379
+
380
+ // Services upserts
381
+ for (const service of topology.services) {
382
+ stmts.push(
383
+ db
384
+ .prepare(
385
+ `INSERT INTO services (id, name, type, tier, status, version, health_endpoint, health_status, last_seen, metadata, updated_at)
386
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
387
+ ON CONFLICT(id) DO UPDATE SET
388
+ name = excluded.name, type = excluded.type, tier = excluded.tier,
389
+ status = excluded.status, version = excluded.version,
390
+ health_endpoint = excluded.health_endpoint, health_status = excluded.health_status,
391
+ last_seen = excluded.last_seen, metadata = excluded.metadata,
392
+ updated_at = CURRENT_TIMESTAMP`
393
+ )
394
+ .bind(
395
+ service.id,
396
+ service.name ?? 'unknown',
397
+ service.type ?? 'unknown',
398
+ service.tier ?? 0,
399
+ service.status ?? 'unknown',
400
+ service.version ?? null,
401
+ service.health_endpoint ?? null,
402
+ service.health_status ?? 'unknown',
403
+ service.last_seen ?? new Date().toISOString(),
404
+ service.metadata ? JSON.stringify(service.metadata) : null
405
+ )
406
+ );
407
+ }
408
+
409
+ // Connections upserts
410
+ for (const conn of topology.connections) {
411
+ stmts.push(
412
+ db
413
+ .prepare(
414
+ `INSERT INTO connections (from_service, to_service, connection_type, protocol, status, metadata, updated_at)
415
+ VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
416
+ ON CONFLICT(from_service, to_service, connection_type) DO UPDATE SET
417
+ protocol = excluded.protocol, status = excluded.status,
418
+ metadata = excluded.metadata, updated_at = CURRENT_TIMESTAMP`
419
+ )
420
+ .bind(
421
+ conn.from_service,
422
+ conn.to_service,
423
+ conn.connection_type,
424
+ conn.protocol ?? null,
425
+ conn.status ?? 'unknown',
426
+ conn.metadata ? JSON.stringify(conn.metadata) : null
427
+ )
428
+ );
429
+ }
430
+
431
+ await db.batch(stmts);
432
+
433
+ log.debug('Snapshot stored in D1');
434
+ }
435
+
436
+ async function cacheTopology(kv: KVNamespace, topology: Topology, log: Logger): Promise<void> {
437
+ // Cache full topology (15-minute TTL matches cron interval)
438
+ await kv.put('topology:latest', JSON.stringify(topology), {
439
+ expirationTtl: 900,
440
+ });
441
+
442
+ // Cache derived health summary
443
+ const healthSummary = {
444
+ total: topology.services.length,
445
+ up: topology.services.filter((s) => s.health_status === 'up').length,
446
+ down: topology.services.filter((s) => s.health_status === 'down').length,
447
+ degraded: topology.services.filter((s) => s.health_status === 'degraded').length,
448
+ unknown: topology.services.filter((s) => s.health_status === 'unknown').length,
449
+ timestamp: topology.timestamp,
450
+ };
451
+
452
+ await kv.put('topology:health', JSON.stringify(healthSummary), {
453
+ expirationTtl: 900,
454
+ });
455
+
456
+ // Cache version matrix
457
+ const versionMatrix = topology.services
458
+ .filter((s) => s.version && s.version !== 'unknown')
459
+ .map((s) => ({
460
+ id: s.id,
461
+ name: s.name,
462
+ version: s.version,
463
+ }));
464
+
465
+ await kv.put('topology:versions', JSON.stringify(versionMatrix), {
466
+ expirationTtl: 900,
467
+ });
468
+
469
+ // Cache configuration gaps
470
+ const configGaps = topology.services.filter(
471
+ (s) =>
472
+ s.status === 'partial' ||
473
+ s.status === 'not-deployed' ||
474
+ (s.metadata && JSON.stringify(s.metadata).includes('TBD'))
475
+ );
476
+
477
+ await kv.put('topology:config-gaps', JSON.stringify(configGaps), {
478
+ expirationTtl: 900,
479
+ });
480
+
481
+ log.debug('Topology cached in KV');
482
+ }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Platform SDK Test Client Worker
3
+ *
4
+ * Validates Platform SDK integration by triggering telemetry flow
5
+ * and testing circuit breaker functionality.
6
+ *
7
+ * Endpoints:
8
+ * GET /trigger-usage - Trigger SDK telemetry (KV operations)
9
+ * GET /sdk-health - Dual-plane health check (control + data plane)
10
+ * GET /health - Basic liveness probe
11
+ *
12
+ * Extension points:
13
+ * - Add cron schedule (every 6 hours) for automated SDK health monitoring
14
+ * - Extend coverage to test all SDK service proxies (D1, AI, R2, Queue, etc.)
15
+ */
16
+
17
+ import {
18
+ withFeatureBudget,
19
+ CircuitBreakerError,
20
+ completeTracking,
21
+ } from '@littlebearapps/platform-consumer-sdk';
22
+
23
+ // TODO: Update these feature IDs to match your project slug
24
+ const FEATURE_VALIDATION = 'platform:sdk-test:validation';
25
+ const FEATURE_HEALTH_CHECK = 'platform:sdk-test:health-check';
26
+
27
+ interface Env {
28
+ TEST_KV: KVNamespace; // Use a non-reserved name so SDK tracks operations
29
+ PLATFORM_CACHE: KVNamespace; // Reserved for SDK circuit breaker state
30
+ PLATFORM_TELEMETRY: Queue;
31
+ }
32
+
33
+ export default {
34
+ async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
35
+ const url = new URL(request.url);
36
+
37
+ if (url.pathname === '/trigger-usage') {
38
+ try {
39
+ // Wrap env with SDK tracking for test feature
40
+ const trackedEnv = withFeatureBudget(env, FEATURE_VALIDATION, { ctx });
41
+
42
+ // Make a test KV operation (will be tracked)
43
+ // Use TEST_KV (not PLATFORM_CACHE which is reserved and skipped by SDK)
44
+ const testKey = `test:${Date.now()}`;
45
+ await trackedEnv.TEST_KV.put(testKey, 'sdk-validation-test', { expirationTtl: 60 });
46
+ const value = await trackedEnv.TEST_KV.get(testKey);
47
+ await trackedEnv.TEST_KV.delete(testKey);
48
+
49
+ // Explicitly flush telemetry after all operations complete
50
+ await completeTracking(trackedEnv);
51
+
52
+ return Response.json({
53
+ success: true,
54
+ message: 'SDK telemetry triggered successfully',
55
+ featureId: FEATURE_VALIDATION,
56
+ operations: {
57
+ kvWrites: 1,
58
+ kvReads: 1,
59
+ kvDeletes: 1,
60
+ },
61
+ testValue: value,
62
+ timestamp: new Date().toISOString(),
63
+ });
64
+ } catch (e) {
65
+ if (e instanceof CircuitBreakerError) {
66
+ return Response.json(
67
+ {
68
+ success: false,
69
+ error: 'CircuitBreakerError',
70
+ featureId: e.featureId,
71
+ level: e.level,
72
+ reason: e.reason,
73
+ timestamp: new Date().toISOString(),
74
+ },
75
+ { status: 503 }
76
+ );
77
+ }
78
+ throw e;
79
+ }
80
+ }
81
+
82
+ if (url.pathname === '/sdk-health') {
83
+ try {
84
+ const trackedEnv = withFeatureBudget(env, FEATURE_HEALTH_CHECK, { ctx });
85
+ const healthResult = await trackedEnv.health();
86
+ await completeTracking(trackedEnv);
87
+
88
+ return Response.json({
89
+ success: true,
90
+ healthResult,
91
+ timestamp: new Date().toISOString(),
92
+ });
93
+ } catch (e) {
94
+ if (e instanceof CircuitBreakerError) {
95
+ return Response.json(
96
+ {
97
+ success: false,
98
+ error: 'CircuitBreakerError',
99
+ featureId: e.featureId,
100
+ level: e.level,
101
+ reason: e.reason,
102
+ },
103
+ { status: 503 }
104
+ );
105
+ }
106
+ return Response.json(
107
+ {
108
+ success: false,
109
+ error: e instanceof Error ? e.message : String(e),
110
+ },
111
+ { status: 500 }
112
+ );
113
+ }
114
+ }
115
+
116
+ if (url.pathname === '/health') {
117
+ return Response.json({ status: 'ok', worker: 'platform-sdk-test-client' });
118
+ }
119
+
120
+ return Response.json(
121
+ { error: 'Not found', endpoints: ['/trigger-usage', '/sdk-health', '/health'] },
122
+ { status: 404 }
123
+ );
124
+ },
125
+ };
@@ -0,0 +1,85 @@
1
+ {
2
+ // =============================================================================
3
+ // Platform Mapper Worker
4
+ // =============================================================================
5
+ // Auto-discovers Cloudflare infrastructure and services.
6
+ //
7
+ // Deploy: wrangler deploy -c wrangler.{{projectSlug}}-mapper.jsonc
8
+ // Tail logs: wrangler tail {{projectSlug}}-mapper --format pretty
9
+ // Cron: Every 15 minutes
10
+ // Secrets: CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID
11
+ // =============================================================================
12
+
13
+ "$schema": "node_modules/wrangler/config-schema.json",
14
+
15
+ "name": "{{projectSlug}}-mapper",
16
+ "main": "workers/platform-mapper.ts",
17
+ "compatibility_date": "2026-01-01",
18
+ "compatibility_flags": ["nodejs_compat_v2"],
19
+
20
+ // D1 Database
21
+ // TODO: Replace database_id with your D1 database ID
22
+ "d1_databases": [
23
+ {
24
+ "binding": "PLATFORM_DB",
25
+ "database_name": "{{projectSlug}}-metrics",
26
+ "database_id": "YOUR_D1_DATABASE_ID"
27
+ }
28
+ ],
29
+
30
+ // KV Namespaces
31
+ // TODO: Replace IDs with your KV namespace IDs
32
+ "kv_namespaces": [
33
+ {
34
+ "binding": "PLATFORM_CACHE",
35
+ "id": "YOUR_PLATFORM_CACHE_KV_ID"
36
+ },
37
+ {
38
+ "binding": "SERVICE_REGISTRY",
39
+ "id": "YOUR_SERVICE_REGISTRY_KV_ID"
40
+ }
41
+ ],
42
+
43
+ // Cron Triggers - Every 15 minutes
44
+ "triggers": {
45
+ "crons": ["*/15 * * * *"]
46
+ },
47
+
48
+ // Queue Producers (Platform SDK telemetry)
49
+ "queues": {
50
+ "producers": [
51
+ {
52
+ "queue": "platform-telemetry",
53
+ "binding": "PLATFORM_TELEMETRY"
54
+ }
55
+ ]
56
+ },
57
+
58
+ // Observability
59
+ "observability": {
60
+ "enabled": true,
61
+ "logs": {
62
+ "enabled": true,
63
+ "invocation_logs": true,
64
+ "head_sampling_rate": 1.0
65
+ },
66
+ "traces": {
67
+ "enabled": true,
68
+ "head_sampling_rate": 0.1
69
+ }
70
+ },
71
+
72
+ // Source maps for readable stack traces
73
+ "upload_source_maps": true
74
+
75
+ // Tail Consumer (Error Collection) — uncomment if using error-collector
76
+ // "tail_consumers": [
77
+ // { "service": "{{projectSlug}}-error-collector" }
78
+ // ]
79
+
80
+ // Secrets (add via: wrangler secret put <NAME> -c wrangler.{{projectSlug}}-mapper.jsonc)
81
+ // - CLOUDFLARE_API_TOKEN
82
+ // - CLOUDFLARE_ACCOUNT_ID
83
+ // - SLACK_WEBHOOK_URL (optional, for attribution alerts)
84
+ // - GATUS_TOKEN (optional, for heartbeat monitoring)
85
+ }