@littlebearapps/create-platform 1.0.0 → 1.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 (69) hide show
  1. package/README.md +98 -0
  2. package/dist/index.d.ts +6 -1
  3. package/dist/index.js +36 -6
  4. package/dist/prompts.d.ts +14 -2
  5. package/dist/prompts.js +29 -7
  6. package/dist/templates.js +78 -0
  7. package/package.json +3 -2
  8. package/templates/full/workers/lib/pattern-discovery/ai-prompt.ts +644 -0
  9. package/templates/full/workers/lib/pattern-discovery/clustering.ts +278 -0
  10. package/templates/full/workers/lib/pattern-discovery/shadow-evaluation.ts +603 -0
  11. package/templates/full/workers/lib/pattern-discovery/storage.ts +806 -0
  12. package/templates/full/workers/lib/pattern-discovery/types.ts +159 -0
  13. package/templates/full/workers/lib/pattern-discovery/validation.ts +278 -0
  14. package/templates/full/workers/pattern-discovery.ts +661 -0
  15. package/templates/full/workers/platform-alert-router.ts +1809 -0
  16. package/templates/full/workers/platform-notifications.ts +424 -0
  17. package/templates/full/workers/platform-search.ts +480 -0
  18. package/templates/full/workers/platform-settings.ts +436 -0
  19. package/templates/shared/workers/lib/analytics-engine.ts +357 -0
  20. package/templates/shared/workers/lib/billing.ts +293 -0
  21. package/templates/shared/workers/lib/circuit-breaker-middleware.ts +25 -0
  22. package/templates/shared/workers/lib/control.ts +292 -0
  23. package/templates/shared/workers/lib/economics.ts +368 -0
  24. package/templates/shared/workers/lib/metrics.ts +103 -0
  25. package/templates/shared/workers/lib/platform-settings.ts +407 -0
  26. package/templates/shared/workers/lib/shared/allowances.ts +333 -0
  27. package/templates/shared/workers/lib/shared/cloudflare.ts +1362 -0
  28. package/templates/shared/workers/lib/shared/types.ts +58 -0
  29. package/templates/shared/workers/lib/telemetry-sampling.ts +360 -0
  30. package/templates/shared/workers/lib/usage/collectors/example.ts +96 -0
  31. package/templates/shared/workers/lib/usage/collectors/index.ts +128 -0
  32. package/templates/shared/workers/lib/usage/handlers/audit.ts +306 -0
  33. package/templates/shared/workers/lib/usage/handlers/backfill.ts +845 -0
  34. package/templates/shared/workers/lib/usage/handlers/behavioral.ts +429 -0
  35. package/templates/shared/workers/lib/usage/handlers/data-queries.ts +507 -0
  36. package/templates/shared/workers/lib/usage/handlers/dlq-admin.ts +364 -0
  37. package/templates/shared/workers/lib/usage/handlers/health-trends.ts +222 -0
  38. package/templates/shared/workers/lib/usage/handlers/index.ts +35 -0
  39. package/templates/shared/workers/lib/usage/handlers/usage-admin.ts +421 -0
  40. package/templates/shared/workers/lib/usage/handlers/usage-features.ts +1262 -0
  41. package/templates/shared/workers/lib/usage/handlers/usage-metrics.ts +2420 -0
  42. package/templates/shared/workers/lib/usage/handlers/usage-settings.ts +610 -0
  43. package/templates/shared/workers/lib/usage/queue/budget-enforcement.ts +1032 -0
  44. package/templates/shared/workers/lib/usage/queue/cost-budget-enforcement.ts +128 -0
  45. package/templates/shared/workers/lib/usage/queue/cost-calculator.ts +77 -0
  46. package/templates/shared/workers/lib/usage/queue/dlq-handler.ts +161 -0
  47. package/templates/shared/workers/lib/usage/queue/index.ts +19 -0
  48. package/templates/shared/workers/lib/usage/queue/telemetry-processor.ts +790 -0
  49. package/templates/shared/workers/lib/usage/scheduled/anomaly-detection.ts +732 -0
  50. package/templates/shared/workers/lib/usage/scheduled/data-collection.ts +956 -0
  51. package/templates/shared/workers/lib/usage/scheduled/error-digest.ts +343 -0
  52. package/templates/shared/workers/lib/usage/scheduled/index.ts +18 -0
  53. package/templates/shared/workers/lib/usage/scheduled/rollups.ts +1561 -0
  54. package/templates/shared/workers/lib/usage/shared/constants.ts +362 -0
  55. package/templates/shared/workers/lib/usage/shared/index.ts +14 -0
  56. package/templates/shared/workers/lib/usage/shared/types.ts +1066 -0
  57. package/templates/shared/workers/lib/usage/shared/utils.ts +795 -0
  58. package/templates/shared/workers/platform-usage.ts +1915 -0
  59. package/templates/standard/workers/error-collector.ts +2670 -0
  60. package/templates/standard/workers/lib/error-collector/capture.ts +213 -0
  61. package/templates/standard/workers/lib/error-collector/digest.ts +448 -0
  62. package/templates/standard/workers/lib/error-collector/email-health-alerts.ts +262 -0
  63. package/templates/standard/workers/lib/error-collector/fingerprint.ts +258 -0
  64. package/templates/standard/workers/lib/error-collector/gap-alerts.ts +293 -0
  65. package/templates/standard/workers/lib/error-collector/github.ts +329 -0
  66. package/templates/standard/workers/lib/error-collector/types.ts +262 -0
  67. package/templates/standard/workers/lib/sentinel/gap-detection.ts +734 -0
  68. package/templates/standard/workers/lib/shared/slack-alerts.ts +585 -0
  69. package/templates/standard/workers/platform-sentinel.ts +1744 -0
