@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.
- package/dist/templates.d.ts +1 -1
- package/dist/templates.js +232 -2
- package/package.json +1 -1
- package/templates/full/config/audit-targets.yaml +72 -0
- package/templates/full/dashboard/src/components/notifications/NotificationBell.tsx +30 -0
- package/templates/full/dashboard/src/components/notifications/NotificationList.tsx +116 -0
- package/templates/full/dashboard/src/components/notifications/index.ts +2 -0
- package/templates/full/dashboard/src/components/patterns/ActivePatterns.tsx +62 -0
- package/templates/full/dashboard/src/components/patterns/PatternStats.tsx +60 -0
- package/templates/full/dashboard/src/components/patterns/PatternTabs.tsx +116 -0
- package/templates/full/dashboard/src/components/patterns/SuggestionsQueue.tsx +115 -0
- package/templates/full/dashboard/src/components/patterns/SystemPatterns.tsx +52 -0
- package/templates/full/dashboard/src/components/patterns/index.ts +5 -0
- package/templates/full/dashboard/src/components/reports/GapDetectionReport.tsx +69 -0
- package/templates/full/dashboard/src/components/reports/SdkAuditReport.tsx +72 -0
- package/templates/full/dashboard/src/components/reports/index.ts +2 -0
- package/templates/full/dashboard/src/components/search/SearchModal.tsx +108 -0
- package/templates/full/dashboard/src/pages/api/notifications/[id]/read.ts +37 -0
- package/templates/full/dashboard/src/pages/api/notifications/index.ts +47 -0
- package/templates/full/dashboard/src/pages/api/notifications/read-all.ts +28 -0
- package/templates/full/dashboard/src/pages/api/notifications/unread-count.ts +31 -0
- package/templates/full/dashboard/src/pages/api/patterns/approve.ts +55 -0
- package/templates/full/dashboard/src/pages/api/patterns/cache-refresh.ts +38 -0
- package/templates/full/dashboard/src/pages/api/patterns/discover.ts +36 -0
- package/templates/full/dashboard/src/pages/api/patterns/index.ts +36 -0
- package/templates/full/dashboard/src/pages/api/patterns/ready-for-review.ts +39 -0
- package/templates/full/dashboard/src/pages/api/patterns/reject.ts +54 -0
- package/templates/full/dashboard/src/pages/api/patterns/stats.ts +39 -0
- package/templates/full/dashboard/src/pages/api/patterns/suggestions.ts +43 -0
- package/templates/full/dashboard/src/pages/api/reports/audit.ts +45 -0
- package/templates/full/dashboard/src/pages/api/reports/usage.ts +52 -0
- package/templates/full/dashboard/src/pages/api/search/index.ts +74 -0
- package/templates/full/dashboard/src/pages/api/search/reindex.ts +28 -0
- package/templates/full/dashboard/src/pages/api/search/stats.ts +27 -0
- package/templates/full/dashboard/src/pages/api/settings/index.ts +37 -0
- package/templates/full/dashboard/src/pages/api/settings/update.ts +41 -0
- package/templates/full/dashboard/src/pages/api/topology/index.ts +56 -0
- package/templates/full/dashboard/src/pages/notifications.astro +11 -0
- package/templates/full/migrations/008_auditor.sql +99 -0
- package/templates/full/migrations/010_pricing_versions.sql +110 -0
- package/templates/full/migrations/011_multi_account.sql +51 -0
- package/templates/full/scripts/ops/set-kv-pricing.ts +182 -0
- package/templates/full/scripts/ops/universal-backfill.ts +147 -0
- package/templates/full/workers/lib/ai-judge-schema.ts +181 -0
- package/templates/full/workers/lib/auditor/comprehensive-report.ts +407 -0
- package/templates/full/workers/lib/auditor/feature-coverage.ts +348 -0
- package/templates/full/workers/lib/auditor/index.ts +9 -0
- package/templates/full/workers/lib/auditor/types.ts +167 -0
- package/templates/full/workers/platform-auditor.ts +1071 -0
- package/templates/full/wrangler.auditor.jsonc.hbs +75 -0
- package/templates/shared/.github/workflows/contract-check.yml.hbs +42 -0
- package/templates/shared/.github/workflows/dashboard-deploy.yml.hbs +39 -0
- package/templates/shared/.github/workflows/platform-check.yml.hbs +28 -0
- package/templates/shared/.github/workflows/security.yml +33 -0
- package/templates/shared/config/observability.yaml.hbs +276 -0
- package/templates/shared/contracts/schemas/envelope.v1.schema.json +64 -0
- package/templates/shared/contracts/schemas/error_report.v1.schema.json +65 -0
- package/templates/shared/contracts/types/telemetry-envelope.types.ts +139 -0
- package/templates/shared/dashboard/astro.config.mjs +21 -0
- package/templates/shared/dashboard/package.json.hbs +29 -0
- package/templates/shared/dashboard/src/components/Header.astro +29 -0
- package/templates/shared/dashboard/src/components/Nav.astro.hbs +59 -0
- package/templates/shared/dashboard/src/components/infrastructure/AlertHistory.tsx +57 -0
- package/templates/shared/dashboard/src/components/infrastructure/InfrastructureStats.tsx +73 -0
- package/templates/shared/dashboard/src/components/infrastructure/ServiceRegistry.tsx +55 -0
- package/templates/shared/dashboard/src/components/infrastructure/UptimeStatus.tsx +56 -0
- package/templates/shared/dashboard/src/components/infrastructure/index.ts +4 -0
- package/templates/shared/dashboard/src/components/overview/ActivityFeed.tsx +134 -0
- package/templates/shared/dashboard/src/components/overview/CostQuadrant.tsx +131 -0
- package/templates/shared/dashboard/src/components/overview/ErrorsQuadrant.tsx +113 -0
- package/templates/shared/dashboard/src/components/overview/HealthQuadrant.tsx +87 -0
- package/templates/shared/dashboard/src/components/overview/MissionControl.tsx +139 -0
- package/templates/shared/dashboard/src/components/resources/AllowanceStatus.tsx +44 -0
- package/templates/shared/dashboard/src/components/resources/CostCentreOverview.tsx +42 -0
- package/templates/shared/dashboard/src/components/resources/ResourceTabs.tsx +69 -0
- package/templates/shared/dashboard/src/components/resources/index.ts +3 -0
- package/templates/shared/dashboard/src/components/settings/SettingsCard.tsx +21 -0
- package/templates/shared/dashboard/src/components/settings/index.ts +1 -0
- package/templates/shared/dashboard/src/components/ui/AlertBanner.tsx +39 -0
- package/templates/shared/dashboard/src/components/ui/Breadcrumbs.tsx +27 -0
- package/templates/shared/dashboard/src/components/ui/EmptyState.tsx +26 -0
- package/templates/shared/dashboard/src/components/ui/ErrorBoundary.tsx +42 -0
- package/templates/shared/dashboard/src/components/ui/LoadingSkeleton.tsx +18 -0
- package/templates/shared/dashboard/src/components/ui/PageShell.tsx +26 -0
- package/templates/shared/dashboard/src/components/ui/Sparkline.tsx +127 -0
- package/templates/shared/dashboard/src/components/ui/StatusDot.tsx +21 -0
- package/templates/shared/dashboard/src/components/ui/Toast.tsx +44 -0
- package/templates/shared/dashboard/src/components/ui/index.ts +9 -0
- package/templates/shared/dashboard/src/components/usage/AnomaliesWidget.tsx +68 -0
- package/templates/shared/dashboard/src/components/usage/HourlyUsageChart.tsx +55 -0
- package/templates/shared/dashboard/src/components/usage/PlanAllowanceDashboard.tsx +67 -0
- package/templates/shared/dashboard/src/components/usage/ProjectCostBreakdown.tsx +55 -0
- package/templates/shared/dashboard/src/components/usage/index.ts +4 -0
- package/templates/shared/dashboard/src/env.d.ts.hbs +34 -0
- package/templates/shared/dashboard/src/layouts/DashboardLayout.astro +37 -0
- package/templates/shared/dashboard/src/lib/cloudflare/costs.ts +21 -0
- package/templates/shared/dashboard/src/lib/fetch.ts +29 -0
- package/templates/shared/dashboard/src/lib/types.ts +72 -0
- package/templates/shared/dashboard/src/middleware/auth.ts +100 -0
- package/templates/shared/dashboard/src/middleware/index.ts +1 -0
- package/templates/shared/dashboard/src/pages/api/costs/overview.ts +65 -0
- package/templates/shared/dashboard/src/pages/api/costs/providers.ts +47 -0
- package/templates/shared/dashboard/src/pages/api/infrastructure/services.ts +55 -0
- package/templates/shared/dashboard/src/pages/api/infrastructure/stats.ts +99 -0
- package/templates/shared/dashboard/src/pages/api/overview/summary.ts +311 -0
- package/templates/shared/dashboard/src/pages/api/usage/allowances.ts +56 -0
- package/templates/shared/dashboard/src/pages/api/usage/anomalies.ts +45 -0
- package/templates/shared/dashboard/src/pages/api/usage/billing.ts +53 -0
- package/templates/shared/dashboard/src/pages/api/usage/circuit-breakers.ts +44 -0
- package/templates/shared/dashboard/src/pages/api/usage/granular.ts +50 -0
- package/templates/shared/dashboard/src/pages/api/usage/hourly.ts +45 -0
- package/templates/shared/dashboard/src/pages/api/usage/projects.ts +51 -0
- package/templates/shared/dashboard/src/pages/api/usage/status.ts +42 -0
- package/templates/shared/dashboard/src/pages/api/user/identity.ts +11 -0
- package/templates/shared/dashboard/src/pages/dashboard.astro +11 -0
- package/templates/shared/dashboard/src/pages/index.astro +3 -0
- package/templates/shared/dashboard/src/pages/resources.astro +11 -0
- package/templates/shared/dashboard/src/pages/settings/index.astro +28 -0
- package/templates/shared/dashboard/src/pages/settings/notifications.astro +34 -0
- package/templates/shared/dashboard/src/pages/settings/thresholds.astro +39 -0
- package/templates/shared/dashboard/src/pages/settings/usage.astro +28 -0
- package/templates/shared/dashboard/src/styles/global.css +29 -0
- package/templates/shared/dashboard/tailwind.config.mjs +9 -0
- package/templates/shared/dashboard/tsconfig.json +9 -0
- package/templates/shared/dashboard/wrangler.json.hbs +47 -0
- package/templates/shared/docs/architecture.md +89 -0
- package/templates/shared/docs/post-deploy-runbook.md +126 -0
- package/templates/shared/docs/troubleshooting.md +91 -0
- package/templates/shared/package.json.hbs +17 -1
- package/templates/shared/scripts/ops/backfill-cloudflare-daily.ts +145 -0
- package/templates/shared/scripts/ops/backfill-cloudflare-hourly.ts +473 -0
- package/templates/shared/scripts/ops/backfill-monthly-rollups.ts +125 -0
- package/templates/shared/scripts/ops/discover-graphql-datasets.ts +482 -0
- package/templates/shared/scripts/ops/reset-budget-state.ts +279 -0
- package/templates/shared/scripts/ops/validate-controls.js +141 -0
- package/templates/shared/scripts/ops/validate-pipeline.ts +237 -0
- package/templates/shared/scripts/ops/verify-account-completeness.ts +236 -0
- package/templates/shared/scripts/validate-schemas.js +61 -0
- package/templates/shared/tests/contract/validate-schemas.test.ts +130 -0
- package/templates/shared/tests/fixtures/telemetry-envelope-invalid.json +9 -0
- package/templates/shared/tests/fixtures/telemetry-envelope-valid.json +27 -0
- package/templates/shared/tests/helpers/mock-d1.ts +61 -0
- package/templates/shared/tests/helpers/mock-kv.ts +37 -0
- package/templates/shared/tests/unit/workers/batch-persistence.test.ts +133 -0
- package/templates/shared/tests/unit/workers/budget-enforcement.test.ts +214 -0
- package/templates/shared/vitest.config.ts +18 -0
- package/templates/shared/workers/lib/usage/collectors/anthropic.ts +114 -0
- package/templates/shared/workers/lib/usage/collectors/apify.ts +96 -0
- package/templates/shared/workers/lib/usage/collectors/custom-http.ts +151 -0
- package/templates/shared/workers/lib/usage/collectors/deepseek.ts +92 -0
- package/templates/shared/workers/lib/usage/collectors/gemini.ts +263 -0
- package/templates/shared/workers/lib/usage/collectors/github.ts +362 -0
- package/templates/shared/workers/lib/usage/collectors/index.ts +31 -15
- package/templates/shared/workers/lib/usage/collectors/minimax.ts +106 -0
- package/templates/shared/workers/lib/usage/collectors/openai.ts +171 -0
- package/templates/shared/workers/lib/usage/collectors/resend.ts +79 -0
- package/templates/shared/workers/lib/usage/collectors/stripe.ts +192 -0
- package/templates/shared/workers/lib/usage/shared/types.ts +46 -0
- package/templates/shared/workers/platform-usage.ts +98 -8
- package/templates/standard/dashboard/src/components/errors/ErrorStats.tsx +53 -0
- package/templates/standard/dashboard/src/components/errors/ErrorsTable.tsx +133 -0
- package/templates/standard/dashboard/src/components/errors/index.ts +2 -0
- package/templates/standard/dashboard/src/components/health/CircuitBreakerEvents.tsx +69 -0
- package/templates/standard/dashboard/src/components/health/CircuitBreakerPanel.tsx +97 -0
- package/templates/standard/dashboard/src/components/health/DlqStatusCard.tsx +52 -0
- package/templates/standard/dashboard/src/components/health/HealthTabs.tsx +86 -0
- package/templates/standard/dashboard/src/components/health/index.ts +4 -0
- package/templates/standard/dashboard/src/lib/errors.ts +28 -0
- package/templates/standard/dashboard/src/pages/api/errors/[fingerprint]/mute.ts +49 -0
- package/templates/standard/dashboard/src/pages/api/errors/[fingerprint]/resolve.ts +36 -0
- package/templates/standard/dashboard/src/pages/api/errors/[fingerprint].ts +55 -0
- package/templates/standard/dashboard/src/pages/api/errors/index.ts +58 -0
- package/templates/standard/dashboard/src/pages/api/errors/stats.ts +55 -0
- package/templates/standard/dashboard/src/pages/api/health/audit-history.ts +37 -0
- package/templates/standard/dashboard/src/pages/api/health/dlq.ts +43 -0
- package/templates/standard/dashboard/src/pages/circuit-breakers.astro +13 -0
- package/templates/standard/dashboard/src/pages/errors.astro +13 -0
- package/templates/standard/dashboard/src/pages/health.astro +11 -0
- package/templates/standard/migrations/009_topology_mapper.sql +65 -0
- package/templates/standard/tests/unit/error-collector/capture.test.ts +106 -0
- package/templates/standard/tests/unit/error-collector/fingerprint.test.ts +155 -0
- package/templates/standard/workers/lib/error-collector/email-health-alerts.ts +37 -3
- package/templates/standard/workers/lib/error-collector/gap-alerts.ts +32 -1
- package/templates/standard/workers/lib/mapper/attribution-check.ts +339 -0
- package/templates/standard/workers/lib/mapper/index.ts +7 -0
- package/templates/standard/workers/platform-mapper.ts +482 -0
- package/templates/standard/workers/platform-sdk-test-client.ts +125 -0
- package/templates/standard/wrangler.mapper.jsonc.hbs +85 -0
- package/templates/standard/wrangler.sdk-test-client.jsonc.hbs +62 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Nav from '../components/Nav.astro';
|
|
3
|
+
import Header from '../components/Header.astro';
|
|
4
|
+
import '../styles/global.css';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
title: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { title } = Astro.props;
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
<!doctype html>
|
|
14
|
+
<html lang="en" class="h-full">
|
|
15
|
+
<head>
|
|
16
|
+
<meta charset="utf-8" />
|
|
17
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
18
|
+
<title>{title} | Platform Dashboard</title>
|
|
19
|
+
<script is:inline>
|
|
20
|
+
if (localStorage.getItem('theme') === 'dark' ||
|
|
21
|
+
(!localStorage.getItem('theme') && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
|
22
|
+
document.documentElement.classList.add('dark');
|
|
23
|
+
}
|
|
24
|
+
</script>
|
|
25
|
+
</head>
|
|
26
|
+
<body class="h-full bg-gray-50 dark:bg-gray-900">
|
|
27
|
+
<div class="flex h-full">
|
|
28
|
+
<Nav />
|
|
29
|
+
<div class="flex-1 flex flex-col min-w-0">
|
|
30
|
+
<Header />
|
|
31
|
+
<main class="flex-1 overflow-y-auto p-4 lg:p-6">
|
|
32
|
+
<slot />
|
|
33
|
+
</main>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
</body>
|
|
37
|
+
</html>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Workers Paid Plan monthly allowances (used to compute net costs). */
|
|
2
|
+
export const CF_PAID_ALLOWANCES: Record<string, number> = {
|
|
3
|
+
d1_reads: 25_000_000_000,
|
|
4
|
+
d1_writes: 50_000_000,
|
|
5
|
+
kv_reads: 10_000_000,
|
|
6
|
+
kv_writes: 1_000_000,
|
|
7
|
+
worker_requests: 10_000_000,
|
|
8
|
+
r2_reads: 10_000_000,
|
|
9
|
+
r2_writes: 1_000_000,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/** Per-unit pricing after allowances are exhausted (USD). */
|
|
13
|
+
export const CF_PRICING: Record<string, number> = {
|
|
14
|
+
d1_reads: 0.001 / 4_000_000, // $0.001 per 4M rows
|
|
15
|
+
d1_writes: 1.0 / 1_000_000, // $1.00 per 1M rows
|
|
16
|
+
kv_reads: 0.50 / 1_000_000, // $0.50 per 1M reads
|
|
17
|
+
kv_writes: 5.0 / 1_000_000, // $5.00 per 1M writes
|
|
18
|
+
worker_requests: 0.30 / 1_000_000, // $0.30 per 1M requests
|
|
19
|
+
r2_reads: 0.36 / 1_000_000, // $0.36 per 1M Class B
|
|
20
|
+
r2_writes: 4.50 / 1_000_000, // $4.50 per 1M Class A
|
|
21
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const inflight = new Map<string, Promise<unknown>>();
|
|
2
|
+
|
|
3
|
+
export async function fetchWithDedup<T>(url: string, init?: RequestInit): Promise<T> {
|
|
4
|
+
const key = `${init?.method ?? 'GET'}:${url}`;
|
|
5
|
+
|
|
6
|
+
if (inflight.has(key)) {
|
|
7
|
+
return inflight.get(key) as Promise<T>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const promise = (async () => {
|
|
11
|
+
const controller = new AbortController();
|
|
12
|
+
const timeout = setTimeout(() => controller.abort(), 10000);
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const res = await fetch(url, { ...init, signal: controller.signal });
|
|
16
|
+
clearTimeout(timeout);
|
|
17
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
18
|
+
return (await res.json()) as T;
|
|
19
|
+
} catch (error) {
|
|
20
|
+
clearTimeout(timeout);
|
|
21
|
+
throw error;
|
|
22
|
+
} finally {
|
|
23
|
+
inflight.delete(key);
|
|
24
|
+
}
|
|
25
|
+
})();
|
|
26
|
+
|
|
27
|
+
inflight.set(key, promise);
|
|
28
|
+
return promise;
|
|
29
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export interface OverviewSummary {
|
|
2
|
+
health: {
|
|
3
|
+
servicesTotal: number;
|
|
4
|
+
servicesUp: number;
|
|
5
|
+
servicesDown: number;
|
|
6
|
+
uptimePct: number;
|
|
7
|
+
lastAuditScore: number | null;
|
|
8
|
+
lastAuditDate: string | null;
|
|
9
|
+
};
|
|
10
|
+
errors: {
|
|
11
|
+
p0Count: number;
|
|
12
|
+
p1Count: number;
|
|
13
|
+
p2Count: number;
|
|
14
|
+
p3Count: number;
|
|
15
|
+
p4Count: number;
|
|
16
|
+
newToday: number;
|
|
17
|
+
dailyTrend: number[];
|
|
18
|
+
topErrors: Array<{
|
|
19
|
+
fingerprint: string;
|
|
20
|
+
message: string;
|
|
21
|
+
script_name: string;
|
|
22
|
+
priority: string;
|
|
23
|
+
occurrence_count: number;
|
|
24
|
+
}>;
|
|
25
|
+
};
|
|
26
|
+
costs: {
|
|
27
|
+
mtdSpend: number;
|
|
28
|
+
dailyBurnRate: number;
|
|
29
|
+
projectedMonthly: number;
|
|
30
|
+
budgetPct: number;
|
|
31
|
+
monthlyBudget: number;
|
|
32
|
+
dailyTrend: number[];
|
|
33
|
+
};
|
|
34
|
+
activity: {
|
|
35
|
+
notifications: Array<{
|
|
36
|
+
id: string;
|
|
37
|
+
title: string;
|
|
38
|
+
category: string;
|
|
39
|
+
priority: string;
|
|
40
|
+
source: string;
|
|
41
|
+
created_at: number;
|
|
42
|
+
action_url: string | null;
|
|
43
|
+
}>;
|
|
44
|
+
pendingPatterns: number;
|
|
45
|
+
};
|
|
46
|
+
alerts: {
|
|
47
|
+
hasP0P1: boolean;
|
|
48
|
+
trippedBreakers: number;
|
|
49
|
+
warningBreakers: number;
|
|
50
|
+
servicesDown: number;
|
|
51
|
+
};
|
|
52
|
+
dataQuality?: {
|
|
53
|
+
latestSnapshot: string | null;
|
|
54
|
+
snapshotAgeMinutes: number;
|
|
55
|
+
status: 'fresh' | 'stale' | 'unknown';
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface CircuitBreakerState {
|
|
60
|
+
key: string;
|
|
61
|
+
feature: string;
|
|
62
|
+
status: string;
|
|
63
|
+
reason: string | null;
|
|
64
|
+
trippedAt: string | null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface UsageStatus {
|
|
68
|
+
status: string;
|
|
69
|
+
latestSnapshot: string | null;
|
|
70
|
+
latestCost: number;
|
|
71
|
+
trackedFeatures: number;
|
|
72
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { MiddlewareHandler } from 'astro';
|
|
2
|
+
import { createRemoteJWKSet, jwtVerify } from 'jose';
|
|
3
|
+
|
|
4
|
+
type AccessEnv = {
|
|
5
|
+
CF_ACCESS_AUD?: string;
|
|
6
|
+
CF_ACCESS_ISSUER?: string;
|
|
7
|
+
CF_ACCESS_TEAM_DOMAIN?: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const jwksCache = new Map<string, ReturnType<typeof createRemoteJWKSet>>();
|
|
11
|
+
|
|
12
|
+
function resolveIssuer(env: AccessEnv): string | null {
|
|
13
|
+
if (env.CF_ACCESS_ISSUER) {
|
|
14
|
+
return env.CF_ACCESS_ISSUER.replace(/\/$/, '');
|
|
15
|
+
}
|
|
16
|
+
if (env.CF_ACCESS_TEAM_DOMAIN) {
|
|
17
|
+
const domain = env.CF_ACCESS_TEAM_DOMAIN.replace(/https?:\/\//, '').replace(/\/$/, '');
|
|
18
|
+
return `https://${domain}`;
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function parseAudiences(env: AccessEnv): string[] {
|
|
24
|
+
if (!env.CF_ACCESS_AUD) return [];
|
|
25
|
+
return env.CF_ACCESS_AUD.split(',').map((aud) => aud.trim()).filter(Boolean);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getRemoteJwks(issuer: string) {
|
|
29
|
+
if (!jwksCache.has(issuer)) {
|
|
30
|
+
const jwksUrl = new URL('/cdn-cgi/access/certs', issuer);
|
|
31
|
+
jwksCache.set(issuer, createRemoteJWKSet(jwksUrl));
|
|
32
|
+
}
|
|
33
|
+
return jwksCache.get(issuer)!;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isDevelopment() {
|
|
37
|
+
return typeof import.meta !== 'undefined' && import.meta.env && import.meta.env.DEV;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const onRequest: MiddlewareHandler = async (context, next) => {
|
|
41
|
+
const url = new URL(context.request.url);
|
|
42
|
+
const pathname = url.pathname;
|
|
43
|
+
|
|
44
|
+
// Allow API routes to handle their own auth if needed
|
|
45
|
+
if (pathname.startsWith('/api/')) {
|
|
46
|
+
return next();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const runtimeEnv = (context.locals.runtime?.env ?? {}) as AccessEnv;
|
|
50
|
+
const issuer = resolveIssuer(runtimeEnv);
|
|
51
|
+
const audiences = parseAudiences(runtimeEnv);
|
|
52
|
+
|
|
53
|
+
// Bypass auth during build/prerender
|
|
54
|
+
if (!issuer && audiences.length === 0) {
|
|
55
|
+
const jwt = context.request.headers.get('cf-access-jwt-assertion');
|
|
56
|
+
if (!jwt) {
|
|
57
|
+
return next();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const jwt =
|
|
62
|
+
context.request.headers.get('cf-access-jwt-assertion') ??
|
|
63
|
+
context.request.headers.get('Cf-Access-Jwt-Assertion');
|
|
64
|
+
|
|
65
|
+
if (!jwt) {
|
|
66
|
+
if (isDevelopment() && !issuer) {
|
|
67
|
+
console.warn('[auth] Missing Access JWT in development; skipping validation');
|
|
68
|
+
return next();
|
|
69
|
+
}
|
|
70
|
+
return new Response('Unauthorized', { status: 401 });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!issuer || audiences.length === 0) {
|
|
74
|
+
if (isDevelopment()) {
|
|
75
|
+
console.warn('[auth] Missing Access configuration in development; skipping validation');
|
|
76
|
+
return next();
|
|
77
|
+
}
|
|
78
|
+
console.error('[auth] Access configuration missing: expected CF_ACCESS_ISSUER (or CF_ACCESS_TEAM_DOMAIN) and CF_ACCESS_AUD');
|
|
79
|
+
return new Response('Server configuration error', { status: 500 });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const jwks = getRemoteJwks(issuer);
|
|
84
|
+
const verification = await jwtVerify(jwt, jwks, { issuer, audience: audiences });
|
|
85
|
+
const payload = verification.payload as Record<string, unknown>;
|
|
86
|
+
|
|
87
|
+
context.locals.user = {
|
|
88
|
+
email: typeof payload.email === 'string' ? payload.email : undefined,
|
|
89
|
+
name: typeof payload.name === 'string' ? payload.name : undefined,
|
|
90
|
+
sub: typeof payload.sub === 'string' ? payload.sub : undefined,
|
|
91
|
+
audience: payload.aud,
|
|
92
|
+
issuer: payload.iss,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
return next();
|
|
96
|
+
} catch (error) {
|
|
97
|
+
console.error('[auth] Access JWT validation failed', error);
|
|
98
|
+
return new Response('Unauthorized', { status: 401 });
|
|
99
|
+
}
|
|
100
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { onRequest } from './auth';
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { APIRoute } from 'astro';
|
|
2
|
+
import { CF_PAID_ALLOWANCES, CF_PRICING } from '../../../lib/cloudflare/costs';
|
|
3
|
+
|
|
4
|
+
interface Env {
|
|
5
|
+
PLATFORM_DB?: D1Database;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface CostLine {
|
|
9
|
+
resource: string;
|
|
10
|
+
used: number;
|
|
11
|
+
allowance: number;
|
|
12
|
+
overage: number;
|
|
13
|
+
cost: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const GET: APIRoute = async ({ locals }) => {
|
|
17
|
+
const env = locals.runtime?.env as Env | undefined;
|
|
18
|
+
const db = env?.PLATFORM_DB;
|
|
19
|
+
|
|
20
|
+
const overview = {
|
|
21
|
+
lines: [] as CostLine[],
|
|
22
|
+
totalCost: 0,
|
|
23
|
+
period: 'mtd',
|
|
24
|
+
billingStart: '',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const now = new Date();
|
|
28
|
+
overview.billingStart = new Date(now.getFullYear(), now.getMonth(), 1).toISOString().slice(0, 10);
|
|
29
|
+
|
|
30
|
+
if (db) {
|
|
31
|
+
try {
|
|
32
|
+
const result = await db
|
|
33
|
+
.prepare(
|
|
34
|
+
`SELECT SUM(d1_reads) as d1_reads, SUM(d1_writes) as d1_writes,
|
|
35
|
+
SUM(kv_reads) as kv_reads, SUM(kv_writes) as kv_writes,
|
|
36
|
+
SUM(worker_requests) as worker_requests,
|
|
37
|
+
SUM(r2_reads) as r2_reads, SUM(r2_writes) as r2_writes
|
|
38
|
+
FROM daily_usage_rollups
|
|
39
|
+
WHERE project = 'all' AND snapshot_date >= ?
|
|
40
|
+
LIMIT 1`
|
|
41
|
+
)
|
|
42
|
+
.bind(overview.billingStart)
|
|
43
|
+
.first<Record<string, number>>();
|
|
44
|
+
|
|
45
|
+
if (result) {
|
|
46
|
+
for (const [resource, allowance] of Object.entries(CF_PAID_ALLOWANCES)) {
|
|
47
|
+
const used = result[resource] ?? 0;
|
|
48
|
+
const overage = Math.max(0, used - allowance);
|
|
49
|
+
const unitPrice = CF_PRICING[resource] ?? 0;
|
|
50
|
+
const cost = Math.round(overage * unitPrice * 100) / 100;
|
|
51
|
+
|
|
52
|
+
overview.lines.push({ resource, used, allowance, overage, cost });
|
|
53
|
+
overview.totalCost += cost;
|
|
54
|
+
}
|
|
55
|
+
overview.totalCost = Math.round(overview.totalCost * 100) / 100;
|
|
56
|
+
}
|
|
57
|
+
} catch {
|
|
58
|
+
// Table may not exist
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return new Response(JSON.stringify(overview), {
|
|
63
|
+
headers: { 'Content-Type': 'application/json', 'Cache-Control': 'max-age=300' },
|
|
64
|
+
});
|
|
65
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { APIRoute } from 'astro';
|
|
2
|
+
|
|
3
|
+
interface Env {
|
|
4
|
+
PLATFORM_DB?: D1Database;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export const GET: APIRoute = async ({ locals }) => {
|
|
8
|
+
const env = locals.runtime?.env as Env | undefined;
|
|
9
|
+
const db = env?.PLATFORM_DB;
|
|
10
|
+
|
|
11
|
+
const providers: Array<{
|
|
12
|
+
provider: string;
|
|
13
|
+
mtd_cost: number;
|
|
14
|
+
latest_date: string;
|
|
15
|
+
}> = [];
|
|
16
|
+
|
|
17
|
+
if (db) {
|
|
18
|
+
try {
|
|
19
|
+
const monthStart = new Date();
|
|
20
|
+
monthStart.setDate(1);
|
|
21
|
+
const cutoff = monthStart.toISOString().slice(0, 10);
|
|
22
|
+
|
|
23
|
+
const result = await db
|
|
24
|
+
.prepare(
|
|
25
|
+
`SELECT provider,
|
|
26
|
+
SUM(cost_usd) as mtd_cost,
|
|
27
|
+
MAX(snapshot_date) as latest_date
|
|
28
|
+
FROM third_party_usage
|
|
29
|
+
WHERE snapshot_date >= ?
|
|
30
|
+
GROUP BY provider
|
|
31
|
+
ORDER BY mtd_cost DESC
|
|
32
|
+
LIMIT 20`
|
|
33
|
+
)
|
|
34
|
+
.bind(cutoff)
|
|
35
|
+
.all();
|
|
36
|
+
if (result.results) {
|
|
37
|
+
providers.push(...(result.results as typeof providers));
|
|
38
|
+
}
|
|
39
|
+
} catch {
|
|
40
|
+
// Table may not exist
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return new Response(JSON.stringify({ providers }), {
|
|
45
|
+
headers: { 'Content-Type': 'application/json', 'Cache-Control': 'max-age=300' },
|
|
46
|
+
});
|
|
47
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { APIRoute } from 'astro';
|
|
2
|
+
|
|
3
|
+
interface Env {
|
|
4
|
+
PLATFORM_DB?: D1Database;
|
|
5
|
+
PLATFORM_CACHE?: KVNamespace;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const GET: APIRoute = async ({ locals }) => {
|
|
9
|
+
const env = locals.runtime?.env as Env | undefined;
|
|
10
|
+
const db = env?.PLATFORM_DB;
|
|
11
|
+
const kv = env?.PLATFORM_CACHE;
|
|
12
|
+
|
|
13
|
+
let services: Array<{
|
|
14
|
+
name: string;
|
|
15
|
+
type: string;
|
|
16
|
+
project: string;
|
|
17
|
+
status: string;
|
|
18
|
+
}> = [];
|
|
19
|
+
|
|
20
|
+
// Try KV first
|
|
21
|
+
if (kv) {
|
|
22
|
+
try {
|
|
23
|
+
const registry = await kv.get('SERVICE_REGISTRY', 'json');
|
|
24
|
+
if (registry && Array.isArray(registry)) {
|
|
25
|
+
services = registry as typeof services;
|
|
26
|
+
}
|
|
27
|
+
} catch {
|
|
28
|
+
// KV may not be available
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Fall back to D1
|
|
33
|
+
if (services.length === 0 && db) {
|
|
34
|
+
try {
|
|
35
|
+
const result = await db
|
|
36
|
+
.prepare(
|
|
37
|
+
`SELECT resource_name as name, resource_type as type,
|
|
38
|
+
project_id as project, 'deployed' as status
|
|
39
|
+
FROM resource_project_mapping
|
|
40
|
+
ORDER BY project_id, resource_type
|
|
41
|
+
LIMIT 200`
|
|
42
|
+
)
|
|
43
|
+
.all();
|
|
44
|
+
if (result.results) {
|
|
45
|
+
services = result.results as typeof services;
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
// Table may not exist
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return new Response(JSON.stringify({ services }), {
|
|
53
|
+
headers: { 'Content-Type': 'application/json', 'Cache-Control': 'max-age=120' },
|
|
54
|
+
});
|
|
55
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { APIRoute } from 'astro';
|
|
2
|
+
|
|
3
|
+
interface Env {
|
|
4
|
+
PLATFORM_DB?: D1Database;
|
|
5
|
+
PLATFORM_CACHE?: KVNamespace;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const GET: APIRoute = async ({ locals }) => {
|
|
9
|
+
const env = locals.runtime?.env as Env | undefined;
|
|
10
|
+
const db = env?.PLATFORM_DB;
|
|
11
|
+
const kv = env?.PLATFORM_CACHE;
|
|
12
|
+
|
|
13
|
+
const stats = {
|
|
14
|
+
services: {
|
|
15
|
+
total: 0,
|
|
16
|
+
byStatus: { deployed: 0, development: 0, deprecated: 0, paused: 0 },
|
|
17
|
+
byType: {} as Record<string, number>,
|
|
18
|
+
byProject: {} as Record<string, number>,
|
|
19
|
+
},
|
|
20
|
+
alerts: {
|
|
21
|
+
total: 0,
|
|
22
|
+
unacknowledged: 0,
|
|
23
|
+
critical: 0,
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// Try KV first (faster, populated by sync-config)
|
|
28
|
+
if (kv) {
|
|
29
|
+
try {
|
|
30
|
+
const services = await kv.get('SERVICE_REGISTRY', 'json');
|
|
31
|
+
if (services && Array.isArray(services)) {
|
|
32
|
+
stats.services.total = services.length;
|
|
33
|
+
for (const s of services as Array<{ status: string; type: string; project: string }>) {
|
|
34
|
+
const status = s.status as keyof typeof stats.services.byStatus;
|
|
35
|
+
if (status in stats.services.byStatus) {
|
|
36
|
+
stats.services.byStatus[status]++;
|
|
37
|
+
}
|
|
38
|
+
stats.services.byType[s.type] = (stats.services.byType[s.type] || 0) + 1;
|
|
39
|
+
stats.services.byProject[s.project] = (stats.services.byProject[s.project] || 0) + 1;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
// KV may not be available
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Fall back to D1 if KV had nothing
|
|
48
|
+
if (stats.services.total === 0 && db) {
|
|
49
|
+
try {
|
|
50
|
+
const rows = await db
|
|
51
|
+
.prepare(
|
|
52
|
+
`SELECT resource_type, project_id, COUNT(*) as cnt
|
|
53
|
+
FROM resource_project_mapping
|
|
54
|
+
GROUP BY resource_type, project_id
|
|
55
|
+
LIMIT 200`
|
|
56
|
+
)
|
|
57
|
+
.all<{ resource_type: string; project_id: string; cnt: number }>();
|
|
58
|
+
if (rows.results) {
|
|
59
|
+
for (const row of rows.results) {
|
|
60
|
+
stats.services.total += row.cnt;
|
|
61
|
+
stats.services.byStatus.deployed += row.cnt;
|
|
62
|
+
stats.services.byType[row.resource_type] = (stats.services.byType[row.resource_type] || 0) + row.cnt;
|
|
63
|
+
stats.services.byProject[row.project_id] = (stats.services.byProject[row.project_id] || 0) + row.cnt;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
} catch {
|
|
67
|
+
// Table may not exist
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Alert stats from notifications
|
|
72
|
+
if (db) {
|
|
73
|
+
try {
|
|
74
|
+
const alertResult = await db
|
|
75
|
+
.prepare(
|
|
76
|
+
`SELECT
|
|
77
|
+
COUNT(*) as total,
|
|
78
|
+
SUM(CASE WHEN category IN ('error', 'warning') THEN 1 ELSE 0 END) as unacknowledged,
|
|
79
|
+
SUM(CASE WHEN priority IN ('critical', 'high') AND category = 'error' THEN 1 ELSE 0 END) as critical
|
|
80
|
+
FROM notifications
|
|
81
|
+
WHERE created_at > unixepoch() - 86400 * 7
|
|
82
|
+
AND (expires_at IS NULL OR expires_at > unixepoch())
|
|
83
|
+
LIMIT 1`
|
|
84
|
+
)
|
|
85
|
+
.first<{ total: number; unacknowledged: number; critical: number }>();
|
|
86
|
+
if (alertResult) {
|
|
87
|
+
stats.alerts.total = alertResult.total || 0;
|
|
88
|
+
stats.alerts.unacknowledged = alertResult.unacknowledged || 0;
|
|
89
|
+
stats.alerts.critical = alertResult.critical || 0;
|
|
90
|
+
}
|
|
91
|
+
} catch {
|
|
92
|
+
// Table may not exist
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return new Response(JSON.stringify(stats), {
|
|
97
|
+
headers: { 'Content-Type': 'application/json', 'Cache-Control': 'max-age=120' },
|
|
98
|
+
});
|
|
99
|
+
};
|