@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,436 @@
1
+ /**
2
+ * Platform Settings Worker
3
+ *
4
+ * Unified settings management with project/category/key namespacing.
5
+ * Provides API endpoints for reading and updating platform settings.
6
+ *
7
+ * Storage:
8
+ * - D1: platform_settings table
9
+ *
10
+ * @module workers/platform-settings
11
+ * @created 2026-02-03
12
+ * @task task-303.1
13
+ */
14
+
15
+ import type {
16
+ KVNamespace,
17
+ ExecutionContext,
18
+ D1Database,
19
+ } from '@cloudflare/workers-types';
20
+ import {
21
+ withFeatureBudget,
22
+ CircuitBreakerError,
23
+ createLoggerFromRequest,
24
+ } from '@littlebearapps/platform-sdk';
25
+
26
+ // =============================================================================
27
+ // TYPES
28
+ // =============================================================================
29
+
30
+ interface Env {
31
+ PLATFORM_DB: D1Database;
32
+ PLATFORM_CACHE: KVNamespace;
33
+ CLOUDFLARE_ACCOUNT_ID: string;
34
+ }
35
+
36
+ interface Setting {
37
+ id: string;
38
+ project: string;
39
+ category: string;
40
+ key: string;
41
+ value: string; // JSON-encoded
42
+ description: string | null;
43
+ updated_at: number;
44
+ updated_by: string | null;
45
+ }
46
+
47
+ interface SettingGroup {
48
+ project: string;
49
+ category: string;
50
+ settings: Setting[];
51
+ }
52
+
53
+ interface UpdateSettingRequest {
54
+ value: unknown;
55
+ description?: string;
56
+ }
57
+
58
+ interface BulkUpdateRequest {
59
+ settings: Array<{
60
+ project: string;
61
+ category: string;
62
+ key: string;
63
+ value: unknown;
64
+ description?: string;
65
+ }>;
66
+ }
67
+
68
+ // =============================================================================
69
+ // CONSTANTS
70
+ // =============================================================================
71
+
72
+ const FEATURE_ID = 'platform:settings:api';
73
+ // Add your project names here, or load from D1 project_registry table
74
+ const VALID_PROJECTS = ['global'];
75
+ const VALID_CATEGORIES = ['notifications', 'thresholds', 'display', 'api', 'features'];
76
+
77
+ // =============================================================================
78
+ // HELPERS
79
+ // =============================================================================
80
+
81
+ function generateId(project: string, category: string, key: string): string {
82
+ return `${project}:${category}:${key}`;
83
+ }
84
+
85
+ function getUserEmail(request: Request): string {
86
+ const cfAccessEmail = request.headers.get('cf-access-authenticated-user-email');
87
+ return cfAccessEmail || 'anonymous';
88
+ }
89
+
90
+ function validateProject(project: string): boolean {
91
+ return VALID_PROJECTS.includes(project);
92
+ }
93
+
94
+ function validateCategory(category: string): boolean {
95
+ return VALID_CATEGORIES.includes(category);
96
+ }
97
+
98
+ // =============================================================================
99
+ // API HANDLERS
100
+ // =============================================================================
101
+
102
+ async function handleListSettings(env: Env, url: URL): Promise<Response> {
103
+ const project = url.searchParams.get('project');
104
+ const category = url.searchParams.get('category');
105
+
106
+ let query = 'SELECT * FROM platform_settings WHERE 1=1';
107
+ const params: string[] = [];
108
+
109
+ if (project) {
110
+ query += ' AND project = ?';
111
+ params.push(project);
112
+ }
113
+ if (category) {
114
+ query += ' AND category = ?';
115
+ params.push(category);
116
+ }
117
+
118
+ query += ' ORDER BY project, category, key';
119
+
120
+ const result = await env.PLATFORM_DB.prepare(query).bind(...params).all<Setting>();
121
+ const settings = result.results || [];
122
+
123
+ // Group by project and category
124
+ const grouped: Record<string, Record<string, Setting[]>> = {};
125
+ for (const setting of settings) {
126
+ if (!grouped[setting.project]) {
127
+ grouped[setting.project] = {};
128
+ }
129
+ if (!grouped[setting.project][setting.category]) {
130
+ grouped[setting.project][setting.category] = [];
131
+ }
132
+ grouped[setting.project][setting.category].push(setting);
133
+ }
134
+
135
+ return Response.json({
136
+ settings,
137
+ grouped,
138
+ count: settings.length,
139
+ });
140
+ }
141
+
142
+ async function handleGetSettings(
143
+ env: Env,
144
+ project: string,
145
+ category: string
146
+ ): Promise<Response> {
147
+ if (!validateProject(project)) {
148
+ return Response.json({ error: `Invalid project: ${project}` }, { status: 400 });
149
+ }
150
+ if (!validateCategory(category)) {
151
+ return Response.json({ error: `Invalid category: ${category}` }, { status: 400 });
152
+ }
153
+
154
+ const result = await env.PLATFORM_DB.prepare(
155
+ 'SELECT * FROM platform_settings WHERE project = ? AND category = ? ORDER BY key'
156
+ )
157
+ .bind(project, category)
158
+ .all<Setting>();
159
+
160
+ const settings = result.results || [];
161
+
162
+ // Parse JSON values for response
163
+ const parsed = settings.map((s) => ({
164
+ ...s,
165
+ parsed_value: JSON.parse(s.value),
166
+ }));
167
+
168
+ return Response.json({
169
+ project,
170
+ category,
171
+ settings: parsed,
172
+ count: settings.length,
173
+ });
174
+ }
175
+
176
+ async function handleGetSetting(
177
+ env: Env,
178
+ project: string,
179
+ category: string,
180
+ key: string
181
+ ): Promise<Response> {
182
+ if (!validateProject(project)) {
183
+ return Response.json({ error: `Invalid project: ${project}` }, { status: 400 });
184
+ }
185
+ if (!validateCategory(category)) {
186
+ return Response.json({ error: `Invalid category: ${category}` }, { status: 400 });
187
+ }
188
+
189
+ const result = await env.PLATFORM_DB.prepare(
190
+ 'SELECT * FROM platform_settings WHERE project = ? AND category = ? AND key = ?'
191
+ )
192
+ .bind(project, category, key)
193
+ .first<Setting>();
194
+
195
+ if (!result) {
196
+ return Response.json({ error: 'Setting not found' }, { status: 404 });
197
+ }
198
+
199
+ return Response.json({
200
+ ...result,
201
+ parsed_value: JSON.parse(result.value),
202
+ });
203
+ }
204
+
205
+ async function handleUpdateSetting(
206
+ request: Request,
207
+ env: Env,
208
+ project: string,
209
+ category: string,
210
+ key: string
211
+ ): Promise<Response> {
212
+ if (!validateProject(project)) {
213
+ return Response.json({ error: `Invalid project: ${project}` }, { status: 400 });
214
+ }
215
+ if (!validateCategory(category)) {
216
+ return Response.json({ error: `Invalid category: ${category}` }, { status: 400 });
217
+ }
218
+
219
+ const body = (await request.json()) as UpdateSettingRequest;
220
+ const email = getUserEmail(request);
221
+ const id = generateId(project, category, key);
222
+ const now = Math.floor(Date.now() / 1000);
223
+ const valueJson = JSON.stringify(body.value);
224
+
225
+ // Upsert the setting
226
+ await env.PLATFORM_DB.prepare(
227
+ `INSERT INTO platform_settings (id, project, category, key, value, description, updated_at, updated_by)
228
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
229
+ ON CONFLICT(project, category, key) DO UPDATE SET
230
+ value = excluded.value,
231
+ description = COALESCE(excluded.description, platform_settings.description),
232
+ updated_at = excluded.updated_at,
233
+ updated_by = excluded.updated_by`
234
+ )
235
+ .bind(id, project, category, key, valueJson, body.description || null, now, email)
236
+ .run();
237
+
238
+ return Response.json({
239
+ success: true,
240
+ id,
241
+ project,
242
+ category,
243
+ key,
244
+ value: body.value,
245
+ updated_at: now,
246
+ updated_by: email,
247
+ });
248
+ }
249
+
250
+ async function handleBulkUpdate(request: Request, env: Env): Promise<Response> {
251
+ const body = (await request.json()) as BulkUpdateRequest;
252
+ const email = getUserEmail(request);
253
+ const now = Math.floor(Date.now() / 1000);
254
+
255
+ if (!body.settings || !Array.isArray(body.settings)) {
256
+ return Response.json({ error: 'Missing settings array' }, { status: 400 });
257
+ }
258
+
259
+ const results: Array<{ id: string; success: boolean; error?: string }> = [];
260
+
261
+ for (const setting of body.settings) {
262
+ if (!validateProject(setting.project)) {
263
+ results.push({
264
+ id: generateId(setting.project, setting.category, setting.key),
265
+ success: false,
266
+ error: `Invalid project: ${setting.project}`,
267
+ });
268
+ continue;
269
+ }
270
+ if (!validateCategory(setting.category)) {
271
+ results.push({
272
+ id: generateId(setting.project, setting.category, setting.key),
273
+ success: false,
274
+ error: `Invalid category: ${setting.category}`,
275
+ });
276
+ continue;
277
+ }
278
+
279
+ const id = generateId(setting.project, setting.category, setting.key);
280
+ const valueJson = JSON.stringify(setting.value);
281
+
282
+ try {
283
+ await env.PLATFORM_DB.prepare(
284
+ `INSERT INTO platform_settings (id, project, category, key, value, description, updated_at, updated_by)
285
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
286
+ ON CONFLICT(project, category, key) DO UPDATE SET
287
+ value = excluded.value,
288
+ description = COALESCE(excluded.description, platform_settings.description),
289
+ updated_at = excluded.updated_at,
290
+ updated_by = excluded.updated_by`
291
+ )
292
+ .bind(
293
+ id,
294
+ setting.project,
295
+ setting.category,
296
+ setting.key,
297
+ valueJson,
298
+ setting.description || null,
299
+ now,
300
+ email
301
+ )
302
+ .run();
303
+
304
+ results.push({ id, success: true });
305
+ } catch (error) {
306
+ results.push({
307
+ id,
308
+ success: false,
309
+ error: error instanceof Error ? error.message : 'Unknown error',
310
+ });
311
+ }
312
+ }
313
+
314
+ const successCount = results.filter((r) => r.success).length;
315
+ return Response.json({
316
+ success: successCount === body.settings.length,
317
+ total: body.settings.length,
318
+ succeeded: successCount,
319
+ failed: body.settings.length - successCount,
320
+ results,
321
+ });
322
+ }
323
+
324
+ async function handleDeleteSetting(
325
+ env: Env,
326
+ project: string,
327
+ category: string,
328
+ key: string
329
+ ): Promise<Response> {
330
+ if (!validateProject(project)) {
331
+ return Response.json({ error: `Invalid project: ${project}` }, { status: 400 });
332
+ }
333
+ if (!validateCategory(category)) {
334
+ return Response.json({ error: `Invalid category: ${category}` }, { status: 400 });
335
+ }
336
+
337
+ const result = await env.PLATFORM_DB.prepare(
338
+ 'DELETE FROM platform_settings WHERE project = ? AND category = ? AND key = ?'
339
+ )
340
+ .bind(project, category, key)
341
+ .run();
342
+
343
+ if (result.meta.changes === 0) {
344
+ return Response.json({ error: 'Setting not found' }, { status: 404 });
345
+ }
346
+
347
+ return Response.json({ success: true, deleted: generateId(project, category, key) });
348
+ }
349
+
350
+ // =============================================================================
351
+ // MAIN WORKER
352
+ // =============================================================================
353
+
354
+ export default {
355
+ async fetch(
356
+ request: Request,
357
+ env: Env,
358
+ ctx: ExecutionContext
359
+ ): Promise<Response> {
360
+ const url = new URL(request.url);
361
+
362
+ // Health check (lightweight)
363
+ if (url.pathname === '/health') {
364
+ return Response.json({
365
+ status: 'ok',
366
+ service: 'platform-settings',
367
+ timestamp: new Date().toISOString(),
368
+ });
369
+ }
370
+
371
+ const log = createLoggerFromRequest(request, env, 'platform-settings', FEATURE_ID);
372
+
373
+ try {
374
+ const trackedEnv = withFeatureBudget(env, FEATURE_ID, { ctx });
375
+
376
+ // GET /settings - List all settings
377
+ if (url.pathname === '/settings' && request.method === 'GET') {
378
+ return await handleListSettings(trackedEnv, url);
379
+ }
380
+
381
+ // PUT /settings/bulk - Bulk update
382
+ if (url.pathname === '/settings/bulk' && request.method === 'PUT') {
383
+ return await handleBulkUpdate(request, trackedEnv);
384
+ }
385
+
386
+ // Routes with project/category/key parameters
387
+ // GET /settings/:project/:category
388
+ const categoryMatch = url.pathname.match(/^\/settings\/([^/]+)\/([^/]+)$/);
389
+ if (categoryMatch && request.method === 'GET') {
390
+ return await handleGetSettings(trackedEnv, categoryMatch[1], categoryMatch[2]);
391
+ }
392
+
393
+ // GET/PUT/DELETE /settings/:project/:category/:key
394
+ const keyMatch = url.pathname.match(/^\/settings\/([^/]+)\/([^/]+)\/([^/]+)$/);
395
+ if (keyMatch) {
396
+ const [, project, category, key] = keyMatch;
397
+ if (request.method === 'GET') {
398
+ return await handleGetSetting(trackedEnv, project, category, key);
399
+ }
400
+ if (request.method === 'PUT') {
401
+ return await handleUpdateSetting(request, trackedEnv, project, category, key);
402
+ }
403
+ if (request.method === 'DELETE') {
404
+ return await handleDeleteSetting(trackedEnv, project, category, key);
405
+ }
406
+ }
407
+
408
+ // API index
409
+ return Response.json({
410
+ service: 'platform-settings',
411
+ version: '1.0.0',
412
+ endpoints: [
413
+ 'GET /health - Health check',
414
+ 'GET /settings - List all settings (with filters)',
415
+ 'GET /settings/:project/:category - Get settings for project/category',
416
+ 'GET /settings/:project/:category/:key - Get specific setting',
417
+ 'PUT /settings/:project/:category/:key - Update setting',
418
+ 'DELETE /settings/:project/:category/:key - Delete setting',
419
+ 'PUT /settings/bulk - Bulk update multiple settings',
420
+ ],
421
+ valid_projects: VALID_PROJECTS,
422
+ valid_categories: VALID_CATEGORIES,
423
+ });
424
+ } catch (error) {
425
+ if (error instanceof CircuitBreakerError) {
426
+ log.warn('Circuit breaker tripped', error);
427
+ return Response.json(
428
+ { error: 'Service temporarily unavailable' },
429
+ { status: 503, headers: { 'Retry-After': '60' } }
430
+ );
431
+ }
432
+ log.error('Request failed', error);
433
+ return Response.json({ error: 'Internal server error' }, { status: 500 });
434
+ }
435
+ },
436
+ };