@@ -0,0 +1,357 @@
1
+ /**
2
+ * Analytics Engine SQL API Helper
3
+ *
4
+ * Provides helpers for querying Analytics Engine via the SQL API.
5
+ * Used by the daily rollup to aggregate SDK telemetry from PLATFORM_ANALYTICS.
6
+ *
7
+ * @module workers/lib/analytics-engine
8
+ */
9
+
10
+ import { withExponentialBackoff } from '@littlebearapps/platform-sdk';
11
+
12
+ // =============================================================================
13
+ // TYPES
14
+ // =============================================================================
15
+
16
+ /**
17
+ * Analytics Engine SQL API response structure.
18
+ *
19
+ * The SQL API returns data in one of two formats:
20
+ * 1. Direct format (success): { meta: [...], data: [...], rows: N }
21
+ * 2. Wrapped format (via REST API): { success: true, result: { meta, data, rows } }
22
+ * 3. Error format: { errors: [...] }
23
+ */
24
+ interface AnalyticsEngineResponse {
25
+ // Direct format (SQL API)
26
+ meta?: Array<{ name: string; type: string }>;
27
+ data?: unknown[];
28
+ rows?: number;
29
+ rows_before_limit_at_least?: number;
30
+
31
+ // Wrapped format (REST API)
32
+ success?: boolean;
33
+ errors?: Array<{ code: number; message: string }>;
34
+ result?: {
35
+ data: unknown[];
36
+ meta: Array<{ name: string; type: string }>;
37
+ rows: number;
38
+ rows_before_limit_at_least: number;
39
+ };
40
+ }
41
+
42
+ /**
43
+ * Daily usage aggregation from Analytics Engine
44
+ */
45
+ export interface DailyUsageAggregation {
46
+ project_id: string;
47
+ feature_id: string;
48
+ d1_reads: number;
49
+ d1_writes: number;
50
+ d1_rows_read: number;
51
+ d1_rows_written: number;
52
+ kv_reads: number;
53
+ kv_writes: number;
54
+ kv_deletes: number;
55
+ kv_lists: number;
56
+ ai_requests: number;
57
+ ai_neurons: number;
58
+ vectorize_queries: number;
59
+ vectorize_inserts: number;
60
+ vectorize_deletes: number;
61
+ interaction_count: number;
62
+ }
63
+
64
+ // =============================================================================
65
+ // ANALYTICS ENGINE SQL API CLIENT
66
+ // =============================================================================
67
+
68
+ /**
69
+ * Query Analytics Engine via the SQL API.
70
+ *
71
+ * @param accountId Cloudflare account ID
72
+ * @param apiToken Cloudflare API token with Analytics Engine read access
73
+ * @param sql SQL query to execute
74
+ * @returns Query results
75
+ */
76
+ export async function queryAnalyticsEngine<T>(
77
+ accountId: string,
78
+ apiToken: string,
79
+ sql: string
80
+ ): Promise<T[]> {
81
+ const url = `https://api.cloudflare.com/client/v4/accounts/${accountId}/analytics_engine/sql`;
82
+
83
+ const response = await fetch(url, {
84
+ method: 'POST',
85
+ headers: {
86
+ Authorization: `Bearer ${apiToken}`,
87
+ 'Content-Type': 'text/plain',
88
+ },
89
+ body: sql,
90
+ });
91
+
92
+ if (!response.ok) {
93
+ const text = await response.text();
94
+ throw new Error(`Analytics Engine API error: ${response.status} - ${text}`);
95
+ }
96
+
97
+ const rawText = await response.text();
98
+ let data: AnalyticsEngineResponse;
99
+ try {
100
+ data = JSON.parse(rawText) as AnalyticsEngineResponse;
101
+ } catch {
102
+ throw new Error(`Analytics Engine returned invalid JSON: ${rawText.slice(0, 500)}`);
103
+ }
104
+
105
+ // Check for error response
106
+ if (data.errors && data.errors.length > 0) {
107
+ const errorMessages = data.errors.map((e) => e.message).join(', ');
108
+ throw new Error(`Analytics Engine query failed: ${errorMessages}`);
109
+ }
110
+
111
+ // Handle both response formats:
112
+ // 1. Direct format: { meta, data, rows }
113
+ // 2. Wrapped format: { success, result: { meta, data, rows } }
114
+ const meta = data.meta ?? data.result?.meta;
115
+ const resultData = data.data ?? data.result?.data;
116
+
117
+ // Validate response structure
118
+ if (!meta || !resultData) {
119
+ throw new Error(
120
+ `Analytics Engine response missing expected fields. ` +
121
+ `Got keys: ${JSON.stringify(Object.keys(data))}`
122
+ );
123
+ }
124
+
125
+ // Map the result data to typed objects using column metadata
126
+ // Analytics Engine can return data in two formats:
127
+ // 1. Array of arrays: [[val1, val2], [val1, val2]] - needs column mapping
128
+ // 2. Array of objects: [{col1: val1, col2: val2}, ...] - already in object format
129
+ const columns = meta.map((m) => m.name);
130
+
131
+ return resultData.map((row) => {
132
+ // If row is already an object (not an array), return it directly
133
+ if (row !== null && typeof row === 'object' && !Array.isArray(row)) {
134
+ return row as T;
135
+ }
136
+
137
+ // Row is an array - map using column metadata
138
+ const rowArray = row as unknown[];
139
+ const obj: Record<string, unknown> = {};
140
+ columns.forEach((col, i) => {
141
+ obj[col] = rowArray[i];
142
+ });
143
+ return obj as T;
144
+ });
145
+ }
146
+
147
+ /**
148
+ * Get daily usage aggregation from Analytics Engine.
149
+ * Queries the PLATFORM_ANALYTICS dataset for yesterday's telemetry data.
150
+ *
151
+ * @param accountId Cloudflare account ID
152
+ * @param apiToken Cloudflare API token
153
+ * @param datasetName Analytics Engine dataset name (default: platform-analytics)
154
+ * @returns Aggregated usage by project and feature
155
+ */
156
+ export async function getDailyUsageFromAnalyticsEngine(
157
+ accountId: string,
158
+ apiToken: string,
159
+ datasetName = 'platform-analytics'
160
+ ): Promise<DailyUsageAggregation[]> {
161
+ // Query for yesterday's data (00:00:00 to 23:59:59 UTC)
162
+ // Analytics Engine uses blob1-20 and double1-20 naming convention
163
+ //
164
+ // Data schema from queue handler (platform-usage.ts):
165
+ // blobs: [project, category, feature] (feature_key is in indexes)
166
+ // doubles: [d1Writes, d1Reads, kvReads, kvWrites, doRequests, doGbSeconds,
167
+ // r2ClassA, r2ClassB, aiNeurons, queueMessages, requests, cpuMs,
168
+ // d1RowsRead, d1RowsWritten, kvDeletes, kvLists, aiRequests,
169
+ // vectorizeQueries, vectorizeInserts, workflowInvocations]
170
+ // indexes: [feature_key]
171
+ //
172
+ // NOTE: Table name must be quoted because it contains a hyphen
173
+ const sql = `
174
+ SELECT
175
+ blob1 as project_id,
176
+ index1 as feature_id,
177
+ SUM(double2) as d1_reads,
178
+ SUM(double1) as d1_writes,
179
+ SUM(double13) as d1_rows_read,
180
+ SUM(double14) as d1_rows_written,
181
+ SUM(double3) as kv_reads,
182
+ SUM(double4) as kv_writes,
183
+ SUM(double15) as kv_deletes,
184
+ SUM(double16) as kv_lists,
185
+ SUM(double17) as ai_requests,
186
+ SUM(double9) as ai_neurons,
187
+ SUM(double18) as vectorize_queries,
188
+ SUM(double19) as vectorize_inserts,
189
+ 0 as vectorize_deletes,
190
+ count() as interaction_count
191
+ FROM "${datasetName}"
192
+ WHERE timestamp >= NOW() - INTERVAL '1' DAY
193
+ GROUP BY project_id, feature_id
194
+ ORDER BY project_id, feature_id
195
+ `;
196
+
197
+ return queryAnalyticsEngine<DailyUsageAggregation>(accountId, apiToken, sql);
198
+ }
199
+
200
+ /**
201
+ * Get aggregated project-level usage from Analytics Engine.
202
+ * Groups all features by project for higher-level reporting.
203
+ *
204
+ * @param accountId Cloudflare account ID
205
+ * @param apiToken Cloudflare API token
206
+ * @param datasetName Analytics Engine dataset name
207
+ * @returns Aggregated usage by project
208
+ */
209
+ export async function getProjectUsageFromAnalyticsEngine(
210
+ accountId: string,
211
+ apiToken: string,
212
+ datasetName = 'platform-analytics'
213
+ ): Promise<Omit<DailyUsageAggregation, 'feature_id'>[]> {
214
+ // NOTE: Table name must be quoted because it contains a hyphen
215
+ // Schema matches METRIC_FIELDS order from platform-sdk/constants.ts
216
+ const sql = `
217
+ SELECT
218
+ blob1 as project_id,
219
+ SUM(double2) as d1_reads,
220
+ SUM(double1) as d1_writes,
221
+ SUM(double13) as d1_rows_read,
222
+ SUM(double14) as d1_rows_written,
223
+ SUM(double3) as kv_reads,
224
+ SUM(double4) as kv_writes,
225
+ SUM(double15) as kv_deletes,
226
+ SUM(double16) as kv_lists,
227
+ SUM(double17) as ai_requests,
228
+ SUM(double9) as ai_neurons,
229
+ SUM(double18) as vectorize_queries,
230
+ SUM(double19) as vectorize_inserts,
231
+ 0 as vectorize_deletes,
232
+ count() as interaction_count
233
+ FROM "${datasetName}"
234
+ WHERE timestamp >= NOW() - INTERVAL '1' DAY
235
+ GROUP BY project_id
236
+ ORDER BY project_id
237
+ `;
238
+
239
+ return queryAnalyticsEngine<Omit<DailyUsageAggregation, 'feature_id'>>(accountId, apiToken, sql);
240
+ }
241
+
242
+ // =============================================================================
243
+ // TIME-BUCKETED QUERIES
244
+ // =============================================================================
245
+
246
+ /**
247
+ * Time-bucketed usage data from Analytics Engine.
248
+ * Aggregates metrics by time bucket (hour/day) and project.
249
+ */
250
+ export interface TimeBucketedUsage {
251
+ time_bucket: string;
252
+ project_id: string;
253
+ d1_writes: number;
254
+ d1_reads: number;
255
+ d1_rows_read: number;
256
+ d1_rows_written: number;
257
+ kv_reads: number;
258
+ kv_writes: number;
259
+ kv_deletes: number;
260
+ kv_lists: number;
261
+ do_requests: number;
262
+ do_gb_seconds: number;
263
+ r2_class_a: number;
264
+ r2_class_b: number;
265
+ ai_neurons: number;
266
+ ai_requests: number;
267
+ queue_messages: number;
268
+ requests: number;
269
+ cpu_ms: number;
270
+ vectorize_queries: number;
271
+ vectorize_inserts: number;
272
+ workflow_invocations: number;
273
+ interaction_count: number;
274
+ }
275
+
276
+ /**
277
+ * Query parameters for time-bucketed usage.
278
+ */
279
+ export interface TimeBucketQueryParams {
280
+ period: '24h' | '7d' | '30d';
281
+ groupBy: 'hour' | 'day';
282
+ project?: string;
283
+ }
284
+
285
+ /**
286
+ * Query usage by time bucket from Analytics Engine.
287
+ * Returns aggregated metrics grouped by time interval (hour/day) and project.
288
+ *
289
+ * @param accountId Cloudflare account ID
290
+ * @param apiToken Cloudflare API token
291
+ * @param params Query parameters (period, groupBy, optional project filter)
292
+ * @param datasetName Analytics Engine dataset name
293
+ * @returns Time-bucketed usage data
294
+ */
295
+ export async function queryUsageByTimeBucket(
296
+ accountId: string,
297
+ apiToken: string,
298
+ params: TimeBucketQueryParams,
299
+ datasetName = 'platform-analytics'
300
+ ): Promise<TimeBucketedUsage[]> {
301
+ // Determine interval based on groupBy
302
+ const interval = params.groupBy === 'hour' ? 'HOUR' : 'DAY';
303
+
304
+ // Map period to interval parts (number and unit must be separate for Analytics Engine)
305
+ const periodMap: Record<string, { num: string; unit: string }> = {
306
+ '24h': { num: '1', unit: 'DAY' },
307
+ '7d': { num: '7', unit: 'DAY' },
308
+ '30d': { num: '30', unit: 'DAY' },
309
+ };
310
+ const periodParts = periodMap[params.period] ?? { num: '1', unit: 'DAY' };
311
+
312
+ // Build project filter clause
313
+ const projectFilter = params.project ? `AND blob1 = '${params.project}'` : '';
314
+
315
+ // NOTE: Table name must be quoted because it contains a hyphen
316
+ // Analytics Engine columns map (from platform-sdk/constants.ts METRIC_FIELDS):
317
+ // double1=d1Writes, double2=d1Reads, double3=kvReads, double4=kvWrites,
318
+ // double5=doRequests, double6=doGbSeconds, double7=r2ClassA, double8=r2ClassB,
319
+ // double9=aiNeurons, double10=queueMessages, double11=requests, double12=cpuMs,
320
+ // double13=d1RowsRead, double14=d1RowsWritten, double15=kvDeletes, double16=kvLists,
321
+ // double17=aiRequests, double18=vectorizeQueries, double19=vectorizeInserts,
322
+ // double20=workflowInvocations
323
+ // blobs: blob1=project, blob2=category, blob3=feature
324
+ const sql = `
325
+ SELECT
326
+ toStartOfInterval(timestamp, INTERVAL '1' ${interval}) as time_bucket,
327
+ blob1 as project_id,
328
+ SUM(double1) as d1_writes,
329
+ SUM(double2) as d1_reads,
330
+ SUM(double13) as d1_rows_read,
331
+ SUM(double14) as d1_rows_written,
332
+ SUM(double3) as kv_reads,
333
+ SUM(double4) as kv_writes,
334
+ SUM(double15) as kv_deletes,
335
+ SUM(double16) as kv_lists,
336
+ SUM(double5) as do_requests,
337
+ SUM(double6) as do_gb_seconds,
338
+ SUM(double7) as r2_class_a,
339
+ SUM(double8) as r2_class_b,
340
+ SUM(double9) as ai_neurons,
341
+ SUM(double17) as ai_requests,
342
+ SUM(double10) as queue_messages,
343
+ SUM(double11) as requests,
344
+ SUM(double12) as cpu_ms,
345
+ SUM(double18) as vectorize_queries,
346
+ SUM(double19) as vectorize_inserts,
347
+ SUM(double20) as workflow_invocations,
348
+ count() as interaction_count
349
+ FROM "${datasetName}"
350
+ WHERE timestamp >= NOW() - INTERVAL '${periodParts.num}' ${periodParts.unit}
351
+ ${projectFilter}
352
+ GROUP BY time_bucket, project_id
353
+ ORDER BY time_bucket ASC, project_id ASC
354
+ `;
355
+
356
+ return queryAnalyticsEngine<TimeBucketedUsage>(accountId, apiToken, sql);
357
+ }
@@ -0,0 +1,293 @@
1
+ /**
2
+ * Billing Period Utilities
3
+ *
4
+ * Provides billing-cycle-aware calculations for accurate allowance proration.
5
+ * Supports both calendar-month and mid-month billing cycles.
6
+ *
7
+ * @see https://developers.cloudflare.com/workers/platform/pricing/
8
+ */
9
+
10
+ /**
11
+ * Billing period information
12
+ */
13
+ export interface BillingPeriod {
14
+ /** Start date of the current billing period */
15
+ startDate: Date;
16
+ /** End date of the current billing period */
17
+ endDate: Date;
18
+ /** Total days in this billing period */
19
+ daysInPeriod: number;
20
+ /** Days elapsed since billing period started */
21
+ daysElapsed: number;
22
+ /** Days remaining until billing period ends */
23
+ daysRemaining: number;
24
+ /** Progress through billing period (0-1) */
25
+ progress: number;
26
+ }
27
+
28
+ /**
29
+ * Plan types supported by Cloudflare
30
+ */
31
+ export type PlanType = 'free' | 'paid' | 'enterprise';
32
+
33
+ /**
34
+ * Billing settings from D1
35
+ */
36
+ export interface BillingSettings {
37
+ accountId: string;
38
+ planType: PlanType;
39
+ billingCycleDay: number; // 1-28 or 0 for calendar month
40
+ billingCurrency: string;
41
+ baseCostMonthly: number;
42
+ notes?: string;
43
+ }
44
+
45
+ /**
46
+ * Calculate the billing period boundaries for a given reference date.
47
+ *
48
+ * @param billingCycleDay - Day of month billing starts (1-28) or 0 for calendar month
49
+ * @param refDate - Reference date (defaults to now)
50
+ * @returns Billing period information
51
+ *
52
+ * @example
53
+ * // Calendar month billing (billing_cycle_day = 0 or 1)
54
+ * calculateBillingPeriod(1, new Date('2026-01-15'))
55
+ * // Returns: startDate: Jan 1, endDate: Jan 31, daysInPeriod: 31
56
+ *
57
+ * @example
58
+ * // Mid-month billing (billing_cycle_day = 15)
59
+ * calculateBillingPeriod(15, new Date('2026-01-20'))
60
+ * // Returns: startDate: Jan 15, endDate: Feb 14, daysInPeriod: 31
61
+ */
62
+ export function calculateBillingPeriod(
63
+ billingCycleDay: number,
64
+ refDate = new Date()
65
+ ): BillingPeriod {
66
+ // Normalise to calendar month if 0 or 1
67
+ const cycleDay = billingCycleDay <= 1 ? 1 : Math.min(billingCycleDay, 28);
68
+
69
+ const year = refDate.getFullYear();
70
+ const month = refDate.getMonth();
71
+ const day = refDate.getDate();
72
+
73
+ let startDate: Date;
74
+ let endDate: Date;
75
+
76
+ if (cycleDay === 1) {
77
+ // Calendar month billing
78
+ startDate = new Date(year, month, 1);
79
+ endDate = new Date(year, month + 1, 0); // Last day of current month
80
+ } else {
81
+ // Mid-month billing
82
+ if (day >= cycleDay) {
83
+ // We're in the period that started this month
84
+ startDate = new Date(year, month, cycleDay);
85
+ endDate = new Date(year, month + 1, cycleDay - 1);
86
+ } else {
87
+ // We're in the period that started last month
88
+ startDate = new Date(year, month - 1, cycleDay);
89
+ endDate = new Date(year, month, cycleDay - 1);
90
+ }
91
+ }
92
+
93
+ // Calculate days
94
+ const daysInPeriod =
95
+ Math.round((endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24)) + 1;
96
+ const daysElapsed =
97
+ Math.round((refDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24)) + 1;
98
+ const daysRemaining = Math.max(0, daysInPeriod - daysElapsed);
99
+ const progress = Math.min(1, daysElapsed / daysInPeriod);
100
+
101
+ return {
102
+ startDate,
103
+ endDate,
104
+ daysInPeriod,
105
+ daysElapsed,
106
+ daysRemaining,
107
+ progress,
108
+ };
109
+ }
110
+
111
+ /**
112
+ * Prorate a monthly allowance based on query period vs billing period.
113
+ *
114
+ * @param monthlyAllowance - Full monthly allowance (e.g., 10M Workers requests)
115
+ * @param periodDays - Number of days in the query period (e.g., 1 for 24h, 7 for 7d)
116
+ * @param billingDays - Total days in the billing period (default 30)
117
+ * @returns Prorated allowance for the query period
118
+ *
119
+ * @example
120
+ * // 24h query against 10M monthly allowance
121
+ * prorateAllowance(10_000_000, 1, 30)
122
+ * // Returns: 333,333 (1/30th of monthly)
123
+ *
124
+ * @example
125
+ * // 7d query against 50M monthly allowance
126
+ * prorateAllowance(50_000_000, 7, 31)
127
+ * // Returns: 11,290,323 (7/31ths of monthly)
128
+ */
129
+ export function prorateAllowance(
130
+ monthlyAllowance: number,
131
+ periodDays: number,
132
+ billingDays = 30
133
+ ): number {
134
+ if (billingDays <= 0) return monthlyAllowance;
135
+ if (periodDays >= billingDays) return monthlyAllowance;
136
+ return Math.round(monthlyAllowance * (periodDays / billingDays));
137
+ }
138
+
139
+ /**
140
+ * Calculate billable usage after subtracting prorated allowance.
141
+ *
142
+ * @param usage - Raw usage for the period
143
+ * @param monthlyAllowance - Full monthly allowance
144
+ * @param periodDays - Number of days in the query period
145
+ * @param billingDays - Total days in the billing period (default 30)
146
+ * @returns Object with raw, prorated allowance, billable usage, and percentage
147
+ *
148
+ * @example
149
+ * // 500K requests in 24h against 10M monthly allowance
150
+ * calculateBillableUsage(500_000, 10_000_000, 1, 30)
151
+ * // Returns: { raw: 500000, proratedAllowance: 333333, billable: 166667, pctOfAllowance: 150 }
152
+ */
153
+ export function calculateBillableUsage(
154
+ usage: number,
155
+ monthlyAllowance: number,
156
+ periodDays: number,
157
+ billingDays = 30
158
+ ): {
159
+ raw: number;
160
+ proratedAllowance: number;
161
+ billable: number;
162
+ pctOfAllowance: number;
163
+ } {
164
+ const proratedAllowance = prorateAllowance(monthlyAllowance, periodDays, billingDays);
165
+ const billable = Math.max(0, usage - proratedAllowance);
166
+ const pctOfAllowance = proratedAllowance > 0 ? (usage / proratedAllowance) * 100 : 0;
167
+
168
+ return {
169
+ raw: usage,
170
+ proratedAllowance,
171
+ billable,
172
+ pctOfAllowance,
173
+ };
174
+ }
175
+
176
+ /**
177
+ * Get the default billing settings.
178
+ * Used as fallback when D1 data is unavailable.
179
+ */
180
+ export function getDefaultBillingSettings(): BillingSettings {
181
+ return {
182
+ accountId: 'default',
183
+ planType: 'paid',
184
+ billingCycleDay: 1, // Calendar month
185
+ billingCurrency: 'USD',
186
+ baseCostMonthly: 5.0, // Workers Paid Plan
187
+ notes: 'Default billing settings',
188
+ };
189
+ }
190
+
191
+ /**
192
+ * Calculate fair share allowance allocation for a project.
193
+ *
194
+ * Uses proportional fair share: each project gets a share of the total
195
+ * allowance proportional to their share of total usage.
196
+ *
197
+ * @param projectUsage - Usage for this project
198
+ * @param totalAccountUsage - Total usage across all projects
199
+ * @param monthlyAllowance - Total monthly allowance for the account
200
+ * @returns Object with allowance share and billable usage
201
+ */
202
+ export function calculateProjectAllowanceShare(
203
+ projectUsage: number,
204
+ totalAccountUsage: number,
205
+ monthlyAllowance: number
206
+ ): {
207
+ share: number;
208
+ billable: number;
209
+ proportion: number;
210
+ } {
211
+ if (totalAccountUsage <= 0) {
212
+ return { share: 0, billable: 0, proportion: 0 };
213
+ }
214
+
215
+ const proportion = projectUsage / totalAccountUsage;
216
+ const share = monthlyAllowance * proportion;
217
+ const billable = Math.max(0, projectUsage - share);
218
+
219
+ return {
220
+ share: Math.round(share),
221
+ billable: Math.round(billable),
222
+ proportion,
223
+ };
224
+ }
225
+
226
+ /**
227
+ * Billing window with ISO date strings for SQL queries.
228
+ */
229
+ export interface BillingWindow {
230
+ /** Start date as YYYY-MM-DD string */
231
+ startDate: string;
232
+ /** End date as YYYY-MM-DD string */
233
+ endDate: string;
234
+ /** Days elapsed in current period */
235
+ daysElapsed: number;
236
+ /** Total days in billing period */
237
+ daysInPeriod: number;
238
+ /** Progress through period (0-1) */
239
+ progress: number;
240
+ }
241
+
242
+ /**
243
+ * Get billing window dates for SQL queries.
244
+ *
245
+ * Convenience wrapper around calculateBillingPeriod that returns
246
+ * ISO date strings ready for D1 queries.
247
+ *
248
+ * @param anchorDay - Day of month billing resets (1-28) or 0/1 for calendar month
249
+ * @param refDate - Reference date (defaults to now)
250
+ * @returns Billing window with date strings
251
+ */
252
+ export function getBillingWindow(anchorDay: number, refDate = new Date()): BillingWindow {
253
+ const period = calculateBillingPeriod(anchorDay, refDate);
254
+
255
+ // Format as YYYY-MM-DD in local time (not UTC) to match D1 date storage
256
+ const formatLocalDate = (date: Date): string => {
257
+ const y = date.getFullYear();
258
+ const m = String(date.getMonth() + 1).padStart(2, '0');
259
+ const d = String(date.getDate()).padStart(2, '0');
260
+ return `${y}-${m}-${d}`;
261
+ };
262
+
263
+ return {
264
+ startDate: formatLocalDate(period.startDate),
265
+ endDate: formatLocalDate(period.endDate),
266
+ daysElapsed: period.daysElapsed,
267
+ daysInPeriod: period.daysInPeriod,
268
+ progress: period.progress,
269
+ };
270
+ }
271
+
272
+ /**
273
+ * Format billing period for display.
274
+ *
275
+ * @param period - Billing period from calculateBillingPeriod
276
+ * @returns Formatted string like "Jan 1 - Jan 31"
277
+ */
278
+ export function formatBillingPeriod(period: BillingPeriod): string {
279
+ const formatter = new Intl.DateTimeFormat('en-AU', { month: 'short', day: 'numeric' });
280
+ return `${formatter.format(period.startDate)} - ${formatter.format(period.endDate)}`;
281
+ }
282
+
283
+ /**
284
+ * Get billing countdown text.
285
+ *
286
+ * @param daysRemaining - Days remaining in billing period
287
+ * @returns Human-readable countdown string
288
+ */
289
+ export function getBillingCountdownText(daysRemaining: number): string {
290
+ if (daysRemaining <= 0) return 'Billing reset today';
291
+ if (daysRemaining === 1) return '1 day until billing reset';
292
+ return `${daysRemaining} days until billing reset`;
293
+ }
@@ -0,0 +1,25 @@
1
+ /// <reference types="@cloudflare/workers-types" />
2
+
3
+ /**
4
+ * Circuit Breaker Middleware -- Thin Re-export from Platform SDK
5
+ *
6
+ * All logic lives in @littlebearapps/platform-sdk/middleware.
7
+ * This file re-exports with the original names for backward compatibility.
8
+ *
9
+ * @see packages/platform-sdk/src/middleware.ts
10
+ */
11
+
12
+ // Re-export with original names (SDK uses prefixed names)
13
+ export {
14
+ PROJECT_CB_STATUS as CB_STATUS,
15
+ CB_PROJECT_KEYS,
16
+ CB_ERROR_CODES,
17
+ BUDGET_STATUS_HEADER,
18
+ checkProjectCircuitBreaker as checkCircuitBreaker,
19
+ checkProjectCircuitBreakerDetailed as checkCircuitBreakerDetailed,
20
+ createCircuitBreakerMiddleware,
21
+ getCircuitBreakerStates,
22
+ type CircuitBreakerStatusValue,
23
+ type CircuitBreakerCheckResult,
24
+ type CircuitBreakerErrorResponse,
25
+ } from '@littlebearapps/platform-sdk/middleware';