@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.
- package/README.md +112 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +89 -0
- package/dist/prompts.d.ts +27 -0
- package/dist/prompts.js +80 -0
- package/dist/scaffold.d.ts +5 -0
- package/dist/scaffold.js +65 -0
- package/dist/templates.d.ts +16 -0
- package/dist/templates.js +131 -0
- package/package.json +46 -0
- package/templates/full/migrations/006_pattern_discovery.sql +199 -0
- package/templates/full/migrations/007_notifications_search.sql +127 -0
- package/templates/full/workers/lib/pattern-discovery/ai-prompt.ts +644 -0
- package/templates/full/workers/lib/pattern-discovery/clustering.ts +278 -0
- package/templates/full/workers/lib/pattern-discovery/shadow-evaluation.ts +603 -0
- package/templates/full/workers/lib/pattern-discovery/storage.ts +806 -0
- package/templates/full/workers/lib/pattern-discovery/types.ts +159 -0
- package/templates/full/workers/lib/pattern-discovery/validation.ts +278 -0
- package/templates/full/workers/pattern-discovery.ts +661 -0
- package/templates/full/workers/platform-alert-router.ts +1809 -0
- package/templates/full/workers/platform-notifications.ts +424 -0
- package/templates/full/workers/platform-search.ts +480 -0
- package/templates/full/workers/platform-settings.ts +436 -0
- package/templates/full/wrangler.alert-router.jsonc.hbs +34 -0
- package/templates/full/wrangler.notifications.jsonc.hbs +23 -0
- package/templates/full/wrangler.pattern-discovery.jsonc.hbs +33 -0
- package/templates/full/wrangler.search.jsonc.hbs +16 -0
- package/templates/full/wrangler.settings.jsonc.hbs +23 -0
- package/templates/shared/README.md.hbs +69 -0
- package/templates/shared/config/budgets.yaml.hbs +72 -0
- package/templates/shared/config/services.yaml.hbs +45 -0
- package/templates/shared/migrations/001_core_tables.sql +117 -0
- package/templates/shared/migrations/002_usage_warehouse.sql +830 -0
- package/templates/shared/migrations/003_feature_tracking.sql +250 -0
- package/templates/shared/migrations/004_settings_alerts.sql +452 -0
- package/templates/shared/migrations/seed.sql.hbs +4 -0
- package/templates/shared/package.json.hbs +21 -0
- package/templates/shared/scripts/sync-config.ts +242 -0
- package/templates/shared/tsconfig.json +12 -0
- package/templates/shared/workers/lib/analytics-engine.ts +357 -0
- package/templates/shared/workers/lib/billing.ts +293 -0
- package/templates/shared/workers/lib/circuit-breaker-middleware.ts +25 -0
- package/templates/shared/workers/lib/control.ts +292 -0
- package/templates/shared/workers/lib/economics.ts +368 -0
- package/templates/shared/workers/lib/metrics.ts +103 -0
- package/templates/shared/workers/lib/platform-settings.ts +407 -0
- package/templates/shared/workers/lib/shared/allowances.ts +333 -0
- package/templates/shared/workers/lib/shared/cloudflare.ts +1362 -0
- package/templates/shared/workers/lib/shared/types.ts +58 -0
- package/templates/shared/workers/lib/telemetry-sampling.ts +360 -0
- package/templates/shared/workers/lib/usage/collectors/example.ts +96 -0
- package/templates/shared/workers/lib/usage/collectors/index.ts +128 -0
- package/templates/shared/workers/lib/usage/handlers/audit.ts +306 -0
- package/templates/shared/workers/lib/usage/handlers/backfill.ts +845 -0
- package/templates/shared/workers/lib/usage/handlers/behavioral.ts +429 -0
- package/templates/shared/workers/lib/usage/handlers/data-queries.ts +507 -0
- package/templates/shared/workers/lib/usage/handlers/dlq-admin.ts +364 -0
- package/templates/shared/workers/lib/usage/handlers/health-trends.ts +222 -0
- package/templates/shared/workers/lib/usage/handlers/index.ts +35 -0
- package/templates/shared/workers/lib/usage/handlers/usage-admin.ts +421 -0
- package/templates/shared/workers/lib/usage/handlers/usage-features.ts +1262 -0
- package/templates/shared/workers/lib/usage/handlers/usage-metrics.ts +2420 -0
- package/templates/shared/workers/lib/usage/handlers/usage-settings.ts +610 -0
- package/templates/shared/workers/lib/usage/queue/budget-enforcement.ts +1032 -0
- package/templates/shared/workers/lib/usage/queue/cost-budget-enforcement.ts +128 -0
- package/templates/shared/workers/lib/usage/queue/cost-calculator.ts +77 -0
- package/templates/shared/workers/lib/usage/queue/dlq-handler.ts +161 -0
- package/templates/shared/workers/lib/usage/queue/index.ts +19 -0
- package/templates/shared/workers/lib/usage/queue/telemetry-processor.ts +790 -0
- package/templates/shared/workers/lib/usage/scheduled/anomaly-detection.ts +732 -0
- package/templates/shared/workers/lib/usage/scheduled/data-collection.ts +956 -0
- package/templates/shared/workers/lib/usage/scheduled/error-digest.ts +343 -0
- package/templates/shared/workers/lib/usage/scheduled/index.ts +18 -0
- package/templates/shared/workers/lib/usage/scheduled/rollups.ts +1561 -0
- package/templates/shared/workers/lib/usage/shared/constants.ts +362 -0
- package/templates/shared/workers/lib/usage/shared/index.ts +14 -0
- package/templates/shared/workers/lib/usage/shared/types.ts +1066 -0
- package/templates/shared/workers/lib/usage/shared/utils.ts +795 -0
- package/templates/shared/workers/platform-usage.ts +1915 -0
- package/templates/shared/wrangler.usage.jsonc.hbs +58 -0
- package/templates/standard/migrations/005_error_collection.sql +162 -0
- package/templates/standard/workers/error-collector.ts +2670 -0
- package/templates/standard/workers/lib/error-collector/capture.ts +213 -0
- package/templates/standard/workers/lib/error-collector/digest.ts +448 -0
- package/templates/standard/workers/lib/error-collector/email-health-alerts.ts +262 -0
- package/templates/standard/workers/lib/error-collector/fingerprint.ts +258 -0
- package/templates/standard/workers/lib/error-collector/gap-alerts.ts +293 -0
- package/templates/standard/workers/lib/error-collector/github.ts +329 -0
- package/templates/standard/workers/lib/error-collector/types.ts +262 -0
- package/templates/standard/workers/lib/sentinel/gap-detection.ts +734 -0
- package/templates/standard/workers/lib/shared/slack-alerts.ts +585 -0
- package/templates/standard/workers/platform-sentinel.ts +1744 -0
- package/templates/standard/wrangler.error-collector.jsonc.hbs +44 -0
- 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
|
+
};
|