@littlebearapps/platform-admin-sdk 1.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.
Files changed (94) hide show
  1. package/README.md +112 -0
  2. package/dist/index.d.ts +16 -0
  3. package/dist/index.js +89 -0
  4. package/dist/prompts.d.ts +27 -0
  5. package/dist/prompts.js +80 -0
  6. package/dist/scaffold.d.ts +5 -0
  7. package/dist/scaffold.js +65 -0
  8. package/dist/templates.d.ts +16 -0
  9. package/dist/templates.js +131 -0
  10. package/package.json +46 -0
  11. package/templates/full/migrations/006_pattern_discovery.sql +199 -0
  12. package/templates/full/migrations/007_notifications_search.sql +127 -0
  13. package/templates/full/workers/lib/pattern-discovery/ai-prompt.ts +644 -0
  14. package/templates/full/workers/lib/pattern-discovery/clustering.ts +278 -0
  15. package/templates/full/workers/lib/pattern-discovery/shadow-evaluation.ts +603 -0
  16. package/templates/full/workers/lib/pattern-discovery/storage.ts +806 -0
  17. package/templates/full/workers/lib/pattern-discovery/types.ts +159 -0
  18. package/templates/full/workers/lib/pattern-discovery/validation.ts +278 -0
  19. package/templates/full/workers/pattern-discovery.ts +661 -0
  20. package/templates/full/workers/platform-alert-router.ts +1809 -0
  21. package/templates/full/workers/platform-notifications.ts +424 -0
  22. package/templates/full/workers/platform-search.ts +480 -0
  23. package/templates/full/workers/platform-settings.ts +436 -0
  24. package/templates/full/wrangler.alert-router.jsonc.hbs +34 -0
  25. package/templates/full/wrangler.notifications.jsonc.hbs +23 -0
  26. package/templates/full/wrangler.pattern-discovery.jsonc.hbs +33 -0
  27. package/templates/full/wrangler.search.jsonc.hbs +16 -0
  28. package/templates/full/wrangler.settings.jsonc.hbs +23 -0
  29. package/templates/shared/README.md.hbs +69 -0
  30. package/templates/shared/config/budgets.yaml.hbs +72 -0
  31. package/templates/shared/config/services.yaml.hbs +45 -0
  32. package/templates/shared/migrations/001_core_tables.sql +117 -0
  33. package/templates/shared/migrations/002_usage_warehouse.sql +830 -0
  34. package/templates/shared/migrations/003_feature_tracking.sql +250 -0
  35. package/templates/shared/migrations/004_settings_alerts.sql +452 -0
  36. package/templates/shared/migrations/seed.sql.hbs +4 -0
  37. package/templates/shared/package.json.hbs +21 -0
  38. package/templates/shared/scripts/sync-config.ts +242 -0
  39. package/templates/shared/tsconfig.json +12 -0
  40. package/templates/shared/workers/lib/analytics-engine.ts +357 -0
  41. package/templates/shared/workers/lib/billing.ts +293 -0
  42. package/templates/shared/workers/lib/circuit-breaker-middleware.ts +25 -0
  43. package/templates/shared/workers/lib/control.ts +292 -0
  44. package/templates/shared/workers/lib/economics.ts +368 -0
  45. package/templates/shared/workers/lib/metrics.ts +103 -0
  46. package/templates/shared/workers/lib/platform-settings.ts +407 -0
  47. package/templates/shared/workers/lib/shared/allowances.ts +333 -0
  48. package/templates/shared/workers/lib/shared/cloudflare.ts +1362 -0
  49. package/templates/shared/workers/lib/shared/types.ts +58 -0
  50. package/templates/shared/workers/lib/telemetry-sampling.ts +360 -0
  51. package/templates/shared/workers/lib/usage/collectors/example.ts +96 -0
  52. package/templates/shared/workers/lib/usage/collectors/index.ts +128 -0
  53. package/templates/shared/workers/lib/usage/handlers/audit.ts +306 -0
  54. package/templates/shared/workers/lib/usage/handlers/backfill.ts +845 -0
  55. package/templates/shared/workers/lib/usage/handlers/behavioral.ts +429 -0
  56. package/templates/shared/workers/lib/usage/handlers/data-queries.ts +507 -0
  57. package/templates/shared/workers/lib/usage/handlers/dlq-admin.ts +364 -0
  58. package/templates/shared/workers/lib/usage/handlers/health-trends.ts +222 -0
  59. package/templates/shared/workers/lib/usage/handlers/index.ts +35 -0
  60. package/templates/shared/workers/lib/usage/handlers/usage-admin.ts +421 -0
  61. package/templates/shared/workers/lib/usage/handlers/usage-features.ts +1262 -0
  62. package/templates/shared/workers/lib/usage/handlers/usage-metrics.ts +2420 -0
  63. package/templates/shared/workers/lib/usage/handlers/usage-settings.ts +610 -0
  64. package/templates/shared/workers/lib/usage/queue/budget-enforcement.ts +1032 -0
  65. package/templates/shared/workers/lib/usage/queue/cost-budget-enforcement.ts +128 -0
  66. package/templates/shared/workers/lib/usage/queue/cost-calculator.ts +77 -0
  67. package/templates/shared/workers/lib/usage/queue/dlq-handler.ts +161 -0
  68. package/templates/shared/workers/lib/usage/queue/index.ts +19 -0
  69. package/templates/shared/workers/lib/usage/queue/telemetry-processor.ts +790 -0
  70. package/templates/shared/workers/lib/usage/scheduled/anomaly-detection.ts +732 -0
  71. package/templates/shared/workers/lib/usage/scheduled/data-collection.ts +956 -0
  72. package/templates/shared/workers/lib/usage/scheduled/error-digest.ts +343 -0
  73. package/templates/shared/workers/lib/usage/scheduled/index.ts +18 -0
  74. package/templates/shared/workers/lib/usage/scheduled/rollups.ts +1561 -0
  75. package/templates/shared/workers/lib/usage/shared/constants.ts +362 -0
  76. package/templates/shared/workers/lib/usage/shared/index.ts +14 -0
  77. package/templates/shared/workers/lib/usage/shared/types.ts +1066 -0
  78. package/templates/shared/workers/lib/usage/shared/utils.ts +795 -0
  79. package/templates/shared/workers/platform-usage.ts +1915 -0
  80. package/templates/shared/wrangler.usage.jsonc.hbs +58 -0
  81. package/templates/standard/migrations/005_error_collection.sql +162 -0
  82. package/templates/standard/workers/error-collector.ts +2670 -0
  83. package/templates/standard/workers/lib/error-collector/capture.ts +213 -0
  84. package/templates/standard/workers/lib/error-collector/digest.ts +448 -0
  85. package/templates/standard/workers/lib/error-collector/email-health-alerts.ts +262 -0
  86. package/templates/standard/workers/lib/error-collector/fingerprint.ts +258 -0
  87. package/templates/standard/workers/lib/error-collector/gap-alerts.ts +293 -0
  88. package/templates/standard/workers/lib/error-collector/github.ts +329 -0
  89. package/templates/standard/workers/lib/error-collector/types.ts +262 -0
  90. package/templates/standard/workers/lib/sentinel/gap-detection.ts +734 -0
  91. package/templates/standard/workers/lib/shared/slack-alerts.ts +585 -0
  92. package/templates/standard/workers/platform-sentinel.ts +1744 -0
  93. package/templates/standard/wrangler.error-collector.jsonc.hbs +44 -0
  94. package/templates/standard/wrangler.sentinel.jsonc.hbs +45 -0
