@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,236 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
/**
|
|
3
|
+
* Verify Account Completeness Script
|
|
4
|
+
*
|
|
5
|
+
* Queries all Cloudflare GraphQL Analytics endpoints to calculate the true
|
|
6
|
+
* account-wide bill and compare against D1 records for the current month.
|
|
7
|
+
*
|
|
8
|
+
* Services audited:
|
|
9
|
+
* - Workers (requests + CPU)
|
|
10
|
+
* - D1 (rows read/written)
|
|
11
|
+
* - R2 (Class A/B operations)
|
|
12
|
+
* - KV (reads/writes)
|
|
13
|
+
* - Vectorize (queries)
|
|
14
|
+
* - Durable Objects (requests)
|
|
15
|
+
* - Workers AI (neurons)
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* npx tsx scripts/ops/verify-account-completeness.ts
|
|
19
|
+
* npx tsx scripts/ops/verify-account-completeness.ts --start 2026-02-01 --end 2026-02-28
|
|
20
|
+
*
|
|
21
|
+
* Environment Variables:
|
|
22
|
+
* CLOUDFLARE_API_TOKEN — API token with Analytics:Read permissions
|
|
23
|
+
* CLOUDFLARE_ACCOUNT_ID — Your Cloudflare account ID
|
|
24
|
+
* D1_DATABASE_ID — Your platform-metrics D1 database ID
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const GRAPHQL_ENDPOINT = 'https://api.cloudflare.com/client/v4/graphql';
|
|
28
|
+
const REST_API_BASE = 'https://api.cloudflare.com/client/v4';
|
|
29
|
+
|
|
30
|
+
// Pricing constants (Workers Paid Plan)
|
|
31
|
+
const PRICING = {
|
|
32
|
+
workers: { includedRequests: 10_000_000, requestCostPerMillion: 0.3, cpuCostPerMillionMs: 0.02 },
|
|
33
|
+
d1: { includedReads: 25_000_000_000, includedWrites: 50_000_000, readCostPerMillion: 0.001, writeCostPerMillion: 1.0 },
|
|
34
|
+
kv: { includedReads: 10_000_000, includedWrites: 1_000_000, readCostPerMillion: 0.5, writeCostPerMillion: 5.0 },
|
|
35
|
+
r2: { includedClassA: 1_000_000, includedClassB: 10_000_000, classACostPerMillion: 4.5, classBCostPerMillion: 0.36 },
|
|
36
|
+
vectorize: { includedDimensions: 30_000_000, queryCostPerMillionDimensions: 0.01 },
|
|
37
|
+
durableObjects: { includedRequests: 1_000_000, requestCostPerMillion: 0.15 },
|
|
38
|
+
workersAI: { neuronCostPer1000: 0.011 },
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
interface ServiceUsage {
|
|
42
|
+
name: string;
|
|
43
|
+
usage: string;
|
|
44
|
+
includedAllowance: string;
|
|
45
|
+
billableUsage: string;
|
|
46
|
+
estimatedCost: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// GraphQL helper
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
async function queryGraphQL(query: string, variables: Record<string, unknown>): Promise<unknown> {
|
|
54
|
+
const apiToken = process.env.CLOUDFLARE_API_TOKEN;
|
|
55
|
+
if (!apiToken) throw new Error('CLOUDFLARE_API_TOKEN required');
|
|
56
|
+
|
|
57
|
+
const response = await fetch(GRAPHQL_ENDPOINT, {
|
|
58
|
+
method: 'POST',
|
|
59
|
+
headers: { Authorization: `Bearer ${apiToken}`, 'Content-Type': 'application/json' },
|
|
60
|
+
body: JSON.stringify({ query, variables }),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const result = (await response.json()) as { data?: unknown; errors?: Array<{ message: string }> };
|
|
64
|
+
if (result.errors?.length) throw new Error(`GraphQL error: ${result.errors[0].message}`);
|
|
65
|
+
return result.data;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// Service queries
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
async function getWorkersUsage(accountId: string, startDate: string, endDate: string): Promise<ServiceUsage> {
|
|
73
|
+
const query = `query($accountTag:String!,$startDate:Date!,$endDate:Date!){viewer{accounts(filter:{accountTag:$accountTag}){workersInvocationsAdaptive(filter:{date_geq:$startDate,date_leq:$endDate},limit:1000){sum{requests,errors}quantiles{cpuTimeP50}}}}}`;
|
|
74
|
+
const data = (await queryGraphQL(query, { accountTag: accountId, startDate, endDate })) as Record<string, unknown>;
|
|
75
|
+
const results = ((data as { viewer: { accounts: Array<{ workersInvocationsAdaptive: Array<{ sum: { requests: number }; quantiles: { cpuTimeP50: number } }> }> } }).viewer.accounts[0]?.workersInvocationsAdaptive) ?? [];
|
|
76
|
+
let totalRequests = 0;
|
|
77
|
+
let totalCpuMs = 0;
|
|
78
|
+
for (const row of results) {
|
|
79
|
+
totalRequests += row.sum.requests;
|
|
80
|
+
totalCpuMs += row.sum.requests * row.quantiles.cpuTimeP50;
|
|
81
|
+
}
|
|
82
|
+
const billable = Math.max(0, totalRequests - PRICING.workers.includedRequests);
|
|
83
|
+
const cost = (billable / 1_000_000) * PRICING.workers.requestCostPerMillion + (totalCpuMs / 1_000_000) * PRICING.workers.cpuCostPerMillionMs;
|
|
84
|
+
return { name: 'Workers', usage: `${(totalRequests / 1_000_000).toFixed(2)}M requests`, includedAllowance: '10M requests', billableUsage: `${(billable / 1_000_000).toFixed(2)}M`, estimatedCost: cost };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function getD1Usage(accountId: string, startDate: string, endDate: string): Promise<ServiceUsage> {
|
|
88
|
+
const query = `query($accountTag:String!,$startDate:Date!,$endDate:Date!){viewer{accounts(filter:{accountTag:$accountTag}){d1AnalyticsAdaptiveGroups(filter:{date_geq:$startDate,date_leq:$endDate},limit:1000){sum{rowsRead,rowsWritten}}}}}`;
|
|
89
|
+
const data = (await queryGraphQL(query, { accountTag: accountId, startDate, endDate })) as Record<string, unknown>;
|
|
90
|
+
const results = ((data as { viewer: { accounts: Array<{ d1AnalyticsAdaptiveGroups: Array<{ sum: { rowsRead: number; rowsWritten: number } }> }> } }).viewer.accounts[0]?.d1AnalyticsAdaptiveGroups) ?? [];
|
|
91
|
+
let totalReads = 0, totalWrites = 0;
|
|
92
|
+
for (const row of results) { totalReads += row.sum.rowsRead; totalWrites += row.sum.rowsWritten; }
|
|
93
|
+
const billReads = Math.max(0, totalReads - PRICING.d1.includedReads);
|
|
94
|
+
const billWrites = Math.max(0, totalWrites - PRICING.d1.includedWrites);
|
|
95
|
+
const cost = (billReads / 1_000_000) * PRICING.d1.readCostPerMillion + (billWrites / 1_000_000) * PRICING.d1.writeCostPerMillion;
|
|
96
|
+
return { name: 'D1', usage: `${(totalWrites / 1_000_000).toFixed(1)}M writes, ${(totalReads / 1_000_000_000).toFixed(3)}B reads`, includedAllowance: '50M writes, 25B reads', billableUsage: `${(billWrites / 1_000_000).toFixed(1)}M writes`, estimatedCost: cost };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function getKVUsage(accountId: string, startDate: string, endDate: string): Promise<ServiceUsage> {
|
|
100
|
+
const query = `query($accountTag:String!,$startDate:Date!,$endDate:Date!){viewer{accounts(filter:{accountTag:$accountTag}){kvOperationsAdaptiveGroups(filter:{date_geq:$startDate,date_leq:$endDate},limit:1000){sum{requests}dimensions{actionType}}}}}`;
|
|
101
|
+
const data = (await queryGraphQL(query, { accountTag: accountId, startDate, endDate })) as Record<string, unknown>;
|
|
102
|
+
const results = ((data as { viewer: { accounts: Array<{ kvOperationsAdaptiveGroups: Array<{ sum: { requests: number }; dimensions: { actionType: string } }> }> } }).viewer.accounts[0]?.kvOperationsAdaptiveGroups) ?? [];
|
|
103
|
+
let reads = 0, writes = 0;
|
|
104
|
+
for (const row of results) {
|
|
105
|
+
const action = row.dimensions.actionType.toLowerCase();
|
|
106
|
+
if (action === 'read' || action === 'get') reads += row.sum.requests;
|
|
107
|
+
else if (action === 'write' || action === 'put') writes += row.sum.requests;
|
|
108
|
+
}
|
|
109
|
+
const billReads = Math.max(0, reads - PRICING.kv.includedReads);
|
|
110
|
+
const cost = (billReads / 1_000_000) * PRICING.kv.readCostPerMillion + (Math.max(0, writes - PRICING.kv.includedWrites) / 1_000_000) * PRICING.kv.writeCostPerMillion;
|
|
111
|
+
return { name: 'KV', usage: `${(reads / 1_000_000).toFixed(2)}M reads, ${(writes / 1_000).toFixed(1)}K writes`, includedAllowance: '10M reads, 1M writes', billableUsage: `${(billReads / 1_000_000).toFixed(2)}M reads`, estimatedCost: cost };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function getR2Usage(accountId: string, startDate: string, endDate: string): Promise<ServiceUsage> {
|
|
115
|
+
const query = `query($accountTag:String!,$startDate:Date!,$endDate:Date!){viewer{accounts(filter:{accountTag:$accountTag}){r2OperationsAdaptiveGroups(filter:{date_geq:$startDate,date_leq:$endDate},limit:1000){sum{requests}dimensions{actionType}}}}}`;
|
|
116
|
+
const data = (await queryGraphQL(query, { accountTag: accountId, startDate, endDate })) as Record<string, unknown>;
|
|
117
|
+
const results = ((data as { viewer: { accounts: Array<{ r2OperationsAdaptiveGroups: Array<{ sum: { requests: number }; dimensions: { actionType: string } }> }> } }).viewer.accounts[0]?.r2OperationsAdaptiveGroups) ?? [];
|
|
118
|
+
const classBActions = new Set(['GetObject', 'HeadObject', 'HeadBucket']);
|
|
119
|
+
let classA = 0, classB = 0;
|
|
120
|
+
for (const row of results) {
|
|
121
|
+
if (classBActions.has(row.dimensions.actionType)) classB += row.sum.requests;
|
|
122
|
+
else classA += row.sum.requests;
|
|
123
|
+
}
|
|
124
|
+
const billA = Math.max(0, classA - PRICING.r2.includedClassA);
|
|
125
|
+
const billB = Math.max(0, classB - PRICING.r2.includedClassB);
|
|
126
|
+
const cost = (billA / 1_000_000) * PRICING.r2.classACostPerMillion + (billB / 1_000_000) * PRICING.r2.classBCostPerMillion;
|
|
127
|
+
return { name: 'R2', usage: `${(classA / 1_000).toFixed(1)}K A, ${(classB / 1_000).toFixed(1)}K B`, includedAllowance: '1M A, 10M B', billableUsage: `${billA.toLocaleString()} A`, estimatedCost: cost };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
// D1 comparison
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
async function getD1RecordTotal(
|
|
135
|
+
accountId: string,
|
|
136
|
+
apiToken: string,
|
|
137
|
+
databaseId: string,
|
|
138
|
+
startDate: string,
|
|
139
|
+
endDate: string
|
|
140
|
+
): Promise<number> {
|
|
141
|
+
const sql = `SELECT SUM(total_cost_usd) as total FROM daily_usage_rollups WHERE snapshot_date BETWEEN '${startDate}' AND '${endDate}'`;
|
|
142
|
+
const url = `${REST_API_BASE}/accounts/${accountId}/d1/database/${databaseId}/query`;
|
|
143
|
+
const response = await fetch(url, {
|
|
144
|
+
method: 'POST',
|
|
145
|
+
headers: { Authorization: `Bearer ${apiToken}`, 'Content-Type': 'application/json' },
|
|
146
|
+
body: JSON.stringify({ sql }),
|
|
147
|
+
});
|
|
148
|
+
if (!response.ok) return 0;
|
|
149
|
+
const data = (await response.json()) as { result?: Array<{ results?: Array<{ total: number }> }> };
|
|
150
|
+
return data?.result?.[0]?.results?.[0]?.total ?? 0;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
// Main
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
async function main(): Promise<void> {
|
|
158
|
+
const apiToken = process.env.CLOUDFLARE_API_TOKEN;
|
|
159
|
+
const accountId = process.env.CLOUDFLARE_ACCOUNT_ID;
|
|
160
|
+
const databaseId = process.env.D1_DATABASE_ID;
|
|
161
|
+
|
|
162
|
+
if (!apiToken || !accountId || !databaseId) {
|
|
163
|
+
console.error('Error: Required environment variables: CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID, D1_DATABASE_ID');
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Parse date range
|
|
168
|
+
const args = process.argv.slice(2);
|
|
169
|
+
const now = new Date();
|
|
170
|
+
let startDate = `${now.getUTCFullYear()}-${String(now.getUTCMonth() + 1).padStart(2, '0')}-01`;
|
|
171
|
+
let endDate = now.toISOString().split('T')[0];
|
|
172
|
+
for (let i = 0; i < args.length; i++) {
|
|
173
|
+
if (args[i] === '--start' && args[i + 1]) startDate = args[++i];
|
|
174
|
+
else if (args[i] === '--end' && args[i + 1]) endDate = args[++i];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
console.log('='.repeat(80));
|
|
178
|
+
console.log(`ACCOUNT COMPLETENESS AUDIT (${startDate} to ${endDate})`);
|
|
179
|
+
console.log('='.repeat(80));
|
|
180
|
+
console.log('');
|
|
181
|
+
|
|
182
|
+
const services: ServiceUsage[] = [];
|
|
183
|
+
|
|
184
|
+
console.log('Querying Cloudflare GraphQL…');
|
|
185
|
+
for (const [name, fn] of [
|
|
186
|
+
['Workers', () => getWorkersUsage(accountId, startDate, endDate)],
|
|
187
|
+
['D1', () => getD1Usage(accountId, startDate, endDate)],
|
|
188
|
+
['KV', () => getKVUsage(accountId, startDate, endDate)],
|
|
189
|
+
['R2', () => getR2Usage(accountId, startDate, endDate)],
|
|
190
|
+
] as const) {
|
|
191
|
+
console.log(` ${name}…`);
|
|
192
|
+
services.push(await fn());
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
console.log('');
|
|
196
|
+
console.log('Service | Usage | Billable | Est. Cost');
|
|
197
|
+
console.log('-'.repeat(80));
|
|
198
|
+
|
|
199
|
+
let totalCF = 0;
|
|
200
|
+
for (const svc of services) {
|
|
201
|
+
totalCF += svc.estimatedCost;
|
|
202
|
+
console.log(`${svc.name.padEnd(16)} | ${svc.usage.padEnd(31)} | ${svc.billableUsage.padEnd(22)} | $${svc.estimatedCost.toFixed(2).padStart(8)}`);
|
|
203
|
+
}
|
|
204
|
+
console.log('-'.repeat(80));
|
|
205
|
+
console.log(`${'CF TOTAL'.padEnd(16)} | ${' '.repeat(31)} | ${' '.repeat(22)} | $${totalCF.toFixed(2).padStart(8)}`);
|
|
206
|
+
console.log('');
|
|
207
|
+
|
|
208
|
+
// Compare against D1
|
|
209
|
+
console.log('Querying D1 records…');
|
|
210
|
+
const d1Total = await getD1RecordTotal(accountId, apiToken, databaseId, startDate, endDate);
|
|
211
|
+
|
|
212
|
+
console.log('');
|
|
213
|
+
console.log('-'.repeat(80));
|
|
214
|
+
console.log('COMPARISON');
|
|
215
|
+
console.log('-'.repeat(80));
|
|
216
|
+
console.log(` Cloudflare Calculated: $${totalCF.toFixed(2)}`);
|
|
217
|
+
console.log(` Our D1 Records: $${d1Total.toFixed(2)}`);
|
|
218
|
+
console.log(` Variance: $${(totalCF - d1Total).toFixed(2)}`);
|
|
219
|
+
console.log('');
|
|
220
|
+
|
|
221
|
+
const variance = Math.abs(totalCF - d1Total);
|
|
222
|
+
const variancePct = (variance / Math.max(totalCF, d1Total, 0.01)) * 100;
|
|
223
|
+
|
|
224
|
+
if (variancePct < 5) {
|
|
225
|
+
console.log(`VERDICT: ACCURATE (${variancePct.toFixed(1)}% variance — within acceptable margin)`);
|
|
226
|
+
} else if (d1Total > totalCF) {
|
|
227
|
+
console.log(`VERDICT: OVER-RECORDED (D1 records exceed Cloudflare by $${(d1Total - totalCF).toFixed(2)})`);
|
|
228
|
+
} else {
|
|
229
|
+
console.log(`VERDICT: MISSING DATA (Cloudflare exceeds D1 by $${(totalCF - d1Total).toFixed(2)})`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
main().catch((error) => {
|
|
234
|
+
console.error('Fatal error:', error);
|
|
235
|
+
process.exit(1);
|
|
236
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON Schema Validation Script
|
|
3
|
+
*
|
|
4
|
+
* Validates all JSON schemas in contracts/schemas/ are syntactically correct
|
|
5
|
+
* using the Ajv JSON Schema validator.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* node scripts/validate-schemas.js
|
|
9
|
+
* npm run validate:schemas
|
|
10
|
+
*
|
|
11
|
+
* Prerequisites:
|
|
12
|
+
* npm install ajv ajv-formats --save-dev
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import Ajv from 'ajv';
|
|
16
|
+
import addFormats from 'ajv-formats';
|
|
17
|
+
import { readFileSync, readdirSync } from 'fs';
|
|
18
|
+
import { join } from 'path';
|
|
19
|
+
|
|
20
|
+
const ajv = new Ajv({ allErrors: true, strict: true });
|
|
21
|
+
addFormats(ajv);
|
|
22
|
+
|
|
23
|
+
const schemasDir = join(process.cwd(), 'contracts', 'schemas');
|
|
24
|
+
|
|
25
|
+
let schemaFiles;
|
|
26
|
+
try {
|
|
27
|
+
schemaFiles = readdirSync(schemasDir).filter((file) => file.endsWith('.schema.json'));
|
|
28
|
+
} catch {
|
|
29
|
+
console.error(`No schemas directory found at ${schemasDir}`);
|
|
30
|
+
console.error('Create contracts/schemas/ with your JSON schema files.');
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (schemaFiles.length === 0) {
|
|
35
|
+
console.log('No schema files found in contracts/schemas/');
|
|
36
|
+
process.exit(0);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let allValid = true;
|
|
40
|
+
|
|
41
|
+
for (const file of schemaFiles) {
|
|
42
|
+
const path = join(schemasDir, file);
|
|
43
|
+
const schema = JSON.parse(readFileSync(path, 'utf-8'));
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
ajv.compile(schema);
|
|
47
|
+
console.log(` PASS ${file}`);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.error(` FAIL ${file}: ${error.message}`);
|
|
50
|
+
allValid = false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
console.log('');
|
|
55
|
+
if (allValid) {
|
|
56
|
+
console.log(`All ${schemaFiles.length} schemas are valid.`);
|
|
57
|
+
process.exit(0);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
console.error('Some schemas are invalid.');
|
|
61
|
+
process.exit(1);
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract Tests for JSON Schemas
|
|
3
|
+
*
|
|
4
|
+
* Validates telemetry envelope + error report schemas against fixtures.
|
|
5
|
+
* Ensures contracts remain valid as schemas evolve.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, expect, it } from 'vitest';
|
|
9
|
+
import { readFileSync } from 'node:fs';
|
|
10
|
+
import { resolve } from 'node:path';
|
|
11
|
+
|
|
12
|
+
function loadJson(relativePath: string): unknown {
|
|
13
|
+
const fullPath = resolve(__dirname, '..', '..', relativePath);
|
|
14
|
+
return JSON.parse(readFileSync(fullPath, 'utf-8'));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// ===========================================================================
|
|
18
|
+
// Envelope Schema
|
|
19
|
+
// ===========================================================================
|
|
20
|
+
|
|
21
|
+
describe('telemetry envelope schema', () => {
|
|
22
|
+
const schema = loadJson('contracts/schemas/envelope.v1.schema.json') as {
|
|
23
|
+
required?: string[];
|
|
24
|
+
properties?: Record<string, unknown>;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
it('has required version 1 schema structure', () => {
|
|
28
|
+
expect(schema).toBeDefined();
|
|
29
|
+
expect(schema.required).toBeDefined();
|
|
30
|
+
expect(Array.isArray(schema.required)).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('requires core fields', () => {
|
|
34
|
+
const required = schema.required ?? [];
|
|
35
|
+
expect(required).toContain('version');
|
|
36
|
+
expect(required).toContain('project');
|
|
37
|
+
expect(required).toContain('feature');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('defines resource_usage properties', () => {
|
|
41
|
+
const props = schema.properties ?? {};
|
|
42
|
+
expect(props.resource_usage).toBeDefined();
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// ===========================================================================
|
|
47
|
+
// Valid Fixture
|
|
48
|
+
// ===========================================================================
|
|
49
|
+
|
|
50
|
+
describe('valid telemetry envelope fixture', () => {
|
|
51
|
+
const fixture = loadJson('tests/fixtures/telemetry-envelope-valid.json') as Record<string, unknown>;
|
|
52
|
+
|
|
53
|
+
it('has version 1', () => {
|
|
54
|
+
expect(fixture.version).toBe(1);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('has valid project and feature format', () => {
|
|
58
|
+
expect(typeof fixture.project).toBe('string');
|
|
59
|
+
expect(typeof fixture.feature).toBe('string');
|
|
60
|
+
const feature = fixture.feature as string;
|
|
61
|
+
expect(feature.split(':').length).toBeGreaterThanOrEqual(3);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('has resource_usage with numeric values', () => {
|
|
65
|
+
const usage = fixture.resource_usage as Record<string, unknown>;
|
|
66
|
+
expect(usage).toBeDefined();
|
|
67
|
+
expect(typeof usage.d1_reads).toBe('number');
|
|
68
|
+
expect(typeof usage.d1_writes).toBe('number');
|
|
69
|
+
expect(usage.d1_reads).toBeGreaterThanOrEqual(0);
|
|
70
|
+
expect(usage.d1_writes).toBeGreaterThanOrEqual(0);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('has valid handler_type', () => {
|
|
74
|
+
expect(['fetch', 'scheduled', 'queue', 'alarm', 'tail']).toContain(fixture.handler_type);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('has valid outcome', () => {
|
|
78
|
+
expect(['ok', 'error', 'exception', 'canceled']).toContain(fixture.outcome);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// ===========================================================================
|
|
83
|
+
// Invalid Fixture
|
|
84
|
+
// ===========================================================================
|
|
85
|
+
|
|
86
|
+
describe('invalid telemetry envelope fixture', () => {
|
|
87
|
+
const fixture = loadJson('tests/fixtures/telemetry-envelope-invalid.json') as Record<string, unknown>;
|
|
88
|
+
|
|
89
|
+
it('has wrong version', () => {
|
|
90
|
+
expect(fixture.version).not.toBe(1);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('has empty project', () => {
|
|
94
|
+
expect(fixture.project).toBe('');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('has malformed feature ID (missing colons)', () => {
|
|
98
|
+
const feature = fixture.feature as string;
|
|
99
|
+
expect(feature.split(':').length).toBeLessThan(3);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('has invalid resource_usage values', () => {
|
|
103
|
+
const usage = fixture.resource_usage as Record<string, unknown>;
|
|
104
|
+
const hasInvalid = Object.values(usage).some(
|
|
105
|
+
(v) => typeof v !== 'number' || v < 0
|
|
106
|
+
);
|
|
107
|
+
expect(hasInvalid).toBe(true);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// ===========================================================================
|
|
112
|
+
// Error Report Schema
|
|
113
|
+
// ===========================================================================
|
|
114
|
+
|
|
115
|
+
describe('error report schema', () => {
|
|
116
|
+
const schema = loadJson('contracts/schemas/error_report.v1.schema.json') as {
|
|
117
|
+
required?: string[];
|
|
118
|
+
properties?: Record<string, unknown>;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
it('has required schema structure', () => {
|
|
122
|
+
expect(schema).toBeDefined();
|
|
123
|
+
expect(schema.required).toBeDefined();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('requires error identification fields', () => {
|
|
127
|
+
const required = schema.required ?? [];
|
|
128
|
+
expect(required.length).toBeGreaterThan(0);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"timestamp": "2026-03-05T00:00:00Z",
|
|
4
|
+
"sdk_version": "1.4.0",
|
|
5
|
+
"project": "test-project",
|
|
6
|
+
"feature": "test-project:api:endpoint",
|
|
7
|
+
"handler_type": "fetch",
|
|
8
|
+
"outcome": "ok",
|
|
9
|
+
"duration_ms": 42,
|
|
10
|
+
"resource_usage": {
|
|
11
|
+
"d1_reads": 10,
|
|
12
|
+
"d1_writes": 2,
|
|
13
|
+
"kv_reads": 5,
|
|
14
|
+
"kv_writes": 1,
|
|
15
|
+
"r2_reads": 0,
|
|
16
|
+
"r2_writes": 0,
|
|
17
|
+
"ai_requests": 0,
|
|
18
|
+
"vectorize_reads": 0,
|
|
19
|
+
"queue_messages": 0,
|
|
20
|
+
"do_requests": 0,
|
|
21
|
+
"worker_subrequests": 0
|
|
22
|
+
},
|
|
23
|
+
"metadata": {
|
|
24
|
+
"worker_name": "test-worker",
|
|
25
|
+
"environment": "production"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
interface D1RunResult {
|
|
4
|
+
success: boolean;
|
|
5
|
+
results?: unknown[];
|
|
6
|
+
meta?: Record<string, unknown>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class MockD1Database {
|
|
10
|
+
private runResults: D1RunResult[] = [];
|
|
11
|
+
private allResults: Array<{ results: unknown[] }> = [];
|
|
12
|
+
private firstResults: Array<unknown | null> = [];
|
|
13
|
+
statements: string[] = [];
|
|
14
|
+
bindings: unknown[][] = [];
|
|
15
|
+
|
|
16
|
+
queueRunResult(result: D1RunResult): void {
|
|
17
|
+
this.runResults.push(result);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
queueAllResult(results: unknown[]): void {
|
|
21
|
+
this.allResults.push({ results });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
queueFirstResult(result: unknown | null): void {
|
|
25
|
+
this.firstResults.push(result);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
prepare(sql: string) {
|
|
29
|
+
this.statements.push(sql);
|
|
30
|
+
const self = this;
|
|
31
|
+
let boundValues: unknown[] = [];
|
|
32
|
+
|
|
33
|
+
const stmt = {
|
|
34
|
+
bind(...args: unknown[]) {
|
|
35
|
+
boundValues = args;
|
|
36
|
+
self.bindings.push(args);
|
|
37
|
+
return stmt;
|
|
38
|
+
},
|
|
39
|
+
run: vi.fn(async () => {
|
|
40
|
+
return self.runResults.shift() ?? { success: true, results: [] };
|
|
41
|
+
}),
|
|
42
|
+
all: vi.fn(async () => {
|
|
43
|
+
return self.allResults.shift() ?? { results: [] };
|
|
44
|
+
}),
|
|
45
|
+
first: vi.fn(async () => {
|
|
46
|
+
return self.firstResults.shift() ?? null;
|
|
47
|
+
}),
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
return stmt;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async batch(statements: unknown[]): Promise<D1RunResult[]> {
|
|
54
|
+
return statements.map(() => this.runResults.shift() ?? { success: true, results: [] });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async exec(sql: string): Promise<D1RunResult> {
|
|
58
|
+
this.statements.push(sql);
|
|
59
|
+
return { success: true };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
export interface MockKV {
|
|
4
|
+
get: ReturnType<typeof vi.fn>;
|
|
5
|
+
put: ReturnType<typeof vi.fn>;
|
|
6
|
+
delete: ReturnType<typeof vi.fn>;
|
|
7
|
+
list: ReturnType<typeof vi.fn>;
|
|
8
|
+
_store: Map<string, string>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function createMockKV(initialData: Record<string, string> = {}): MockKV {
|
|
12
|
+
const store = new Map<string, string>(Object.entries(initialData));
|
|
13
|
+
return {
|
|
14
|
+
get: vi.fn((key: string, type?: string) => {
|
|
15
|
+
const raw = store.get(key) ?? null;
|
|
16
|
+
if (raw === null) return Promise.resolve(null);
|
|
17
|
+
if (type === 'json') return Promise.resolve(JSON.parse(raw));
|
|
18
|
+
return Promise.resolve(raw);
|
|
19
|
+
}),
|
|
20
|
+
put: vi.fn((key: string, value: string, _opts?: unknown) => {
|
|
21
|
+
store.set(key, typeof value === 'string' ? value : JSON.stringify(value));
|
|
22
|
+
return Promise.resolve();
|
|
23
|
+
}),
|
|
24
|
+
delete: vi.fn((key: string) => {
|
|
25
|
+
store.delete(key);
|
|
26
|
+
return Promise.resolve();
|
|
27
|
+
}),
|
|
28
|
+
list: vi.fn((opts: { prefix?: string; limit?: number } = {}) => {
|
|
29
|
+
const keys = [...store.keys()]
|
|
30
|
+
.filter((k) => !opts.prefix || k.startsWith(opts.prefix))
|
|
31
|
+
.slice(0, opts.limit ?? 1000)
|
|
32
|
+
.map((name) => ({ name }));
|
|
33
|
+
return Promise.resolve({ keys, list_complete: true });
|
|
34
|
+
}),
|
|
35
|
+
_store: store,
|
|
36
|
+
};
|
|
37
|
+
}
|