@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,331 @@
1
+ /**
2
+ * Third-Party Provider Stubs
3
+ *
4
+ * Interface definitions and fetcher stubs for third-party service usage tracking.
5
+ * These will be implemented incrementally as integrations are built.
6
+ *
7
+ * Supported providers (planned):
8
+ * - GitHub (Actions minutes, Advanced Security seats, storage)
9
+ * - OpenAI (API usage, tokens)
10
+ * - Apify (Actor runs, compute units)
11
+ * - Anthropic (API usage, tokens)
12
+ * - Resend (emails sent)
13
+ */
14
+
15
+ /**
16
+ * Provider identifier
17
+ */
18
+ export type ProviderId = 'github' | 'openai' | 'apify' | 'anthropic' | 'resend';
19
+
20
+ /**
21
+ * Usage record from a third-party provider
22
+ */
23
+ export interface ThirdPartyUsageRecord {
24
+ /** Provider identifier */
25
+ provider: ProviderId;
26
+ /** Type of resource (e.g., 'actions_minutes', 'api_tokens') */
27
+ resourceType: string;
28
+ /** Optional specific resource name */
29
+ resourceName?: string;
30
+ /** Numeric usage value */
31
+ usageValue: number;
32
+ /** Unit of measurement */
33
+ usageUnit: string;
34
+ /** Estimated cost in USD */
35
+ costUsd: number;
36
+ /** When this data was collected */
37
+ collectedAt: Date;
38
+ /** Period this usage covers */
39
+ periodStart: Date;
40
+ periodEnd: Date;
41
+ }
42
+
43
+ /**
44
+ * Provider configuration
45
+ */
46
+ export interface ProviderConfig {
47
+ /** Provider identifier */
48
+ id: ProviderId;
49
+ /** Human-readable name */
50
+ name: string;
51
+ /** Whether the provider is currently enabled */
52
+ enabled: boolean;
53
+ /** API key environment variable name */
54
+ apiKeyEnvVar: string;
55
+ /** Pricing model description */
56
+ pricingModel: string;
57
+ /** Documentation URL */
58
+ docsUrl: string;
59
+ }
60
+
61
+ /**
62
+ * Provider interface for fetching usage data
63
+ */
64
+ export interface Provider {
65
+ /** Provider configuration */
66
+ config: ProviderConfig;
67
+
68
+ /**
69
+ * Fetch current usage data from the provider
70
+ * @param apiKey - API key for authentication
71
+ * @param options - Additional fetch options
72
+ * @returns Array of usage records
73
+ */
74
+ fetchUsage(
75
+ apiKey: string,
76
+ options?: {
77
+ startDate?: Date;
78
+ endDate?: Date;
79
+ includeBreakdown?: boolean;
80
+ }
81
+ ): Promise<ThirdPartyUsageRecord[]>;
82
+
83
+ /**
84
+ * Check if the provider credentials are valid
85
+ * @param apiKey - API key to validate
86
+ */
87
+ validateCredentials(apiKey: string): Promise<boolean>;
88
+ }
89
+
90
+ /**
91
+ * Provider configurations
92
+ */
93
+ export const PROVIDER_CONFIGS: Record<ProviderId, ProviderConfig> = {
94
+ github: {
95
+ id: 'github',
96
+ name: 'GitHub',
97
+ enabled: true,
98
+ apiKeyEnvVar: 'GITHUB_TOKEN',
99
+ pricingModel: 'Actions minutes, Advanced Security seats, storage',
100
+ docsUrl: 'https://docs.github.com/en/billing',
101
+ },
102
+ openai: {
103
+ id: 'openai',
104
+ name: 'OpenAI',
105
+ enabled: false,
106
+ apiKeyEnvVar: 'OPENAI_ADMIN_API_KEY',
107
+ pricingModel: 'Per-token pricing for API usage',
108
+ docsUrl: 'https://platform.openai.com/docs/api-reference/usage',
109
+ },
110
+ apify: {
111
+ id: 'apify',
112
+ name: 'Apify',
113
+ enabled: false,
114
+ apiKeyEnvVar: 'APIFY_API_KEY',
115
+ pricingModel: 'Compute units for actor runs',
116
+ docsUrl: 'https://docs.apify.com/platform/billing',
117
+ },
118
+ anthropic: {
119
+ id: 'anthropic',
120
+ name: 'Anthropic',
121
+ enabled: false,
122
+ apiKeyEnvVar: 'ANTHROPIC_ADMIN_API_KEY',
123
+ pricingModel: 'Per-token pricing for Claude API',
124
+ docsUrl: 'https://docs.anthropic.com/en/api/admin-api',
125
+ },
126
+ resend: {
127
+ id: 'resend',
128
+ name: 'Resend',
129
+ enabled: false,
130
+ apiKeyEnvVar: 'RESEND_API_KEY',
131
+ pricingModel: 'Per-email pricing',
132
+ docsUrl: 'https://resend.com/docs/api-reference',
133
+ },
134
+ };
135
+
136
+ // =============================================================================
137
+ // PROVIDER FETCHER STUBS
138
+ // =============================================================================
139
+ // Each stub returns an empty array with a TODO comment.
140
+ // Implement these as integrations are built.
141
+ // =============================================================================
142
+
143
+ /**
144
+ * GitHub Usage Fetcher
145
+ *
146
+ * TODO: Implement GitHub billing API integration
147
+ * - GET /orgs/{org}/settings/billing/actions
148
+ * - GET /orgs/{org}/settings/billing/advanced-security
149
+ * - GET /orgs/{org}/settings/billing/shared-storage
150
+ *
151
+ * @see https://docs.github.com/en/rest/billing
152
+ */
153
+ export async function fetchGitHubUsage(
154
+ _apiKey: string,
155
+ _options?: {
156
+ organization?: string;
157
+ startDate?: Date;
158
+ endDate?: Date;
159
+ }
160
+ ): Promise<ThirdPartyUsageRecord[]> {
161
+ // TODO: Implement GitHub billing API calls
162
+ // 1. Fetch Actions minutes usage
163
+ // 2. Fetch Advanced Security seats
164
+ // 3. Fetch shared storage usage
165
+ // 4. Calculate costs based on GitHub pricing
166
+ console.log('[providers] GitHub usage fetcher not yet implemented');
167
+ return [];
168
+ }
169
+
170
+ /**
171
+ * OpenAI Usage Fetcher
172
+ *
173
+ * TODO: Implement OpenAI Usage API integration
174
+ * - GET /v1/usage (requires admin API key: sk-admin-...)
175
+ *
176
+ * @see https://platform.openai.com/docs/api-reference/usage
177
+ */
178
+ export async function fetchOpenAIUsage(
179
+ _apiKey: string,
180
+ _options?: {
181
+ startDate?: Date;
182
+ endDate?: Date;
183
+ }
184
+ ): Promise<ThirdPartyUsageRecord[]> {
185
+ // TODO: Implement OpenAI Usage API calls
186
+ // 1. Fetch token usage by model
187
+ // 2. Calculate costs based on model pricing
188
+ // 3. Group by date for time-series data
189
+ console.log('[providers] OpenAI usage fetcher not yet implemented');
190
+ return [];
191
+ }
192
+
193
+ /**
194
+ * Apify Usage Fetcher
195
+ *
196
+ * TODO: Implement Apify billing API integration
197
+ * - GET /v2/users/{userId}/usage
198
+ *
199
+ * @see https://docs.apify.com/api/v2#/reference/users/account-usage
200
+ */
201
+ export async function fetchApifyUsage(
202
+ _apiKey: string,
203
+ _options?: {
204
+ startDate?: Date;
205
+ endDate?: Date;
206
+ }
207
+ ): Promise<ThirdPartyUsageRecord[]> {
208
+ // TODO: Implement Apify usage API calls
209
+ // 1. Fetch compute unit usage
210
+ // 2. Fetch actor run counts
211
+ // 3. Calculate costs based on Apify pricing
212
+ console.log('[providers] Apify usage fetcher not yet implemented');
213
+ return [];
214
+ }
215
+
216
+ /**
217
+ * Anthropic Usage Fetcher
218
+ *
219
+ * TODO: Implement Anthropic Admin API integration
220
+ * - GET /v1/organizations/{organization_id}/usage (Admin API)
221
+ *
222
+ * @see https://docs.anthropic.com/en/api/admin-api
223
+ */
224
+ export async function fetchAnthropicUsage(
225
+ _apiKey: string,
226
+ _options?: {
227
+ organizationId?: string;
228
+ startDate?: Date;
229
+ endDate?: Date;
230
+ }
231
+ ): Promise<ThirdPartyUsageRecord[]> {
232
+ // TODO: Implement Anthropic Admin API calls
233
+ // 1. Fetch token usage by model
234
+ // 2. Calculate costs based on Claude pricing
235
+ // 3. Group by date for time-series data
236
+ console.log('[providers] Anthropic usage fetcher not yet implemented');
237
+ return [];
238
+ }
239
+
240
+ /**
241
+ * Resend Usage Fetcher
242
+ *
243
+ * TODO: Implement Resend API integration
244
+ * - No direct usage endpoint; track via webhook or count emails
245
+ *
246
+ * @see https://resend.com/docs/api-reference
247
+ */
248
+ export async function fetchResendUsage(
249
+ _apiKey: string,
250
+ _options?: {
251
+ startDate?: Date;
252
+ endDate?: Date;
253
+ }
254
+ ): Promise<ThirdPartyUsageRecord[]> {
255
+ // TODO: Implement Resend usage tracking
256
+ // 1. Query sent emails count (if available)
257
+ // 2. Or track via our own email logs
258
+ // 3. Calculate costs based on Resend pricing
259
+ console.log('[providers] Resend usage fetcher not yet implemented');
260
+ return [];
261
+ }
262
+
263
+ // =============================================================================
264
+ // AGGREGATION UTILITIES
265
+ // =============================================================================
266
+
267
+ /**
268
+ * Fetch usage from all enabled providers
269
+ */
270
+ export async function fetchAllProviderUsage(
271
+ apiKeys: Partial<Record<ProviderId, string>>,
272
+ options?: {
273
+ startDate?: Date;
274
+ endDate?: Date;
275
+ }
276
+ ): Promise<ThirdPartyUsageRecord[]> {
277
+ const results: ThirdPartyUsageRecord[] = [];
278
+
279
+ // Only fetch from providers that have API keys configured
280
+ const fetchPromises: Promise<ThirdPartyUsageRecord[]>[] = [];
281
+
282
+ if (apiKeys.github) {
283
+ fetchPromises.push(fetchGitHubUsage(apiKeys.github, options));
284
+ }
285
+ if (apiKeys.openai) {
286
+ fetchPromises.push(fetchOpenAIUsage(apiKeys.openai, options));
287
+ }
288
+ if (apiKeys.apify) {
289
+ fetchPromises.push(fetchApifyUsage(apiKeys.apify, options));
290
+ }
291
+ if (apiKeys.anthropic) {
292
+ fetchPromises.push(fetchAnthropicUsage(apiKeys.anthropic, options));
293
+ }
294
+ if (apiKeys.resend) {
295
+ fetchPromises.push(fetchResendUsage(apiKeys.resend, options));
296
+ }
297
+
298
+ // Fetch all in parallel, handle individual failures gracefully
299
+ const settledResults = await Promise.allSettled(fetchPromises);
300
+
301
+ for (const result of settledResults) {
302
+ if (result.status === 'fulfilled') {
303
+ results.push(...result.value);
304
+ } else {
305
+ console.error('[providers] Failed to fetch from provider:', result.reason);
306
+ }
307
+ }
308
+
309
+ return results;
310
+ }
311
+
312
+ /**
313
+ * Calculate total third-party costs from usage records
314
+ */
315
+ export function calculateThirdPartyCosts(records: ThirdPartyUsageRecord[]): {
316
+ byProvider: Record<ProviderId, number>;
317
+ total: number;
318
+ } {
319
+ const byProvider: Partial<Record<ProviderId, number>> = {};
320
+ let total = 0;
321
+
322
+ for (const record of records) {
323
+ byProvider[record.provider] = (byProvider[record.provider] ?? 0) + record.costUsd;
324
+ total += record.costUsd;
325
+ }
326
+
327
+ return {
328
+ byProvider: byProvider as Record<ProviderId, number>,
329
+ total,
330
+ };
331
+ }
@@ -0,0 +1,37 @@
1
+ import type { APIRoute } from 'astro';
2
+
3
+ export const POST: APIRoute = async ({ params, locals }) => {
4
+ const db = (locals.runtime?.env as { PLATFORM_DB?: D1Database } | undefined)?.PLATFORM_DB;
5
+
6
+ if (!db) {
7
+ return new Response(JSON.stringify({ error: 'Database not available' }), {
8
+ status: 503,
9
+ headers: { 'Content-Type': 'application/json' },
10
+ });
11
+ }
12
+
13
+ const id = params.id;
14
+ if (!id) {
15
+ return new Response(JSON.stringify({ error: 'Missing notification id' }), {
16
+ status: 400,
17
+ headers: { 'Content-Type': 'application/json' },
18
+ });
19
+ }
20
+
21
+ try {
22
+ await db
23
+ .prepare(`UPDATE notifications SET read_at = unixepoch() WHERE id = ? AND read_at IS NULL`)
24
+ .bind(id)
25
+ .run();
26
+
27
+ return new Response(
28
+ JSON.stringify({ ok: true }),
29
+ { headers: { 'Content-Type': 'application/json' } }
30
+ );
31
+ } catch {
32
+ return new Response(JSON.stringify({ error: 'Failed to mark as read' }), {
33
+ status: 500,
34
+ headers: { 'Content-Type': 'application/json' },
35
+ });
36
+ }
37
+ };
@@ -0,0 +1,28 @@
1
+ import type { APIRoute } from 'astro';
2
+
3
+ export const POST: APIRoute = async ({ locals }) => {
4
+ const db = (locals.runtime?.env as { PLATFORM_DB?: D1Database } | undefined)?.PLATFORM_DB;
5
+
6
+ if (!db) {
7
+ return new Response(JSON.stringify({ error: 'Database not available' }), {
8
+ status: 503,
9
+ headers: { 'Content-Type': 'application/json' },
10
+ });
11
+ }
12
+
13
+ try {
14
+ const result = await db
15
+ .prepare(`UPDATE notifications SET read_at = unixepoch() WHERE read_at IS NULL`)
16
+ .run();
17
+
18
+ return new Response(
19
+ JSON.stringify({ ok: true, updated: result.meta?.changes ?? 0 }),
20
+ { headers: { 'Content-Type': 'application/json' } }
21
+ );
22
+ } catch {
23
+ return new Response(JSON.stringify({ error: 'Failed to mark all as read' }), {
24
+ status: 500,
25
+ headers: { 'Content-Type': 'application/json' },
26
+ });
27
+ }
28
+ };
@@ -0,0 +1,38 @@
1
+ import type { APIRoute } from 'astro';
2
+
3
+ export const POST: APIRoute = async ({ locals }) => {
4
+ const db = (locals.runtime?.env as { PLATFORM_DB?: D1Database } | undefined)?.PLATFORM_DB;
5
+ const kv = (locals.runtime?.env as { PLATFORM_CACHE?: KVNamespace } | undefined)?.PLATFORM_CACHE;
6
+
7
+ if (!db || !kv) {
8
+ return new Response(JSON.stringify({ error: 'Bindings not available' }), {
9
+ status: 503,
10
+ headers: { 'Content-Type': 'application/json' },
11
+ });
12
+ }
13
+
14
+ try {
15
+ const approved = await db
16
+ .prepare(
17
+ `SELECT id, pattern_type, pattern_value, error_type, priority
18
+ FROM transient_pattern_suggestions
19
+ WHERE status = 'approved'
20
+ ORDER BY match_count DESC
21
+ LIMIT 500`
22
+ )
23
+ .all();
24
+
25
+ const patterns = approved.results ?? [];
26
+ await kv.put('PATTERNS:DYNAMIC:APPROVED', JSON.stringify(patterns), { expirationTtl: 86400 });
27
+
28
+ return new Response(
29
+ JSON.stringify({ refreshed: true, count: patterns.length }),
30
+ { headers: { 'Content-Type': 'application/json' } }
31
+ );
32
+ } catch {
33
+ return new Response(JSON.stringify({ error: 'Cache refresh failed' }), {
34
+ status: 500,
35
+ headers: { 'Content-Type': 'application/json' },
36
+ });
37
+ }
38
+ };
@@ -0,0 +1,36 @@
1
+ import type { APIRoute } from 'astro';
2
+
3
+ export const POST: APIRoute = async ({ locals }) => {
4
+ const db = (locals.runtime?.env as { PLATFORM_DB?: D1Database } | undefined)?.PLATFORM_DB;
5
+
6
+ if (!db) {
7
+ return new Response(JSON.stringify({ error: 'Database not available' }), {
8
+ status: 503,
9
+ headers: { 'Content-Type': 'application/json' },
10
+ });
11
+ }
12
+
13
+ try {
14
+ const unclassified = await db
15
+ .prepare(
16
+ `SELECT fingerprint, script_name, error_message, priority, occurrence_count
17
+ FROM error_occurrences
18
+ WHERE fingerprint NOT IN (
19
+ SELECT pattern_value FROM transient_pattern_suggestions WHERE status = 'approved'
20
+ )
21
+ ORDER BY occurrence_count DESC
22
+ LIMIT 50`
23
+ )
24
+ .all();
25
+
26
+ return new Response(
27
+ JSON.stringify({ discovered: unclassified.results?.length ?? 0, errors: unclassified.results ?? [] }),
28
+ { headers: { 'Content-Type': 'application/json' } }
29
+ );
30
+ } catch {
31
+ return new Response(JSON.stringify({ error: 'Discovery failed' }), {
32
+ status: 500,
33
+ headers: { 'Content-Type': 'application/json' },
34
+ });
35
+ }
36
+ };
@@ -0,0 +1,39 @@
1
+ import type { APIRoute } from 'astro';
2
+
3
+ export const GET: APIRoute = async ({ locals }) => {
4
+ const db = (locals.runtime?.env as { PLATFORM_DB?: D1Database } | undefined)?.PLATFORM_DB;
5
+
6
+ if (!db) {
7
+ return new Response(JSON.stringify({ patterns: [], count: 0 }), {
8
+ headers: { 'Content-Type': 'application/json' },
9
+ });
10
+ }
11
+
12
+ try {
13
+ const patterns = await db
14
+ .prepare(
15
+ `SELECT s.id, s.pattern_type, s.pattern_value, s.error_type, s.priority,
16
+ s.match_count, s.source, s.created_at,
17
+ COUNT(e.id) as evidence_count
18
+ FROM transient_pattern_suggestions s
19
+ LEFT JOIN pattern_match_evidence e ON e.suggestion_id = s.id
20
+ WHERE s.status = 'shadow'
21
+ AND s.match_count >= 3
22
+ AND s.created_at < datetime('now', '-3 days')
23
+ GROUP BY s.id
24
+ ORDER BY s.match_count DESC
25
+ LIMIT 20`
26
+ )
27
+ .all();
28
+
29
+ return new Response(
30
+ JSON.stringify({ patterns: patterns.results ?? [], count: patterns.results?.length ?? 0 }),
31
+ { headers: { 'Content-Type': 'application/json', 'Cache-Control': 'max-age=60' } }
32
+ );
33
+ } catch {
34
+ return new Response(JSON.stringify({ patterns: [], count: 0 }), {
35
+ status: 500,
36
+ headers: { 'Content-Type': 'application/json' },
37
+ });
38
+ }
39
+ };
@@ -0,0 +1,39 @@
1
+ import type { APIRoute } from 'astro';
2
+
3
+ export const GET: APIRoute = async ({ locals }) => {
4
+ const db = (locals.runtime?.env as { PLATFORM_DB?: D1Database } | undefined)?.PLATFORM_DB;
5
+
6
+ if (!db) {
7
+ return new Response(JSON.stringify({ byStatus: {}, total: 0 }), {
8
+ headers: { 'Content-Type': 'application/json' },
9
+ });
10
+ }
11
+
12
+ try {
13
+ const counts = await db
14
+ .prepare(
15
+ `SELECT status, COUNT(*) as count
16
+ FROM transient_pattern_suggestions
17
+ GROUP BY status
18
+ LIMIT 10`
19
+ )
20
+ .all<{ status: string; count: number }>();
21
+
22
+ const byStatus: Record<string, number> = {};
23
+ let total = 0;
24
+ for (const row of counts.results ?? []) {
25
+ byStatus[row.status] = row.count;
26
+ total += row.count;
27
+ }
28
+
29
+ return new Response(
30
+ JSON.stringify({ byStatus, total }),
31
+ { headers: { 'Content-Type': 'application/json', 'Cache-Control': 'max-age=60' } }
32
+ );
33
+ } catch {
34
+ return new Response(JSON.stringify({ byStatus: {}, total: 0 }), {
35
+ status: 500,
36
+ headers: { 'Content-Type': 'application/json' },
37
+ });
38
+ }
39
+ };
@@ -0,0 +1,43 @@
1
+ import type { APIRoute } from 'astro';
2
+
3
+ export const GET: APIRoute = async ({ locals, url }) => {
4
+ const db = (locals.runtime?.env as { PLATFORM_DB?: D1Database } | undefined)?.PLATFORM_DB;
5
+
6
+ if (!db) {
7
+ return new Response(JSON.stringify({ suggestions: [], total: 0 }), {
8
+ headers: { 'Content-Type': 'application/json' },
9
+ });
10
+ }
11
+
12
+ const status = url.searchParams.get('status') ?? 'pending';
13
+ const limit = Math.min(parseInt(url.searchParams.get('limit') ?? '50'), 100);
14
+
15
+ try {
16
+ const suggestions = await db
17
+ .prepare(
18
+ `SELECT id, pattern_type, pattern_value, error_type, priority, status,
19
+ match_count, source, created_at, reviewed_at, reviewer_notes, is_protected
20
+ FROM transient_pattern_suggestions
21
+ WHERE status = ?
22
+ ORDER BY match_count DESC, created_at DESC
23
+ LIMIT ?`
24
+ )
25
+ .bind(status, limit)
26
+ .all();
27
+
28
+ const total = await db
29
+ .prepare(`SELECT COUNT(*) as count FROM transient_pattern_suggestions WHERE status = ? LIMIT 1`)
30
+ .bind(status)
31
+ .first<{ count: number }>();
32
+
33
+ return new Response(
34
+ JSON.stringify({ suggestions: suggestions.results ?? [], total: total?.count ?? 0 }),
35
+ { headers: { 'Content-Type': 'application/json', 'Cache-Control': 'max-age=30' } }
36
+ );
37
+ } catch {
38
+ return new Response(JSON.stringify({ suggestions: [], total: 0 }), {
39
+ status: 500,
40
+ headers: { 'Content-Type': 'application/json' },
41
+ });
42
+ }
43
+ };
@@ -0,0 +1,45 @@
1
+ import type { APIRoute } from 'astro';
2
+
3
+ export const GET: APIRoute = async ({ locals, url }) => {
4
+ const db = (locals.runtime?.env as { PLATFORM_DB?: D1Database } | undefined)?.PLATFORM_DB;
5
+
6
+ if (!db) {
7
+ return new Response(JSON.stringify({ reports: [] }), {
8
+ headers: { 'Content-Type': 'application/json' },
9
+ });
10
+ }
11
+
12
+ const project = url.searchParams.get('project');
13
+ const limit = Math.min(parseInt(url.searchParams.get('limit') ?? '10'), 50);
14
+
15
+ try {
16
+ const query = project
17
+ ? `SELECT id, project, scan_type, ai_judge_score, sdk_score, observability_score,
18
+ cost_protection_score, security_score, scan_date, focused_dimensions
19
+ FROM audit_results
20
+ WHERE project = ?
21
+ ORDER BY scan_date DESC
22
+ LIMIT ?`
23
+ : `SELECT id, project, scan_type, ai_judge_score, sdk_score, observability_score,
24
+ cost_protection_score, security_score, scan_date, focused_dimensions
25
+ FROM audit_results
26
+ ORDER BY scan_date DESC
27
+ LIMIT ?`;
28
+
29
+ const stmt = project
30
+ ? db.prepare(query).bind(project, limit)
31
+ : db.prepare(query).bind(limit);
32
+
33
+ const reports = await stmt.all();
34
+
35
+ return new Response(
36
+ JSON.stringify({ reports: reports.results ?? [] }),
37
+ { headers: { 'Content-Type': 'application/json', 'Cache-Control': 'max-age=120' } }
38
+ );
39
+ } catch {
40
+ return new Response(JSON.stringify({ reports: [] }), {
41
+ status: 500,
42
+ headers: { 'Content-Type': 'application/json' },
43
+ });
44
+ }
45
+ };
@@ -0,0 +1,52 @@
1
+ import type { APIRoute } from 'astro';
2
+
3
+ export const GET: APIRoute = async ({ locals, url }) => {
4
+ const db = (locals.runtime?.env as { PLATFORM_DB?: D1Database } | undefined)?.PLATFORM_DB;
5
+
6
+ if (!db) {
7
+ return new Response(JSON.stringify({ daily: [], monthly: [] }), {
8
+ headers: { 'Content-Type': 'application/json' },
9
+ });
10
+ }
11
+
12
+ const days = Math.min(parseInt(url.searchParams.get('days') ?? '30'), 90);
13
+ const startDate = new Date();
14
+ startDate.setDate(startDate.getDate() - days);
15
+ const startStr = startDate.toISOString().slice(0, 10);
16
+
17
+ try {
18
+ const [daily, monthly] = await Promise.all([
19
+ db
20
+ .prepare(
21
+ `SELECT snapshot_date, d1_reads, d1_writes, kv_reads, kv_writes,
22
+ r2_reads, r2_writes, worker_requests, total_cost_usd
23
+ FROM daily_usage_rollups
24
+ WHERE project = 'all' AND snapshot_date >= ?
25
+ ORDER BY snapshot_date ASC
26
+ LIMIT 90`
27
+ )
28
+ .bind(startStr)
29
+ .all(),
30
+ db
31
+ .prepare(
32
+ `SELECT snapshot_month, d1_reads, d1_writes, kv_reads, kv_writes,
33
+ r2_reads, r2_writes, worker_requests, total_cost_usd
34
+ FROM monthly_usage_rollups
35
+ WHERE project = 'all'
36
+ ORDER BY snapshot_month DESC
37
+ LIMIT 12`
38
+ )
39
+ .all(),
40
+ ]);
41
+
42
+ return new Response(
43
+ JSON.stringify({ daily: daily.results ?? [], monthly: monthly.results ?? [] }),
44
+ { headers: { 'Content-Type': 'application/json', 'Cache-Control': 'max-age=300' } }
45
+ );
46
+ } catch {
47
+ return new Response(JSON.stringify({ daily: [], monthly: [] }), {
48
+ status: 500,
49
+ headers: { 'Content-Type': 'application/json' },
50
+ });
51
+ }
52
+ };