@@ -0,0 +1,424 @@
1
+ /**
2
+ * Platform Notifications Worker
3
+ *
4
+ * Unified notification system for cross-project notifications.
5
+ * Provides API endpoints for listing, reading, and managing notifications.
6
+ *
7
+ * Storage:
8
+ * - D1: Full notification history
9
+ * - KV: Per-user read state (NOTIFICATION_READ:{email})
10
+ *
11
+ * @module workers/platform-notifications
12
+ * @created 2026-02-03
13
+ * @task task-303.1
14
+ */
15
+
16
+ import type {
17
+ KVNamespace,
18
+ ExecutionContext,
19
+ D1Database,
20
+ } from '@cloudflare/workers-types';
21
+ import {
22
+ withFeatureBudget,
23
+ CircuitBreakerError,
24
+ createLoggerFromRequest,
25
+ } from '@littlebearapps/platform-consumer-sdk';
26
+
27
+ // =============================================================================
28
+ // TYPES
29
+ // =============================================================================
30
+
31
+ interface Env {
32
+ PLATFORM_DB: D1Database;
33
+ PLATFORM_CACHE: KVNamespace;
34
+ CLOUDFLARE_ACCOUNT_ID: string;
35
+ }
36
+
37
+ interface Notification {
38
+ id: string;
39
+ category: 'error' | 'warning' | 'info' | 'success';
40
+ source: string;
41
+ source_id: string | null;
42
+ title: string;
43
+ description: string | null;
44
+ priority: 'critical' | 'high' | 'medium' | 'low' | 'info';
45
+ action_url: string | null;
46
+ action_label: string | null;
47
+ project: string | null;
48
+ created_at: number;
49
+ expires_at: number | null;
50
+ is_read?: boolean;
51
+ }
52
+
53
+ interface NotificationPreferences {
54
+ email_enabled: boolean;
55
+ slack_enabled: boolean;
56
+ in_app_enabled: boolean;
57
+ digest_frequency: 'realtime' | 'hourly' | 'daily' | 'weekly';
58
+ muted_sources: string[];
59
+ }
60
+
61
+ interface CreateNotificationRequest {
62
+ category: Notification['category'];
63
+ source: string;
64
+ source_id?: string;
65
+ title: string;
66
+ description?: string;
67
+ priority?: Notification['priority'];
68
+ action_url?: string;
69
+ action_label?: string;
70
+ project?: string;
71
+ expires_at?: number;
72
+ }
73
+
74
+ // =============================================================================
75
+ // CONSTANTS
76
+ // =============================================================================
77
+
78
+ const FEATURE_ID = 'platform:notifications:api';
79
+ const KV_READ_PREFIX = 'NOTIFICATION_READ:';
80
+ const KV_COUNT_PREFIX = 'NOTIFICATION_COUNT:';
81
+ const KV_PREFS_PREFIX = 'NOTIFICATION_PREFS:';
82
+ const READ_STATE_TTL = 90 * 24 * 60 * 60; // 90 days
83
+ const COUNT_CACHE_TTL = 5 * 60; // 5 minutes
84
+
85
+ // =============================================================================
86
+ // HELPERS
87
+ // =============================================================================
88
+
89
+ function generateId(): string {
90
+ return `notif_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
91
+ }
92
+
93
+ function getUserEmail(request: Request): string {
94
+ // Get user email from Cloudflare Access JWT
95
+ const cfAccessEmail = request.headers.get('cf-access-authenticated-user-email');
96
+ return cfAccessEmail || 'anonymous';
97
+ }
98
+
99
+ async function getReadState(kv: KVNamespace, email: string): Promise<Set<string>> {
100
+ const key = `${KV_READ_PREFIX}${email}`;
101
+ const data = await kv.get(key);
102
+ if (!data) return new Set();
103
+ try {
104
+ return new Set(JSON.parse(data));
105
+ } catch {
106
+ return new Set();
107
+ }
108
+ }
109
+
110
+ async function setReadState(
111
+ kv: KVNamespace,
112
+ email: string,
113
+ readIds: Set<string>
114
+ ): Promise<void> {
115
+ const key = `${KV_READ_PREFIX}${email}`;
116
+ // Keep only last 1000 read IDs to prevent unbounded growth
117
+ const idsArray = Array.from(readIds).slice(-1000);
118
+ await kv.put(key, JSON.stringify(idsArray), { expirationTtl: READ_STATE_TTL });
119
+ }
120
+
121
+ async function invalidateCountCache(kv: KVNamespace, email: string): Promise<void> {
122
+ const key = `${KV_COUNT_PREFIX}${email}`;
123
+ await kv.delete(key);
124
+ }
125
+
126
+ // =============================================================================
127
+ // API HANDLERS
128
+ // =============================================================================
129
+
130
+ async function handleListNotifications(
131
+ request: Request,
132
+ env: Env,
133
+ url: URL
134
+ ): Promise<Response> {
135
+ const email = getUserEmail(request);
136
+ const limit = Math.min(parseInt(url.searchParams.get('limit') || '20', 10), 100);
137
+ const offset = parseInt(url.searchParams.get('offset') || '0', 10);
138
+ const project = url.searchParams.get('project');
139
+ const source = url.searchParams.get('source');
140
+ const category = url.searchParams.get('category');
141
+
142
+ // Build query
143
+ let query = 'SELECT * FROM notifications WHERE (expires_at IS NULL OR expires_at > unixepoch())';
144
+ const params: (string | number)[] = [];
145
+
146
+ if (project) {
147
+ query += ' AND project = ?';
148
+ params.push(project);
149
+ }
150
+ if (source) {
151
+ query += ' AND source = ?';
152
+ params.push(source);
153
+ }
154
+ if (category) {
155
+ query += ' AND category = ?';
156
+ params.push(category);
157
+ }
158
+
159
+ query += ' ORDER BY created_at DESC LIMIT ? OFFSET ?';
160
+ params.push(limit, offset);
161
+
162
+ const result = await env.PLATFORM_DB.prepare(query).bind(...params).all<Notification>();
163
+ const notifications = result.results || [];
164
+
165
+ // Get read state and mark notifications
166
+ const readIds = await getReadState(env.PLATFORM_CACHE, email);
167
+ const enrichedNotifications = notifications.map((n) => ({
168
+ ...n,
169
+ is_read: readIds.has(n.id),
170
+ }));
171
+
172
+ return Response.json({
173
+ notifications: enrichedNotifications,
174
+ count: notifications.length,
175
+ offset,
176
+ limit,
177
+ });
178
+ }
179
+
180
+ async function handleUnreadCount(request: Request, env: Env): Promise<Response> {
181
+ const email = getUserEmail(request);
182
+
183
+ // Check cache first
184
+ const cacheKey = `${KV_COUNT_PREFIX}${email}`;
185
+ const cached = await env.PLATFORM_CACHE.get(cacheKey);
186
+ if (cached) {
187
+ return Response.json({ count: parseInt(cached, 10) });
188
+ }
189
+
190
+ // Get all notification IDs from last 30 days
191
+ const thirtyDaysAgo = Math.floor(Date.now() / 1000) - 30 * 24 * 60 * 60;
192
+ const result = await env.PLATFORM_DB.prepare(
193
+ `SELECT id FROM notifications
194
+ WHERE created_at > ?
195
+ AND (expires_at IS NULL OR expires_at > unixepoch())
196
+ ORDER BY created_at DESC
197
+ LIMIT 1000`
198
+ )
199
+ .bind(thirtyDaysAgo)
200
+ .all<{ id: string }>();
201
+
202
+ const allIds = new Set((result.results || []).map((r) => r.id));
203
+ const readIds = await getReadState(env.PLATFORM_CACHE, email);
204
+
205
+ // Count unread
206
+ let unreadCount = 0;
207
+ for (const id of allIds) {
208
+ if (!readIds.has(id)) {
209
+ unreadCount++;
210
+ }
211
+ }
212
+
213
+ // Cache the count
214
+ await env.PLATFORM_CACHE.put(cacheKey, String(unreadCount), {
215
+ expirationTtl: COUNT_CACHE_TTL,
216
+ });
217
+
218
+ return Response.json({ count: unreadCount });
219
+ }
220
+
221
+ async function handleMarkRead(
222
+ request: Request,
223
+ env: Env,
224
+ notificationId: string
225
+ ): Promise<Response> {
226
+ const email = getUserEmail(request);
227
+ const readIds = await getReadState(env.PLATFORM_CACHE, email);
228
+ readIds.add(notificationId);
229
+ await setReadState(env.PLATFORM_CACHE, email, readIds);
230
+ await invalidateCountCache(env.PLATFORM_CACHE, email);
231
+ return Response.json({ success: true, id: notificationId });
232
+ }
233
+
234
+ async function handleMarkAllRead(request: Request, env: Env): Promise<Response> {
235
+ const email = getUserEmail(request);
236
+
237
+ // Get all notification IDs
238
+ const result = await env.PLATFORM_DB.prepare(
239
+ `SELECT id FROM notifications
240
+ WHERE (expires_at IS NULL OR expires_at > unixepoch())
241
+ ORDER BY created_at DESC
242
+ LIMIT 1000`
243
+ ).all<{ id: string }>();
244
+
245
+ const allIds = new Set((result.results || []).map((r) => r.id));
246
+ await setReadState(env.PLATFORM_CACHE, email, allIds);
247
+ await invalidateCountCache(env.PLATFORM_CACHE, email);
248
+
249
+ return Response.json({ success: true, marked_count: allIds.size });
250
+ }
251
+
252
+ async function handleCreateNotification(
253
+ request: Request,
254
+ env: Env
255
+ ): Promise<Response> {
256
+ const body = (await request.json()) as CreateNotificationRequest;
257
+
258
+ if (!body.category || !body.source || !body.title) {
259
+ return Response.json(
260
+ { error: 'Missing required fields: category, source, title' },
261
+ { status: 400 }
262
+ );
263
+ }
264
+
265
+ const id = generateId();
266
+ const now = Math.floor(Date.now() / 1000);
267
+
268
+ await env.PLATFORM_DB.prepare(
269
+ `INSERT INTO notifications (id, category, source, source_id, title, description, priority, action_url, action_label, project, created_at, expires_at)
270
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
271
+ )
272
+ .bind(
273
+ id,
274
+ body.category,
275
+ body.source,
276
+ body.source_id || null,
277
+ body.title,
278
+ body.description || null,
279
+ body.priority || 'info',
280
+ body.action_url || null,
281
+ body.action_label || null,
282
+ body.project || null,
283
+ now,
284
+ body.expires_at || null
285
+ )
286
+ .run();
287
+
288
+ return Response.json({ success: true, id }, { status: 201 });
289
+ }
290
+
291
+ async function handleGetPreferences(request: Request, env: Env): Promise<Response> {
292
+ const email = getUserEmail(request);
293
+ const key = `${KV_PREFS_PREFIX}${email}`;
294
+ const data = await env.PLATFORM_CACHE.get(key);
295
+
296
+ const defaults: NotificationPreferences = {
297
+ email_enabled: true,
298
+ slack_enabled: true,
299
+ in_app_enabled: true,
300
+ digest_frequency: 'daily',
301
+ muted_sources: [],
302
+ };
303
+
304
+ if (!data) {
305
+ return Response.json(defaults);
306
+ }
307
+
308
+ try {
309
+ const prefs = JSON.parse(data) as Partial<NotificationPreferences>;
310
+ return Response.json({ ...defaults, ...prefs });
311
+ } catch {
312
+ return Response.json(defaults);
313
+ }
314
+ }
315
+
316
+ async function handleUpdatePreferences(
317
+ request: Request,
318
+ env: Env
319
+ ): Promise<Response> {
320
+ const email = getUserEmail(request);
321
+ const body = (await request.json()) as Partial<NotificationPreferences>;
322
+ const key = `${KV_PREFS_PREFIX}${email}`;
323
+
324
+ // Merge with existing preferences
325
+ const existing = await env.PLATFORM_CACHE.get(key);
326
+ const current = existing ? JSON.parse(existing) : {};
327
+ const updated = { ...current, ...body };
328
+
329
+ await env.PLATFORM_CACHE.put(key, JSON.stringify(updated));
330
+
331
+ return Response.json({ success: true, preferences: updated });
332
+ }
333
+
334
+ // =============================================================================
335
+ // MAIN WORKER
336
+ // =============================================================================
337
+
338
+ export default {
339
+ async fetch(
340
+ request: Request,
341
+ env: Env,
342
+ ctx: ExecutionContext
343
+ ): Promise<Response> {
344
+ const url = new URL(request.url);
345
+
346
+ // Health check (lightweight, no SDK overhead)
347
+ if (url.pathname === '/health') {
348
+ return Response.json({
349
+ status: 'ok',
350
+ service: 'platform-notifications',
351
+ timestamp: new Date().toISOString(),
352
+ });
353
+ }
354
+
355
+ const log = createLoggerFromRequest(request, env, 'platform-notifications', FEATURE_ID);
356
+
357
+ try {
358
+ // Wrap with feature budget tracking
359
+ const trackedEnv = withFeatureBudget(env, FEATURE_ID, { ctx });
360
+
361
+ // GET /notifications - List notifications
362
+ if (url.pathname === '/notifications' && request.method === 'GET') {
363
+ return await handleListNotifications(request, trackedEnv, url);
364
+ }
365
+
366
+ // GET /notifications/unread-count - Get unread count for badge
367
+ if (url.pathname === '/notifications/unread-count' && request.method === 'GET') {
368
+ return await handleUnreadCount(request, trackedEnv);
369
+ }
370
+
371
+ // POST /notifications/:id/read - Mark single as read
372
+ const readMatch = url.pathname.match(/^\/notifications\/([^/]+)\/read$/);
373
+ if (readMatch && request.method === 'POST') {
374
+ return await handleMarkRead(request, trackedEnv, readMatch[1]);
375
+ }
376
+
377
+ // POST /notifications/read-all - Mark all as read
378
+ if (url.pathname === '/notifications/read-all' && request.method === 'POST') {
379
+ return await handleMarkAllRead(request, trackedEnv);
380
+ }
381
+
382
+ // POST /notifications - Create notification (internal use)
383
+ if (url.pathname === '/notifications' && request.method === 'POST') {
384
+ return await handleCreateNotification(request, trackedEnv);
385
+ }
386
+
387
+ // GET /notifications/preferences - Get user preferences
388
+ if (url.pathname === '/notifications/preferences' && request.method === 'GET') {
389
+ return await handleGetPreferences(request, trackedEnv);
390
+ }
391
+
392
+ // PUT /notifications/preferences - Update user preferences
393
+ if (url.pathname === '/notifications/preferences' && request.method === 'PUT') {
394
+ return await handleUpdatePreferences(request, trackedEnv);
395
+ }
396
+
397
+ // API index
398
+ return Response.json({
399
+ service: 'platform-notifications',
400
+ version: '1.0.0',
401
+ endpoints: [
402
+ 'GET /health - Health check',
403
+ 'GET /notifications - List notifications (with filters)',
404
+ 'GET /notifications/unread-count - Get unread count',
405
+ 'POST /notifications/:id/read - Mark as read',
406
+ 'POST /notifications/read-all - Mark all as read',
407
+ 'POST /notifications - Create notification (internal)',
408
+ 'GET /notifications/preferences - Get user preferences',
409
+ 'PUT /notifications/preferences - Update preferences',
410
+ ],
411
+ });
412
+ } catch (error) {
413
+ if (error instanceof CircuitBreakerError) {
414
+ log.warn('Circuit breaker tripped', error);
415
+ return Response.json(
416
+ { error: 'Service temporarily unavailable' },
417
+ { status: 503, headers: { 'Retry-After': '60' } }
418
+ );
419
+ }
420
+ log.error('Request failed', error);
421
+ return Response.json({ error: 'Internal server error' }, { status: 500 });
422
+ }
423
+ },
424
+ };