@edgebasejs/worker 0.1.8

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 (113) hide show
  1. package/dist/adapter-d1/src/d1-adapter.d.ts +29 -0
  2. package/dist/adapter-d1/src/d1-adapter.d.ts.map +1 -0
  3. package/dist/adapter-d1/src/d1-adapter.js +36 -0
  4. package/dist/adapter-d1/src/d1-adapter.js.map +1 -0
  5. package/dist/adapter-d1/src/index.d.ts +3 -0
  6. package/dist/adapter-d1/src/index.d.ts.map +1 -0
  7. package/dist/adapter-d1/src/index.js +3 -0
  8. package/dist/adapter-d1/src/index.js.map +1 -0
  9. package/dist/adapter-d1/src/schema-to-sql.d.ts +18 -0
  10. package/dist/adapter-d1/src/schema-to-sql.d.ts.map +1 -0
  11. package/dist/adapter-d1/src/schema-to-sql.js +304 -0
  12. package/dist/adapter-d1/src/schema-to-sql.js.map +1 -0
  13. package/dist/core/src/access-rules/column-security.d.ts +80 -0
  14. package/dist/core/src/access-rules/column-security.d.ts.map +1 -0
  15. package/dist/core/src/access-rules/column-security.js +191 -0
  16. package/dist/core/src/access-rules/column-security.js.map +1 -0
  17. package/dist/core/src/access-rules/engine.d.ts +26 -0
  18. package/dist/core/src/access-rules/engine.d.ts.map +1 -0
  19. package/dist/core/src/access-rules/engine.js +76 -0
  20. package/dist/core/src/access-rules/engine.js.map +1 -0
  21. package/dist/core/src/access-rules/index.d.ts +3 -0
  22. package/dist/core/src/access-rules/index.d.ts.map +1 -0
  23. package/dist/core/src/access-rules/index.js +3 -0
  24. package/dist/core/src/access-rules/index.js.map +1 -0
  25. package/dist/core/src/audit/audit-manager.d.ts +108 -0
  26. package/dist/core/src/audit/audit-manager.d.ts.map +1 -0
  27. package/dist/core/src/audit/audit-manager.js +265 -0
  28. package/dist/core/src/audit/audit-manager.js.map +1 -0
  29. package/dist/core/src/auth/auth-service.d.ts +71 -0
  30. package/dist/core/src/auth/auth-service.d.ts.map +1 -0
  31. package/dist/core/src/auth/auth-service.js +177 -0
  32. package/dist/core/src/auth/auth-service.js.map +1 -0
  33. package/dist/core/src/auth/index.d.ts +4 -0
  34. package/dist/core/src/auth/index.d.ts.map +1 -0
  35. package/dist/core/src/auth/index.js +4 -0
  36. package/dist/core/src/auth/index.js.map +1 -0
  37. package/dist/core/src/encryption/encryption-manager.d.ts +97 -0
  38. package/dist/core/src/encryption/encryption-manager.d.ts.map +1 -0
  39. package/dist/core/src/encryption/encryption-manager.js +224 -0
  40. package/dist/core/src/encryption/encryption-manager.js.map +1 -0
  41. package/dist/core/src/index.d.ts +16 -0
  42. package/dist/core/src/index.d.ts.map +1 -0
  43. package/dist/core/src/index.js +16 -0
  44. package/dist/core/src/index.js.map +1 -0
  45. package/dist/core/src/realtime/change-notifier.d.ts +50 -0
  46. package/dist/core/src/realtime/change-notifier.d.ts.map +1 -0
  47. package/dist/core/src/realtime/change-notifier.js +145 -0
  48. package/dist/core/src/realtime/change-notifier.js.map +1 -0
  49. package/dist/core/src/realtime/message-types.d.ts +39 -0
  50. package/dist/core/src/realtime/message-types.d.ts.map +1 -0
  51. package/dist/core/src/realtime/message-types.js +5 -0
  52. package/dist/core/src/realtime/message-types.js.map +1 -0
  53. package/dist/core/src/realtime/subscription-manager.d.ts +67 -0
  54. package/dist/core/src/realtime/subscription-manager.d.ts.map +1 -0
  55. package/dist/core/src/realtime/subscription-manager.js +229 -0
  56. package/dist/core/src/realtime/subscription-manager.js.map +1 -0
  57. package/dist/core/src/search/search-manager.d.ts +93 -0
  58. package/dist/core/src/search/search-manager.d.ts.map +1 -0
  59. package/dist/core/src/search/search-manager.js +258 -0
  60. package/dist/core/src/search/search-manager.js.map +1 -0
  61. package/dist/core/src/storage/file-manager.d.ts +138 -0
  62. package/dist/core/src/storage/file-manager.d.ts.map +1 -0
  63. package/dist/core/src/storage/file-manager.js +224 -0
  64. package/dist/core/src/storage/file-manager.js.map +1 -0
  65. package/dist/core/src/sync/batch-processor.d.ts +97 -0
  66. package/dist/core/src/sync/batch-processor.d.ts.map +1 -0
  67. package/dist/core/src/sync/batch-processor.js +313 -0
  68. package/dist/core/src/sync/batch-processor.js.map +1 -0
  69. package/dist/core/src/sync/csv-processor.d.ts +66 -0
  70. package/dist/core/src/sync/csv-processor.d.ts.map +1 -0
  71. package/dist/core/src/sync/csv-processor.js +223 -0
  72. package/dist/core/src/sync/csv-processor.js.map +1 -0
  73. package/dist/core/src/sync/index.d.ts +3 -0
  74. package/dist/core/src/sync/index.d.ts.map +1 -0
  75. package/dist/core/src/sync/index.js +3 -0
  76. package/dist/core/src/sync/index.js.map +1 -0
  77. package/dist/core/src/sync/sync-engine.d.ts +68 -0
  78. package/dist/core/src/sync/sync-engine.d.ts.map +1 -0
  79. package/dist/core/src/sync/sync-engine.js +317 -0
  80. package/dist/core/src/sync/sync-engine.js.map +1 -0
  81. package/dist/core/src/sync/transaction-manager.d.ts +83 -0
  82. package/dist/core/src/sync/transaction-manager.d.ts.map +1 -0
  83. package/dist/core/src/sync/transaction-manager.js +227 -0
  84. package/dist/core/src/sync/transaction-manager.js.map +1 -0
  85. package/dist/core/src/webhooks/webhook-manager.d.ts +137 -0
  86. package/dist/core/src/webhooks/webhook-manager.d.ts.map +1 -0
  87. package/dist/core/src/webhooks/webhook-manager.js +334 -0
  88. package/dist/core/src/webhooks/webhook-manager.js.map +1 -0
  89. package/dist/shared-types/src/admin.d.ts +101 -0
  90. package/dist/shared-types/src/admin.d.ts.map +1 -0
  91. package/dist/shared-types/src/admin.js +3 -0
  92. package/dist/shared-types/src/admin.js.map +1 -0
  93. package/dist/shared-types/src/auth.d.ts +27 -0
  94. package/dist/shared-types/src/auth.d.ts.map +1 -0
  95. package/dist/shared-types/src/auth.js +2 -0
  96. package/dist/shared-types/src/auth.js.map +1 -0
  97. package/dist/shared-types/src/index.d.ts +5 -0
  98. package/dist/shared-types/src/index.d.ts.map +1 -0
  99. package/dist/shared-types/src/index.js +5 -0
  100. package/dist/shared-types/src/index.js.map +1 -0
  101. package/dist/shared-types/src/schema.d.ts +34 -0
  102. package/dist/shared-types/src/schema.d.ts.map +1 -0
  103. package/dist/shared-types/src/schema.js +2 -0
  104. package/dist/shared-types/src/schema.js.map +1 -0
  105. package/dist/shared-types/src/sync.d.ts +37 -0
  106. package/dist/shared-types/src/sync.d.ts.map +1 -0
  107. package/dist/shared-types/src/sync.js +2 -0
  108. package/dist/shared-types/src/sync.js.map +1 -0
  109. package/dist/worker/src/index.d.ts +18 -0
  110. package/dist/worker/src/index.d.ts.map +1 -0
  111. package/dist/worker/src/index.js +2470 -0
  112. package/dist/worker/src/index.js.map +1 -0
  113. package/package.json +30 -0
@@ -0,0 +1,2470 @@
1
+ import { Hono } from 'hono';
2
+ import { cors } from 'hono/cors';
3
+ import { ServerSyncEngine, hashPassword, verifyPassword, createJWT, parseJWT, SubscriptionManager, ChangeNotifier, TransactionManager, BatchProcessor, CSVProcessor, FileStorageManager, WebhookManager, SearchManager, AuditManager, EncryptionManager, } from '@edgebasejs/core';
4
+ import { D1SyncDatabase, initializeDatabase } from '@edgebasejs/adapter-d1';
5
+ function jsonError(c, status, error, code) {
6
+ return c.json({ error, ...(code ? { code } : {}) }, status);
7
+ }
8
+ function getBearerToken(c) {
9
+ const auth = c.req.header('Authorization');
10
+ if (!auth) {
11
+ return null;
12
+ }
13
+ return auth.replace(/^Bearer\s+/i, '').trim() || null;
14
+ }
15
+ export function createEdgeBaseWorker(options) {
16
+ const { schema, getDb, getJwtSecret, getEnvironment, cors: corsOptions, apiVersion = '0.1.0', autoInitialize = false, } = options;
17
+ const app = new Hono();
18
+ app.use('*', cors(corsOptions));
19
+ let dbInitialized = false;
20
+ let subscriptionManager = null;
21
+ let changeNotifier = null;
22
+ let transactionManager = null;
23
+ let webhookManager = null;
24
+ let searchManager = null;
25
+ let auditManager = null;
26
+ let encryptionManager = null;
27
+ app.use('*', async (c, next) => {
28
+ const shouldAutoInit = typeof autoInitialize === 'function' ? autoInitialize(c) : autoInitialize;
29
+ if (shouldAutoInit && !dbInitialized) {
30
+ try {
31
+ await initializeDatabase(getDb(c), schema);
32
+ dbInitialized = true;
33
+ // Initialize managers
34
+ const db = new D1SyncDatabase(getDb(c));
35
+ subscriptionManager = new SubscriptionManager(db);
36
+ changeNotifier = new ChangeNotifier(subscriptionManager);
37
+ transactionManager = new TransactionManager(db);
38
+ webhookManager = new WebhookManager(db);
39
+ searchManager = new SearchManager(db);
40
+ auditManager = new AuditManager(db);
41
+ encryptionManager = new EncryptionManager();
42
+ // Initialize encryption key if provided
43
+ const getEncryptionKey = options.getEncryptionKey;
44
+ if (getEncryptionKey) {
45
+ const encryptionKey = getEncryptionKey(c);
46
+ if (encryptionKey) {
47
+ await encryptionManager.initializeKey(encryptionKey);
48
+ }
49
+ }
50
+ // Load existing subscriptions from database
51
+ await subscriptionManager.loadFromDatabase();
52
+ }
53
+ catch (error) {
54
+ console.error('Database initialization error:', error);
55
+ }
56
+ }
57
+ await next();
58
+ });
59
+ async function requireAccessUser(c) {
60
+ const token = getBearerToken(c);
61
+ if (!token) {
62
+ return jsonError(c, 401, 'Unauthorized', 'UNAUTHORIZED');
63
+ }
64
+ const payload = parseJWT(token);
65
+ if (!payload || payload.type !== 'access') {
66
+ return jsonError(c, 401, 'Invalid token', 'UNAUTHORIZED');
67
+ }
68
+ const db = getDb(c);
69
+ const userRow = await db
70
+ .prepare('SELECT id, email, created_at, updated_at FROM users WHERE id = ?')
71
+ .bind(payload.userId)
72
+ .first();
73
+ if (!userRow) {
74
+ return jsonError(c, 401, 'User not found', 'UNAUTHORIZED');
75
+ }
76
+ const user = {
77
+ id: userRow.id,
78
+ email: userRow.email,
79
+ createdAt: userRow.created_at,
80
+ updatedAt: userRow.updated_at,
81
+ };
82
+ return { user, payload };
83
+ }
84
+ async function requireAccessAdmin(c) {
85
+ const token = getBearerToken(c);
86
+ if (!token) {
87
+ return jsonError(c, 401, 'Unauthorized', 'UNAUTHORIZED');
88
+ }
89
+ const payload = parseJWT(token);
90
+ if (!payload || payload.type !== 'access') {
91
+ return jsonError(c, 401, 'Invalid token', 'UNAUTHORIZED');
92
+ }
93
+ const db = getDb(c);
94
+ const adminRow = await db
95
+ .prepare('SELECT id, email, role, is_active, created_at, updated_at FROM admins WHERE id = ?')
96
+ .bind(payload.userId)
97
+ .first();
98
+ if (!adminRow || !adminRow.is_active) {
99
+ return jsonError(c, 401, 'Admin not found', 'UNAUTHORIZED');
100
+ }
101
+ const admin = {
102
+ id: adminRow.id,
103
+ email: adminRow.email,
104
+ role: adminRow.role,
105
+ isActive: adminRow.is_active === 1,
106
+ createdAt: adminRow.created_at,
107
+ updatedAt: adminRow.updated_at,
108
+ };
109
+ return { admin, payload };
110
+ }
111
+ async function hasAnyAdmin(db) {
112
+ const row = await db.prepare('SELECT id FROM admins LIMIT 1').first();
113
+ return !!row;
114
+ }
115
+ async function hasUserId(db, userId) {
116
+ const row = await db.prepare('SELECT id FROM users WHERE id = ?').bind(userId).first();
117
+ return !!row;
118
+ }
119
+ async function listTables(db) {
120
+ const tablesResult = await db
121
+ .prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' ORDER BY name")
122
+ .all();
123
+ return (tablesResult?.results || []).map((row) => row.name);
124
+ }
125
+ function requireValidTableName(tableName, allowed) {
126
+ if (!allowed.includes(tableName)) {
127
+ return null;
128
+ }
129
+ return tableName.replace(/"/g, '""');
130
+ }
131
+ function isEntityTable(tableName) {
132
+ return !!schema.entities[tableName];
133
+ }
134
+ function getEntityPrimaryKey(tableName) {
135
+ const entity = schema.entities[tableName];
136
+ if (!entity)
137
+ return null;
138
+ const primary = Object.entries(entity.fields).find(([, field]) => field.primary);
139
+ return primary ? primary[0] : 'id';
140
+ }
141
+ async function updateSyncMetadata(db, entity, recordId, deletedAt) {
142
+ const now = Date.now();
143
+ const existing = await db
144
+ .prepare('SELECT version FROM sync_metadata WHERE entity = ? AND record_id = ?')
145
+ .bind(entity, recordId)
146
+ .first();
147
+ const currentVersion = existing?.version || 0;
148
+ const nextVersion = currentVersion + 1;
149
+ if (deletedAt) {
150
+ await db
151
+ .prepare('INSERT OR REPLACE INTO sync_metadata (entity, record_id, version, updated_at, deleted_at) VALUES (?, ?, ?, ?, ?)')
152
+ .bind(entity, recordId, nextVersion, now, deletedAt)
153
+ .run();
154
+ }
155
+ else {
156
+ await db
157
+ .prepare('INSERT OR REPLACE INTO sync_metadata (entity, record_id, version, updated_at) VALUES (?, ?, ?, ?)')
158
+ .bind(entity, recordId, nextVersion, now)
159
+ .run();
160
+ }
161
+ return nextVersion;
162
+ }
163
+ async function notifyAdminChange(entity, operation, record, recordId, version) {
164
+ if (!subscriptionManager || !changeNotifier) {
165
+ return;
166
+ }
167
+ const subscriptions = subscriptionManager.getSubscriptionsForEntity(entity);
168
+ if (subscriptions.length === 0) {
169
+ return;
170
+ }
171
+ const message = {
172
+ type: 'change',
173
+ entity,
174
+ operation,
175
+ record: operation !== 'delete' ? record : undefined,
176
+ recordId,
177
+ timestamp: Date.now(),
178
+ version,
179
+ };
180
+ changeNotifier.broadcastToAll(message, subscriptions.map((sub) => sub.subscriptionId));
181
+ }
182
+ /**
183
+ * GET /health
184
+ */
185
+ app.get('/health', (c) => {
186
+ return c.json({ status: 'ok', version: apiVersion });
187
+ });
188
+ /**
189
+ * GET /info
190
+ */
191
+ app.get('/info', (c) => {
192
+ return c.json({
193
+ name: 'EdgeBase Worker',
194
+ version: apiVersion,
195
+ environment: getEnvironment ? getEnvironment(c) || 'unknown' : 'unknown',
196
+ });
197
+ });
198
+ /**
199
+ * POST /auth/register
200
+ */
201
+ app.post('/auth/register', async (c) => {
202
+ try {
203
+ const { email, password } = await c.req.json();
204
+ if (!email || !password) {
205
+ return jsonError(c, 400, 'Email and password are required', 'VALIDATION_ERROR');
206
+ }
207
+ const db = getDb(c);
208
+ const jwtSecret = getJwtSecret(c);
209
+ // Check if user already exists
210
+ const existing = await db.prepare('SELECT id FROM users WHERE email = ?').bind(email).first();
211
+ if (existing) {
212
+ return jsonError(c, 409, 'User already exists', 'CONFLICT');
213
+ }
214
+ // Hash password
215
+ const passwordHash = await hashPassword(password);
216
+ // Create user
217
+ const userId = `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
218
+ const now = Date.now();
219
+ await db
220
+ .prepare('INSERT INTO users (id, email, password_hash, created_at, updated_at) VALUES (?, ?, ?, ?, ?)')
221
+ .bind(userId, email, passwordHash, now, now)
222
+ .run();
223
+ // Generate tokens
224
+ const accessToken = createJWT({ userId, email, type: 'access' }, jwtSecret, 3600);
225
+ const refreshToken = createJWT({ userId, email, type: 'refresh' }, jwtSecret, 604800);
226
+ // Store refresh token
227
+ const refreshTokenId = `rt_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
228
+ await db
229
+ .prepare('INSERT INTO refresh_tokens (id, user_id, token, expires_at, created_at) VALUES (?, ?, ?, ?, ?)')
230
+ .bind(refreshTokenId, userId, refreshToken, now + 604800000, now)
231
+ .run();
232
+ const user = {
233
+ id: userId,
234
+ email,
235
+ createdAt: now,
236
+ updatedAt: now,
237
+ };
238
+ const response = {
239
+ user,
240
+ tokens: {
241
+ accessToken,
242
+ refreshToken,
243
+ expiresIn: 3600,
244
+ },
245
+ };
246
+ return c.json(response, 201);
247
+ }
248
+ catch (error) {
249
+ console.error('Register error:', error);
250
+ return jsonError(c, 400, error instanceof Error ? error.message : 'Registration failed', 'VALIDATION_ERROR');
251
+ }
252
+ });
253
+ /**
254
+ * POST /auth/login
255
+ */
256
+ app.post('/auth/login', async (c) => {
257
+ try {
258
+ const { email, password } = await c.req.json();
259
+ if (!email || !password) {
260
+ return jsonError(c, 400, 'Email and password are required', 'VALIDATION_ERROR');
261
+ }
262
+ const db = getDb(c);
263
+ const jwtSecret = getJwtSecret(c);
264
+ // Get user from DB
265
+ const userRow = await db
266
+ .prepare('SELECT id, email, password_hash, created_at, updated_at FROM users WHERE email = ?')
267
+ .bind(email)
268
+ .first();
269
+ if (!userRow) {
270
+ return jsonError(c, 401, 'Invalid credentials', 'UNAUTHORIZED');
271
+ }
272
+ // Verify password
273
+ const passwordValid = await verifyPassword(password, userRow.password_hash);
274
+ if (!passwordValid) {
275
+ return jsonError(c, 401, 'Invalid credentials', 'UNAUTHORIZED');
276
+ }
277
+ // Generate tokens
278
+ const accessToken = createJWT({ userId: userRow.id, email: userRow.email, type: 'access' }, jwtSecret, 3600);
279
+ const refreshToken = createJWT({ userId: userRow.id, email: userRow.email, type: 'refresh' }, jwtSecret, 604800);
280
+ // Store refresh token
281
+ const now = Date.now();
282
+ const refreshTokenId = `rt_${now}_${Math.random().toString(36).substr(2, 9)}`;
283
+ await db
284
+ .prepare('INSERT INTO refresh_tokens (id, user_id, token, expires_at, created_at) VALUES (?, ?, ?, ?, ?)')
285
+ .bind(refreshTokenId, userRow.id, refreshToken, now + 604800000, now)
286
+ .run();
287
+ const user = {
288
+ id: userRow.id,
289
+ email: userRow.email,
290
+ createdAt: userRow.created_at,
291
+ updatedAt: userRow.updated_at,
292
+ };
293
+ const response = {
294
+ user,
295
+ tokens: {
296
+ accessToken,
297
+ refreshToken,
298
+ expiresIn: 3600,
299
+ },
300
+ };
301
+ return c.json(response);
302
+ }
303
+ catch (error) {
304
+ console.error('Login error:', error);
305
+ return jsonError(c, 400, error instanceof Error ? error.message : 'Login failed', 'API_ERROR');
306
+ }
307
+ });
308
+ /**
309
+ * POST /admin/auth/login
310
+ */
311
+ app.post('/admin/auth/login', async (c) => {
312
+ try {
313
+ const body = await c.req.json();
314
+ const { email, password } = body || {};
315
+ if (!email || !password) {
316
+ return jsonError(c, 400, 'Email and password are required', 'VALIDATION_ERROR');
317
+ }
318
+ const db = getDb(c);
319
+ const jwtSecret = getJwtSecret(c);
320
+ const adminRow = await db
321
+ .prepare('SELECT id, email, password_hash, role, is_active, created_at, updated_at FROM admins WHERE email = ?')
322
+ .bind(email)
323
+ .first();
324
+ if (!adminRow || !adminRow.is_active) {
325
+ return jsonError(c, 401, 'Invalid credentials', 'UNAUTHORIZED');
326
+ }
327
+ const passwordValid = await verifyPassword(password, adminRow.password_hash);
328
+ if (!passwordValid) {
329
+ return jsonError(c, 401, 'Invalid credentials', 'UNAUTHORIZED');
330
+ }
331
+ const accessToken = createJWT({ userId: adminRow.id, email: adminRow.email, type: 'access' }, jwtSecret, 3600);
332
+ const refreshToken = createJWT({ userId: adminRow.id, email: adminRow.email, type: 'refresh' }, jwtSecret, 604800);
333
+ const now = Date.now();
334
+ const refreshTokenId = `art_${now}_${Math.random().toString(36).substr(2, 9)}`;
335
+ await db
336
+ .prepare('INSERT INTO admin_refresh_tokens (id, admin_id, token, expires_at, created_at) VALUES (?, ?, ?, ?, ?)')
337
+ .bind(refreshTokenId, adminRow.id, refreshToken, now + 604800000, now)
338
+ .run();
339
+ const admin = {
340
+ id: adminRow.id,
341
+ email: adminRow.email,
342
+ role: adminRow.role,
343
+ isActive: adminRow.is_active === 1,
344
+ createdAt: adminRow.created_at,
345
+ updatedAt: adminRow.updated_at,
346
+ };
347
+ const response = {
348
+ admin,
349
+ accessToken,
350
+ refreshToken,
351
+ expiresIn: 3600,
352
+ };
353
+ return c.json(response);
354
+ }
355
+ catch (error) {
356
+ console.error('Admin login error:', error);
357
+ return jsonError(c, 400, error instanceof Error ? error.message : 'Login failed', 'API_ERROR');
358
+ }
359
+ });
360
+ /**
361
+ * POST /admin/auth/refresh
362
+ */
363
+ app.post('/admin/auth/refresh', async (c) => {
364
+ try {
365
+ const body = await c.req.json();
366
+ const refreshToken = body?.refreshToken;
367
+ if (!refreshToken) {
368
+ return jsonError(c, 400, 'Refresh token is required', 'VALIDATION_ERROR');
369
+ }
370
+ const payload = parseJWT(refreshToken);
371
+ if (!payload || payload.type !== 'refresh') {
372
+ return jsonError(c, 401, 'Invalid refresh token', 'UNAUTHORIZED');
373
+ }
374
+ const db = getDb(c);
375
+ const jwtSecret = getJwtSecret(c);
376
+ const tokenRow = await db
377
+ .prepare(`SELECT r.admin_id, r.expires_at, a.email
378
+ FROM admin_refresh_tokens r
379
+ JOIN admins a ON a.id = r.admin_id
380
+ WHERE r.token = ?`)
381
+ .bind(refreshToken)
382
+ .first();
383
+ if (!tokenRow) {
384
+ return jsonError(c, 401, 'Invalid refresh token', 'UNAUTHORIZED');
385
+ }
386
+ if (tokenRow.expires_at < Date.now()) {
387
+ await db
388
+ .prepare('DELETE FROM admin_refresh_tokens WHERE token = ?')
389
+ .bind(refreshToken)
390
+ .run();
391
+ return jsonError(c, 401, 'Refresh token expired', 'UNAUTHORIZED');
392
+ }
393
+ const accessToken = createJWT({ userId: tokenRow.admin_id, email: tokenRow.email, type: 'access' }, jwtSecret, 3600);
394
+ return c.json({ accessToken, expiresIn: 3600 });
395
+ }
396
+ catch (error) {
397
+ console.error('Admin refresh token error:', error);
398
+ return jsonError(c, 400, error instanceof Error ? error.message : 'Token refresh failed', 'API_ERROR');
399
+ }
400
+ });
401
+ /**
402
+ * POST /admin/auth/logout
403
+ */
404
+ app.post('/admin/auth/logout', async (c) => {
405
+ const auth = await requireAccessAdmin(c);
406
+ if (auth instanceof Response) {
407
+ return auth;
408
+ }
409
+ try {
410
+ const db = getDb(c);
411
+ await db
412
+ .prepare('DELETE FROM admin_refresh_tokens WHERE admin_id = ?')
413
+ .bind(auth.admin.id)
414
+ .run();
415
+ return c.json({ message: 'Logged out successfully', adminId: auth.admin.id });
416
+ }
417
+ catch (error) {
418
+ console.error('Admin logout error:', error);
419
+ return jsonError(c, 500, 'Logout failed', 'API_ERROR');
420
+ }
421
+ });
422
+ /**
423
+ * GET /admin/admins
424
+ */
425
+ app.get('/admin/admins', async (c) => {
426
+ try {
427
+ const db = getDb(c);
428
+ const anyAdmin = await hasAnyAdmin(db);
429
+ if (!anyAdmin) {
430
+ return c.json([]);
431
+ }
432
+ const auth = await requireAccessAdmin(c);
433
+ if (auth instanceof Response) {
434
+ return auth;
435
+ }
436
+ const rows = await db
437
+ .prepare('SELECT id, email, role, is_active, created_at, updated_at FROM admins')
438
+ .all();
439
+ const admins = (rows?.results || []).map((row) => ({
440
+ id: row.id,
441
+ email: row.email,
442
+ role: row.role,
443
+ isActive: row.is_active === 1,
444
+ createdAt: row.created_at,
445
+ updatedAt: row.updated_at,
446
+ }));
447
+ return c.json(admins);
448
+ }
449
+ catch (error) {
450
+ console.error('Admin list error:', error);
451
+ return jsonError(c, 400, error instanceof Error ? error.message : 'Failed to fetch admins', 'API_ERROR');
452
+ }
453
+ });
454
+ /**
455
+ * POST /admin/admins
456
+ */
457
+ app.post('/admin/admins', async (c) => {
458
+ try {
459
+ const db = getDb(c);
460
+ const anyAdmin = await hasAnyAdmin(db);
461
+ if (anyAdmin) {
462
+ const auth = await requireAccessAdmin(c);
463
+ if (auth instanceof Response) {
464
+ return auth;
465
+ }
466
+ if (auth.admin.role !== 'super_admin') {
467
+ return jsonError(c, 403, 'Insufficient permissions', 'FORBIDDEN');
468
+ }
469
+ }
470
+ const body = await c.req.json();
471
+ const { email, password, role } = body || {};
472
+ if (!email || !password || !role) {
473
+ return jsonError(c, 400, 'Email, password, and role are required', 'VALIDATION_ERROR');
474
+ }
475
+ const existing = await db.prepare('SELECT id FROM admins WHERE email = ?').bind(email).first();
476
+ if (existing) {
477
+ return jsonError(c, 409, 'Admin already exists', 'CONFLICT');
478
+ }
479
+ const passwordHash = await hashPassword(password);
480
+ const adminId = `admin_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
481
+ const now = Date.now();
482
+ await db
483
+ .prepare('INSERT INTO admins (id, email, password_hash, role, is_active, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)')
484
+ .bind(adminId, email, passwordHash, role, 1, now, now)
485
+ .run();
486
+ const admin = {
487
+ id: adminId,
488
+ email,
489
+ role,
490
+ isActive: true,
491
+ createdAt: now,
492
+ updatedAt: now,
493
+ };
494
+ return c.json(admin, 201);
495
+ }
496
+ catch (error) {
497
+ console.error('Admin create error:', error);
498
+ return jsonError(c, 400, error instanceof Error ? error.message : 'Admin creation failed', 'API_ERROR');
499
+ }
500
+ });
501
+ /**
502
+ * GET /admin/users
503
+ */
504
+ app.get('/admin/users', async (c) => {
505
+ const auth = await requireAccessAdmin(c);
506
+ if (auth instanceof Response) {
507
+ return auth;
508
+ }
509
+ try {
510
+ const db = getDb(c);
511
+ const page = Math.max(parseInt(c.req.query('page') || '1', 10), 1);
512
+ const pageSize = Math.min(Math.max(parseInt(c.req.query('pageSize') || '20', 10), 1), 100);
513
+ const search = c.req.query('search');
514
+ const offset = (page - 1) * pageSize;
515
+ const params = [];
516
+ let whereClause = '';
517
+ if (search) {
518
+ whereClause = 'WHERE email LIKE ?';
519
+ params.push(`%${search}%`);
520
+ }
521
+ const countRow = await db
522
+ .prepare(`SELECT COUNT(*) as total FROM users ${whereClause}`)
523
+ .bind(...params)
524
+ .first();
525
+ const total = countRow?.total || 0;
526
+ const usersResult = await db
527
+ .prepare(`SELECT id, email, created_at, updated_at FROM users ${whereClause} ORDER BY created_at DESC LIMIT ? OFFSET ?`)
528
+ .bind(...params, pageSize, offset)
529
+ .all();
530
+ const data = (usersResult?.results || []).map((row) => ({
531
+ id: row.id,
532
+ email: row.email,
533
+ createdAt: row.created_at,
534
+ updatedAt: row.updated_at,
535
+ }));
536
+ return c.json({
537
+ data,
538
+ pagination: {
539
+ page,
540
+ pageSize,
541
+ total,
542
+ totalPages: Math.ceil(total / pageSize) || 1,
543
+ },
544
+ });
545
+ }
546
+ catch (error) {
547
+ console.error('Admin users list error:', error);
548
+ return jsonError(c, 400, error instanceof Error ? error.message : 'Failed to fetch users', 'API_ERROR');
549
+ }
550
+ });
551
+ /**
552
+ * GET /admin/users/:id
553
+ */
554
+ app.get('/admin/users/:id', async (c) => {
555
+ const auth = await requireAccessAdmin(c);
556
+ if (auth instanceof Response) {
557
+ return auth;
558
+ }
559
+ try {
560
+ const db = getDb(c);
561
+ const userId = c.req.param('id');
562
+ const row = await db
563
+ .prepare('SELECT id, email, created_at, updated_at FROM users WHERE id = ?')
564
+ .bind(userId)
565
+ .first();
566
+ if (!row) {
567
+ return jsonError(c, 404, 'User not found', 'NOT_FOUND');
568
+ }
569
+ return c.json({
570
+ id: row.id,
571
+ email: row.email,
572
+ createdAt: row.created_at,
573
+ updatedAt: row.updated_at,
574
+ });
575
+ }
576
+ catch (error) {
577
+ console.error('Admin get user error:', error);
578
+ return jsonError(c, 400, error instanceof Error ? error.message : 'Failed to fetch user', 'API_ERROR');
579
+ }
580
+ });
581
+ /**
582
+ * PATCH /admin/users/:id
583
+ */
584
+ app.patch('/admin/users/:id', async (c) => {
585
+ const auth = await requireAccessAdmin(c);
586
+ if (auth instanceof Response) {
587
+ return auth;
588
+ }
589
+ try {
590
+ const db = getDb(c);
591
+ const userId = c.req.param('id');
592
+ const updates = await c.req.json();
593
+ const fields = [];
594
+ const values = [];
595
+ if (typeof updates.email === 'string' && updates.email.trim()) {
596
+ fields.push('email = ?');
597
+ values.push(updates.email.trim());
598
+ }
599
+ if (fields.length === 0) {
600
+ return jsonError(c, 400, 'No valid fields to update', 'VALIDATION_ERROR');
601
+ }
602
+ fields.push('updated_at = ?');
603
+ values.push(Date.now());
604
+ await db
605
+ .prepare(`UPDATE users SET ${fields.join(', ')} WHERE id = ?`)
606
+ .bind(...values, userId)
607
+ .run();
608
+ const row = await db
609
+ .prepare('SELECT id, email, created_at, updated_at FROM users WHERE id = ?')
610
+ .bind(userId)
611
+ .first();
612
+ if (!row) {
613
+ return jsonError(c, 404, 'User not found', 'NOT_FOUND');
614
+ }
615
+ return c.json({
616
+ id: row.id,
617
+ email: row.email,
618
+ createdAt: row.created_at,
619
+ updatedAt: row.updated_at,
620
+ });
621
+ }
622
+ catch (error) {
623
+ console.error('Admin update user error:', error);
624
+ return jsonError(c, 400, error instanceof Error ? error.message : 'Failed to update user', 'API_ERROR');
625
+ }
626
+ });
627
+ /**
628
+ * DELETE /admin/users/:id
629
+ */
630
+ app.delete('/admin/users/:id', async (c) => {
631
+ const auth = await requireAccessAdmin(c);
632
+ if (auth instanceof Response) {
633
+ return auth;
634
+ }
635
+ try {
636
+ const db = getDb(c);
637
+ const userId = c.req.param('id');
638
+ const existing = await db.prepare('SELECT id FROM users WHERE id = ?').bind(userId).first();
639
+ if (!existing) {
640
+ return jsonError(c, 404, 'User not found', 'NOT_FOUND');
641
+ }
642
+ await db.prepare('DELETE FROM users WHERE id = ?').bind(userId).run();
643
+ return c.json({ success: true, message: 'User deleted' });
644
+ }
645
+ catch (error) {
646
+ console.error('Admin delete user error:', error);
647
+ return jsonError(c, 400, error instanceof Error ? error.message : 'Failed to delete user', 'API_ERROR');
648
+ }
649
+ });
650
+ /**
651
+ * GET /admin/database/tables
652
+ */
653
+ app.get('/admin/database/tables', async (c) => {
654
+ const auth = await requireAccessAdmin(c);
655
+ if (auth instanceof Response) {
656
+ return auth;
657
+ }
658
+ try {
659
+ const db = getDb(c);
660
+ const tableNames = await listTables(db);
661
+ const tables = [];
662
+ for (const name of tableNames) {
663
+ const safeName = requireValidTableName(name, tableNames);
664
+ if (!safeName)
665
+ continue;
666
+ const countRow = await db
667
+ .prepare(`SELECT COUNT(*) as count FROM "${safeName}"`)
668
+ .first();
669
+ tables.push({ name, rowCount: countRow?.count || 0 });
670
+ }
671
+ return c.json(tables);
672
+ }
673
+ catch (error) {
674
+ console.error('Admin tables error:', error);
675
+ return jsonError(c, 400, error instanceof Error ? error.message : 'Failed to fetch tables', 'API_ERROR');
676
+ }
677
+ });
678
+ /**
679
+ * GET /admin/database/tables/:name/schema
680
+ */
681
+ app.get('/admin/database/tables/:name/schema', async (c) => {
682
+ const auth = await requireAccessAdmin(c);
683
+ if (auth instanceof Response) {
684
+ return auth;
685
+ }
686
+ try {
687
+ const db = getDb(c);
688
+ const tableName = c.req.param('name');
689
+ const tableNames = await listTables(db);
690
+ const safeName = requireValidTableName(tableName, tableNames);
691
+ if (!safeName) {
692
+ return jsonError(c, 404, 'Table not found', 'NOT_FOUND');
693
+ }
694
+ const columnsResult = await db.prepare(`PRAGMA table_info("${safeName}")`).all();
695
+ const columns = (columnsResult?.results || []).map((row) => ({
696
+ name: row.name,
697
+ type: row.type,
698
+ nullable: row.notnull === 0,
699
+ primaryKey: row.pk === 1,
700
+ defaultValue: row.dflt_value,
701
+ }));
702
+ const primaryKey = columns.filter((col) => col.primaryKey).map((col) => col.name);
703
+ return c.json({
704
+ name: tableName,
705
+ columns,
706
+ primaryKey,
707
+ });
708
+ }
709
+ catch (error) {
710
+ console.error('Admin table schema error:', error);
711
+ return jsonError(c, 400, error instanceof Error ? error.message : 'Failed to fetch schema', 'API_ERROR');
712
+ }
713
+ });
714
+ /**
715
+ * GET /admin/database/tables/:name/data
716
+ */
717
+ app.get('/admin/database/tables/:name/data', async (c) => {
718
+ const auth = await requireAccessAdmin(c);
719
+ if (auth instanceof Response) {
720
+ return auth;
721
+ }
722
+ try {
723
+ const db = getDb(c);
724
+ const tableName = c.req.param('name');
725
+ const tableNames = await listTables(db);
726
+ const safeName = requireValidTableName(tableName, tableNames);
727
+ if (!safeName) {
728
+ return jsonError(c, 404, 'Table not found', 'NOT_FOUND');
729
+ }
730
+ const page = Math.max(parseInt(c.req.query('page') || '1', 10), 1);
731
+ const pageSize = Math.min(Math.max(parseInt(c.req.query('pageSize') || '20', 10), 1), 200);
732
+ const offset = (page - 1) * pageSize;
733
+ const columnsResult = await db.prepare(`PRAGMA table_info("${safeName}")`).all();
734
+ const columns = (columnsResult?.results || []).map((row) => ({
735
+ name: row.name,
736
+ type: row.type,
737
+ nullable: row.notnull === 0,
738
+ primaryKey: row.pk === 1,
739
+ defaultValue: row.dflt_value,
740
+ }));
741
+ const countRow = await db
742
+ .prepare(`SELECT COUNT(*) as count FROM "${safeName}"`)
743
+ .first();
744
+ const total = countRow?.count || 0;
745
+ const dataResult = await db
746
+ .prepare(`SELECT * FROM "${safeName}" LIMIT ? OFFSET ?`)
747
+ .bind(pageSize, offset)
748
+ .all();
749
+ return c.json({
750
+ tableName,
751
+ data: dataResult?.results || [],
752
+ columns,
753
+ pagination: {
754
+ page,
755
+ pageSize,
756
+ total,
757
+ totalPages: Math.ceil(total / pageSize) || 1,
758
+ },
759
+ });
760
+ }
761
+ catch (error) {
762
+ console.error('Admin table data error:', error);
763
+ return jsonError(c, 400, error instanceof Error ? error.message : 'Failed to fetch table data', 'API_ERROR');
764
+ }
765
+ });
766
+ /**
767
+ * POST /admin/database/tables/:name/rows
768
+ */
769
+ app.post('/admin/database/tables/:name/rows', async (c) => {
770
+ const auth = await requireAccessAdmin(c);
771
+ if (auth instanceof Response) {
772
+ return auth;
773
+ }
774
+ const tableName = c.req.param('name');
775
+ if (!isEntityTable(tableName)) {
776
+ return jsonError(c, 403, 'Table is not editable via admin API', 'FORBIDDEN');
777
+ }
778
+ const primaryKey = getEntityPrimaryKey(tableName);
779
+ if (!primaryKey || primaryKey !== 'id') {
780
+ return jsonError(c, 400, 'Unsupported primary key for sync', 'VALIDATION_ERROR');
781
+ }
782
+ try {
783
+ const db = getDb(c);
784
+ const tableNames = await listTables(db);
785
+ const safeName = requireValidTableName(tableName, tableNames);
786
+ if (!safeName) {
787
+ return jsonError(c, 404, 'Table not found', 'NOT_FOUND');
788
+ }
789
+ const data = (await c.req.json());
790
+ const columnsResult = await db.prepare(`PRAGMA table_info("${safeName}")`).all();
791
+ const columns = (columnsResult?.results || []).map((row) => row.name);
792
+ if (!data.id) {
793
+ data.id = `${tableName}_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
794
+ }
795
+ if (columns.includes('createdAt') && data.createdAt == null)
796
+ data.createdAt = Date.now();
797
+ if (columns.includes('updatedAt') && data.updatedAt == null)
798
+ data.updatedAt = Date.now();
799
+ if (columns.includes('created_at') && data.created_at == null)
800
+ data.created_at = Date.now();
801
+ if (columns.includes('updated_at') && data.updated_at == null)
802
+ data.updated_at = Date.now();
803
+ const insertColumns = Object.keys(data).filter((key) => columns.includes(key));
804
+ if (!insertColumns.includes('id')) {
805
+ insertColumns.unshift('id');
806
+ data.id = data.id || `${tableName}_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
807
+ }
808
+ const placeholders = insertColumns.map(() => '?').join(', ');
809
+ const values = insertColumns.map((key) => data[key]);
810
+ await db
811
+ .prepare(`INSERT INTO "${safeName}" (${insertColumns.join(', ')}) VALUES (${placeholders})`)
812
+ .bind(...values)
813
+ .run();
814
+ const version = await updateSyncMetadata(db, tableName, data.id);
815
+ const row = await db
816
+ .prepare(`SELECT * FROM "${safeName}" WHERE id = ?`)
817
+ .bind(data.id)
818
+ .first();
819
+ await notifyAdminChange(tableName, 'create', row || data, data.id, version);
820
+ if (webhookManager) {
821
+ await webhookManager.triggerEvent({
822
+ eventType: 'sync.create',
823
+ entity: tableName,
824
+ recordId: data.id,
825
+ operation: 'create',
826
+ data: row || data,
827
+ userId: auth.admin.id,
828
+ });
829
+ }
830
+ if (auditManager && (await hasUserId(db, auth.admin.id))) {
831
+ await auditManager.logChange({
832
+ id: auth.admin.id,
833
+ email: auth.admin.email,
834
+ createdAt: Date.now(),
835
+ updatedAt: Date.now(),
836
+ }, tableName, data.id, 'create', undefined, row || data);
837
+ }
838
+ return c.json(row || data, 201);
839
+ }
840
+ catch (error) {
841
+ console.error('Admin create row error:', error);
842
+ return jsonError(c, 400, error instanceof Error ? error.message : 'Failed to create row', 'API_ERROR');
843
+ }
844
+ });
845
+ /**
846
+ * PATCH /admin/database/tables/:name/rows/:id
847
+ */
848
+ app.patch('/admin/database/tables/:name/rows/:id', async (c) => {
849
+ const auth = await requireAccessAdmin(c);
850
+ if (auth instanceof Response) {
851
+ return auth;
852
+ }
853
+ const tableName = c.req.param('name');
854
+ if (!isEntityTable(tableName)) {
855
+ return jsonError(c, 403, 'Table is not editable via admin API', 'FORBIDDEN');
856
+ }
857
+ const primaryKey = getEntityPrimaryKey(tableName);
858
+ if (!primaryKey || primaryKey !== 'id') {
859
+ return jsonError(c, 400, 'Unsupported primary key for sync', 'VALIDATION_ERROR');
860
+ }
861
+ try {
862
+ const db = getDb(c);
863
+ const tableNames = await listTables(db);
864
+ const safeName = requireValidTableName(tableName, tableNames);
865
+ if (!safeName) {
866
+ return jsonError(c, 404, 'Table not found', 'NOT_FOUND');
867
+ }
868
+ const recordId = c.req.param('id');
869
+ const updates = (await c.req.json());
870
+ const existing = await db
871
+ .prepare(`SELECT * FROM "${safeName}" WHERE id = ?`)
872
+ .bind(recordId)
873
+ .first();
874
+ if (!existing) {
875
+ return jsonError(c, 404, 'Record not found', 'NOT_FOUND');
876
+ }
877
+ const columnsResult = await db.prepare(`PRAGMA table_info("${safeName}")`).all();
878
+ const columns = (columnsResult?.results || []).map((row) => row.name);
879
+ if (columns.includes('updatedAt') && updates.updatedAt == null)
880
+ updates.updatedAt = Date.now();
881
+ if (columns.includes('updated_at') && updates.updated_at == null)
882
+ updates.updated_at = Date.now();
883
+ const updateColumns = Object.keys(updates).filter((key) => key !== 'id' && columns.includes(key));
884
+ if (updateColumns.length === 0) {
885
+ return jsonError(c, 400, 'No valid fields to update', 'VALIDATION_ERROR');
886
+ }
887
+ const assignments = updateColumns.map((key) => `${key} = ?`).join(', ');
888
+ const values = updateColumns.map((key) => updates[key]);
889
+ await db
890
+ .prepare(`UPDATE "${safeName}" SET ${assignments} WHERE id = ?`)
891
+ .bind(...values, recordId)
892
+ .run();
893
+ const version = await updateSyncMetadata(db, tableName, recordId);
894
+ const row = await db
895
+ .prepare(`SELECT * FROM "${safeName}" WHERE id = ?`)
896
+ .bind(recordId)
897
+ .first();
898
+ await notifyAdminChange(tableName, 'update', row || updates, recordId, version);
899
+ if (webhookManager) {
900
+ await webhookManager.triggerEvent({
901
+ eventType: 'sync.update',
902
+ entity: tableName,
903
+ recordId,
904
+ operation: 'update',
905
+ data: row || updates,
906
+ userId: auth.admin.id,
907
+ });
908
+ }
909
+ if (auditManager && (await hasUserId(db, auth.admin.id))) {
910
+ await auditManager.logChange({
911
+ id: auth.admin.id,
912
+ email: auth.admin.email,
913
+ createdAt: Date.now(),
914
+ updatedAt: Date.now(),
915
+ }, tableName, recordId, 'update', existing, row || updates);
916
+ }
917
+ return c.json(row || updates);
918
+ }
919
+ catch (error) {
920
+ console.error('Admin update row error:', error);
921
+ return jsonError(c, 400, error instanceof Error ? error.message : 'Failed to update row', 'API_ERROR');
922
+ }
923
+ });
924
+ /**
925
+ * DELETE /admin/database/tables/:name/rows/:id
926
+ */
927
+ app.delete('/admin/database/tables/:name/rows/:id', async (c) => {
928
+ const auth = await requireAccessAdmin(c);
929
+ if (auth instanceof Response) {
930
+ return auth;
931
+ }
932
+ const tableName = c.req.param('name');
933
+ if (!isEntityTable(tableName)) {
934
+ return jsonError(c, 403, 'Table is not editable via admin API', 'FORBIDDEN');
935
+ }
936
+ const primaryKey = getEntityPrimaryKey(tableName);
937
+ if (!primaryKey || primaryKey !== 'id') {
938
+ return jsonError(c, 400, 'Unsupported primary key for sync', 'VALIDATION_ERROR');
939
+ }
940
+ try {
941
+ const db = getDb(c);
942
+ const tableNames = await listTables(db);
943
+ const safeName = requireValidTableName(tableName, tableNames);
944
+ if (!safeName) {
945
+ return jsonError(c, 404, 'Table not found', 'NOT_FOUND');
946
+ }
947
+ const recordId = c.req.param('id');
948
+ const existing = await db
949
+ .prepare(`SELECT * FROM "${safeName}" WHERE id = ?`)
950
+ .bind(recordId)
951
+ .first();
952
+ if (!existing) {
953
+ return jsonError(c, 404, 'Record not found', 'NOT_FOUND');
954
+ }
955
+ await db
956
+ .prepare(`DELETE FROM "${safeName}" WHERE id = ?`)
957
+ .bind(recordId)
958
+ .run();
959
+ const version = await updateSyncMetadata(db, tableName, recordId, Date.now());
960
+ await notifyAdminChange(tableName, 'delete', {}, recordId, version);
961
+ if (webhookManager) {
962
+ await webhookManager.triggerEvent({
963
+ eventType: 'sync.delete',
964
+ entity: tableName,
965
+ recordId,
966
+ operation: 'delete',
967
+ data: {},
968
+ userId: auth.admin.id,
969
+ });
970
+ }
971
+ if (auditManager && (await hasUserId(db, auth.admin.id))) {
972
+ await auditManager.logChange({
973
+ id: auth.admin.id,
974
+ email: auth.admin.email,
975
+ createdAt: Date.now(),
976
+ updatedAt: Date.now(),
977
+ }, tableName, recordId, 'delete', existing, undefined);
978
+ }
979
+ return c.json({ success: true });
980
+ }
981
+ catch (error) {
982
+ console.error('Admin delete row error:', error);
983
+ return jsonError(c, 400, error instanceof Error ? error.message : 'Failed to delete row', 'API_ERROR');
984
+ }
985
+ });
986
+ /**
987
+ * GET /admin/schemas
988
+ */
989
+ app.get('/admin/schemas', async (c) => {
990
+ const auth = await requireAccessAdmin(c);
991
+ if (auth instanceof Response) {
992
+ return auth;
993
+ }
994
+ return c.json({
995
+ version: 1,
996
+ timestamp: Date.now(),
997
+ schemas: schema.entities,
998
+ });
999
+ });
1000
+ /**
1001
+ * GET /admin/activity
1002
+ */
1003
+ app.get('/admin/activity', async (c) => {
1004
+ const auth = await requireAccessAdmin(c);
1005
+ if (auth instanceof Response) {
1006
+ return auth;
1007
+ }
1008
+ try {
1009
+ const db = getDb(c);
1010
+ const page = Math.max(parseInt(c.req.query('page') || '1', 10), 1);
1011
+ const pageSize = Math.min(Math.max(parseInt(c.req.query('pageSize') || '20', 10), 1), 100);
1012
+ const offset = (page - 1) * pageSize;
1013
+ const conditions = [];
1014
+ const params = [];
1015
+ const adminId = c.req.query('adminId');
1016
+ if (adminId) {
1017
+ conditions.push('user_id = ?');
1018
+ params.push(adminId);
1019
+ }
1020
+ const entityType = c.req.query('entityType');
1021
+ if (entityType) {
1022
+ conditions.push('entity = ?');
1023
+ params.push(entityType);
1024
+ }
1025
+ const action = c.req.query('action');
1026
+ if (action) {
1027
+ conditions.push('operation = ?');
1028
+ params.push(action);
1029
+ }
1030
+ const dateFrom = c.req.query('dateFrom');
1031
+ if (dateFrom) {
1032
+ conditions.push('created_at >= ?');
1033
+ params.push(Number(dateFrom));
1034
+ }
1035
+ const dateTo = c.req.query('dateTo');
1036
+ if (dateTo) {
1037
+ conditions.push('created_at <= ?');
1038
+ params.push(Number(dateTo));
1039
+ }
1040
+ const whereClause = conditions.length ? `WHERE ${conditions.join(' AND ')}` : '';
1041
+ const countRow = await db
1042
+ .prepare(`SELECT COUNT(*) as total FROM audit_logs ${whereClause}`)
1043
+ .bind(...params)
1044
+ .first();
1045
+ const total = countRow?.total || 0;
1046
+ const rows = await db
1047
+ .prepare(`SELECT * FROM audit_logs ${whereClause} ORDER BY created_at DESC LIMIT ? OFFSET ?`)
1048
+ .bind(...params, pageSize, offset)
1049
+ .all();
1050
+ const entries = [];
1051
+ for (const row of rows?.results || []) {
1052
+ const entity = row.entity;
1053
+ const entityTypeValue = entity === 'admins' ? 'admin' : entity === 'users' ? 'user' : 'database';
1054
+ entries.push({
1055
+ id: row.id,
1056
+ adminId: row.user_id,
1057
+ adminEmail: undefined,
1058
+ action: row.operation,
1059
+ entityType: entityTypeValue,
1060
+ entityId: row.record_id,
1061
+ details: row.changes ? JSON.parse(row.changes) : {},
1062
+ ipAddress: '',
1063
+ createdAt: row.created_at,
1064
+ });
1065
+ }
1066
+ return c.json({
1067
+ data: entries,
1068
+ pagination: {
1069
+ page,
1070
+ pageSize,
1071
+ total,
1072
+ totalPages: Math.ceil(total / pageSize) || 1,
1073
+ },
1074
+ });
1075
+ }
1076
+ catch (error) {
1077
+ console.error('Admin activity error:', error);
1078
+ return jsonError(c, 400, error instanceof Error ? error.message : 'Failed to fetch activity log', 'API_ERROR');
1079
+ }
1080
+ });
1081
+ /**
1082
+ * POST /auth/refresh
1083
+ */
1084
+ app.post('/auth/refresh', async (c) => {
1085
+ try {
1086
+ const body = await c.req.json();
1087
+ const refreshToken = body?.refreshToken;
1088
+ if (!refreshToken) {
1089
+ return jsonError(c, 400, 'Refresh token is required', 'VALIDATION_ERROR');
1090
+ }
1091
+ const payload = parseJWT(refreshToken);
1092
+ if (!payload || payload.type !== 'refresh') {
1093
+ return jsonError(c, 401, 'Invalid refresh token', 'UNAUTHORIZED');
1094
+ }
1095
+ const db = getDb(c);
1096
+ const jwtSecret = getJwtSecret(c);
1097
+ const tokenRow = await db
1098
+ .prepare(`SELECT r.user_id, r.expires_at, u.email
1099
+ FROM refresh_tokens r
1100
+ JOIN users u ON u.id = r.user_id
1101
+ WHERE r.token = ?`)
1102
+ .bind(refreshToken)
1103
+ .first();
1104
+ if (!tokenRow) {
1105
+ return jsonError(c, 401, 'Invalid refresh token', 'UNAUTHORIZED');
1106
+ }
1107
+ if (tokenRow.expires_at < Date.now()) {
1108
+ await db.prepare('DELETE FROM refresh_tokens WHERE token = ?').bind(refreshToken).run();
1109
+ return jsonError(c, 401, 'Refresh token expired', 'UNAUTHORIZED');
1110
+ }
1111
+ const accessToken = createJWT({ userId: tokenRow.user_id, email: tokenRow.email, type: 'access' }, jwtSecret, 3600);
1112
+ return c.json({ accessToken, expiresIn: 3600 });
1113
+ }
1114
+ catch (error) {
1115
+ console.error('Refresh token error:', error);
1116
+ return jsonError(c, 400, error instanceof Error ? error.message : 'Token refresh failed', 'API_ERROR');
1117
+ }
1118
+ });
1119
+ /**
1120
+ * POST /auth/logout
1121
+ */
1122
+ app.post('/auth/logout', async (c) => {
1123
+ const auth = await requireAccessUser(c);
1124
+ if (auth instanceof Response) {
1125
+ return auth;
1126
+ }
1127
+ try {
1128
+ const db = getDb(c);
1129
+ await db.prepare('DELETE FROM refresh_tokens WHERE user_id = ?').bind(auth.user.id).run();
1130
+ return c.json({ message: 'Logged out successfully', userId: auth.user.id });
1131
+ }
1132
+ catch (error) {
1133
+ console.error('Logout error:', error);
1134
+ return jsonError(c, 500, 'Logout failed', 'API_ERROR');
1135
+ }
1136
+ });
1137
+ /**
1138
+ * POST /sync
1139
+ */
1140
+ app.post('/sync', async (c) => {
1141
+ try {
1142
+ const auth = await requireAccessUser(c);
1143
+ if (auth instanceof Response) {
1144
+ return auth;
1145
+ }
1146
+ // Create sync database adapter
1147
+ const db = new D1SyncDatabase(getDb(c));
1148
+ // Create schemas map for ServerSyncEngine
1149
+ const schemas = new Map();
1150
+ for (const [entityName, entitySchema] of Object.entries(schema.entities)) {
1151
+ schemas.set(entityName, entitySchema);
1152
+ }
1153
+ // Create sync engine
1154
+ const syncEngine = new ServerSyncEngine({
1155
+ schemas,
1156
+ db,
1157
+ user: auth.user,
1158
+ encryption: encryptionManager || undefined,
1159
+ });
1160
+ // Process sync request
1161
+ const request = await c.req.json();
1162
+ const response = await syncEngine.sync(request);
1163
+ // Broadcast changes to subscribed clients in real-time
1164
+ if (changeNotifier && subscriptionManager) {
1165
+ for (const change of response.changes) {
1166
+ // For each applied change, notify subscribed clients
1167
+ if (change.operation !== 'delete') {
1168
+ // For create/update, we have the full record
1169
+ await changeNotifier.notifyChange(change.entity, change.operation, change.data || {}, change.id, auth.user, change.version);
1170
+ }
1171
+ else {
1172
+ // For delete, notify with just the ID
1173
+ await changeNotifier.notifyChange(change.entity, 'delete', {}, change.id, auth.user, change.version);
1174
+ }
1175
+ }
1176
+ }
1177
+ // Trigger webhooks for applied changes
1178
+ if (webhookManager) {
1179
+ for (const change of response.changes) {
1180
+ await webhookManager.triggerEvent({
1181
+ eventType: `sync.${change.operation}`,
1182
+ entity: change.entity,
1183
+ recordId: change.id,
1184
+ operation: change.operation,
1185
+ data: change.data,
1186
+ userId: auth.user.id,
1187
+ });
1188
+ }
1189
+ }
1190
+ // Log changes for audit trail
1191
+ if (auditManager) {
1192
+ for (const change of response.changes) {
1193
+ const before = change.operation === 'update' || change.operation === 'delete' ? change.data : undefined;
1194
+ const after = change.operation === 'create' || change.operation === 'update' ? change.data : undefined;
1195
+ await auditManager.logChange(auth.user, change.entity, change.id, change.operation, before, after);
1196
+ }
1197
+ }
1198
+ return c.json(response);
1199
+ }
1200
+ catch (error) {
1201
+ console.error('Sync error:', error);
1202
+ return jsonError(c, 400, error instanceof Error ? error.message : 'Sync failed', 'API_ERROR');
1203
+ }
1204
+ });
1205
+ /**
1206
+ * GET /sync/:entity
1207
+ */
1208
+ app.get('/sync/:entity', async (c) => {
1209
+ const auth = await requireAccessUser(c);
1210
+ if (auth instanceof Response) {
1211
+ return auth;
1212
+ }
1213
+ const entity = c.req.param('entity');
1214
+ if (!schema.entities[entity]) {
1215
+ return jsonError(c, 404, `Entity not found: ${entity}`, 'NOT_FOUND');
1216
+ }
1217
+ try {
1218
+ const db = getDb(c);
1219
+ const result = await db
1220
+ .prepare('SELECT entity, record_id, version, updated_at, deleted_at FROM sync_metadata WHERE entity = ?')
1221
+ .bind(entity)
1222
+ .all();
1223
+ return c.json({
1224
+ entity,
1225
+ metadata: result.results || [],
1226
+ timestamp: Date.now(),
1227
+ });
1228
+ }
1229
+ catch (error) {
1230
+ console.error('Sync metadata error:', error);
1231
+ return jsonError(c, 500, 'Failed to load sync metadata', 'API_ERROR');
1232
+ }
1233
+ });
1234
+ /**
1235
+ * POST /sync/batch
1236
+ * Batch process multiple CRUD operations
1237
+ */
1238
+ app.post('/sync/batch', async (c) => {
1239
+ try {
1240
+ const auth = await requireAccessUser(c);
1241
+ if (auth instanceof Response) {
1242
+ return auth;
1243
+ }
1244
+ // Create sync database adapter
1245
+ const db = new D1SyncDatabase(getDb(c));
1246
+ // Create schemas map for BatchProcessor
1247
+ const schemas = new Map();
1248
+ for (const [entityName, entitySchema] of Object.entries(schema.entities)) {
1249
+ schemas.set(entityName, entitySchema);
1250
+ }
1251
+ // Create batch processor
1252
+ const batchProcessor = new BatchProcessor({
1253
+ schemas,
1254
+ db,
1255
+ user: auth.user,
1256
+ maxBatchSize: 1000,
1257
+ encryption: encryptionManager || undefined,
1258
+ });
1259
+ // Process batch request
1260
+ const request = await c.req.json();
1261
+ const response = await batchProcessor.processBatch(request);
1262
+ // Broadcast changes to subscribed clients in real-time
1263
+ if (changeNotifier && subscriptionManager) {
1264
+ for (const change of response.applied) {
1265
+ // For each applied change, notify subscribed clients
1266
+ if (change.operation !== 'delete') {
1267
+ // For create/update, we have the full record
1268
+ await changeNotifier.notifyChange(change.entity, change.operation, change.data || {}, change.id, auth.user, change.version);
1269
+ }
1270
+ else {
1271
+ // For delete, notify with just the ID
1272
+ await changeNotifier.notifyChange(change.entity, 'delete', {}, change.id, auth.user, change.version);
1273
+ }
1274
+ }
1275
+ }
1276
+ // Trigger webhooks for applied changes
1277
+ if (webhookManager) {
1278
+ for (const change of response.applied) {
1279
+ await webhookManager.triggerEvent({
1280
+ eventType: `sync.${change.operation}`,
1281
+ entity: change.entity,
1282
+ recordId: change.id,
1283
+ operation: change.operation,
1284
+ data: change.data,
1285
+ userId: auth.user.id,
1286
+ });
1287
+ }
1288
+ }
1289
+ // Log changes for audit trail
1290
+ if (auditManager) {
1291
+ for (const change of response.applied) {
1292
+ const before = change.operation === 'update' || change.operation === 'delete' ? change.data : undefined;
1293
+ const after = change.operation === 'create' || change.operation === 'update' ? change.data : undefined;
1294
+ await auditManager.logChange(auth.user, change.entity, change.id, change.operation, before, after);
1295
+ }
1296
+ }
1297
+ return c.json(response);
1298
+ }
1299
+ catch (error) {
1300
+ console.error('Batch error:', error);
1301
+ return jsonError(c, 400, error instanceof Error ? error.message : 'Batch processing failed', 'API_ERROR');
1302
+ }
1303
+ });
1304
+ /**
1305
+ * POST /import/csv
1306
+ * Import data from CSV file
1307
+ */
1308
+ app.post('/import/csv', async (c) => {
1309
+ try {
1310
+ const auth = await requireAccessUser(c);
1311
+ if (auth instanceof Response) {
1312
+ return auth;
1313
+ }
1314
+ const formData = await c.req.formData();
1315
+ const entity = formData.get('entity');
1316
+ const file = formData.get('file');
1317
+ if (!entity) {
1318
+ return jsonError(c, 400, 'Missing entity parameter', 'VALIDATION_ERROR');
1319
+ }
1320
+ if (!schema.entities[entity]) {
1321
+ return jsonError(c, 404, `Entity not found: ${entity}`, 'NOT_FOUND');
1322
+ }
1323
+ if (!file || !(file instanceof Blob)) {
1324
+ return jsonError(c, 400, 'Missing or invalid file', 'VALIDATION_ERROR');
1325
+ }
1326
+ // Read CSV file
1327
+ const csvContent = await file.text();
1328
+ // Parse CSV
1329
+ const importResult = await CSVProcessor.importFromCSV({
1330
+ entity,
1331
+ data: csvContent,
1332
+ hasHeader: true,
1333
+ });
1334
+ return c.json(importResult);
1335
+ }
1336
+ catch (error) {
1337
+ console.error('CSV import error:', error);
1338
+ return jsonError(c, 400, error instanceof Error ? error.message : 'CSV import failed', 'API_ERROR');
1339
+ }
1340
+ });
1341
+ /**
1342
+ * POST /export/csv
1343
+ * Export data to CSV format
1344
+ */
1345
+ app.post('/export/csv', async (c) => {
1346
+ try {
1347
+ const auth = await requireAccessUser(c);
1348
+ if (auth instanceof Response) {
1349
+ return auth;
1350
+ }
1351
+ const { entity, filter, columns } = await c.req.json();
1352
+ if (!entity) {
1353
+ return jsonError(c, 400, 'Missing entity parameter', 'VALIDATION_ERROR');
1354
+ }
1355
+ if (!schema.entities[entity]) {
1356
+ return jsonError(c, 404, `Entity not found: ${entity}`, 'NOT_FOUND');
1357
+ }
1358
+ try {
1359
+ const db = getDb(c);
1360
+ let query = `SELECT * FROM ${entity}`;
1361
+ // Apply filter if provided
1362
+ if (filter && typeof filter === 'object') {
1363
+ const whereConditions = [];
1364
+ const params = [];
1365
+ for (const [key, value] of Object.entries(filter)) {
1366
+ whereConditions.push(`${key} = ?`);
1367
+ params.push(value);
1368
+ }
1369
+ if (whereConditions.length > 0) {
1370
+ query += ` WHERE ${whereConditions.join(' AND ')}`;
1371
+ }
1372
+ // Execute with parameters
1373
+ const result = await db.prepare(query).bind(...params).all();
1374
+ const data = result.results || [];
1375
+ // Export to CSV
1376
+ const exportResult = CSVProcessor.exportToCSV({
1377
+ entity,
1378
+ data,
1379
+ columns,
1380
+ includeMetadata: false,
1381
+ });
1382
+ // Return as downloadable CSV
1383
+ return new Response(exportResult.csv, {
1384
+ headers: {
1385
+ 'Content-Type': 'text/csv',
1386
+ 'Content-Disposition': `attachment; filename="${entity}.csv"`,
1387
+ },
1388
+ });
1389
+ }
1390
+ else {
1391
+ // No filter, get all records
1392
+ const result = await db.prepare(query).all();
1393
+ const data = result.results || [];
1394
+ // Export to CSV
1395
+ const exportResult = CSVProcessor.exportToCSV({
1396
+ entity,
1397
+ data,
1398
+ columns,
1399
+ includeMetadata: false,
1400
+ });
1401
+ // Return as downloadable CSV
1402
+ return new Response(exportResult.csv, {
1403
+ headers: {
1404
+ 'Content-Type': 'text/csv',
1405
+ 'Content-Disposition': `attachment; filename="${entity}.csv"`,
1406
+ },
1407
+ });
1408
+ }
1409
+ }
1410
+ catch (error) {
1411
+ return jsonError(c, 400, error instanceof Error ? error.message : 'Export failed', 'API_ERROR');
1412
+ }
1413
+ }
1414
+ catch (error) {
1415
+ console.error('CSV export error:', error);
1416
+ return jsonError(c, 400, error instanceof Error ? error.message : 'CSV export failed', 'API_ERROR');
1417
+ }
1418
+ });
1419
+ /**
1420
+ * GET /column-permissions/:entity
1421
+ * Get column permissions for an entity
1422
+ */
1423
+ app.get('/column-permissions/:entity', async (c) => {
1424
+ try {
1425
+ const auth = await requireAccessUser(c);
1426
+ if (auth instanceof Response) {
1427
+ return auth;
1428
+ }
1429
+ const entity = c.req.param('entity');
1430
+ if (!schema.entities[entity]) {
1431
+ return jsonError(c, 404, `Entity not found: ${entity}`, 'NOT_FOUND');
1432
+ }
1433
+ const db = getDb(c);
1434
+ const result = await db
1435
+ .prepare('SELECT * FROM column_permissions WHERE entity = ? ORDER BY column_name')
1436
+ .bind(entity)
1437
+ .all();
1438
+ return c.json({
1439
+ entity,
1440
+ permissions: result.results || [],
1441
+ });
1442
+ }
1443
+ catch (error) {
1444
+ console.error('Column permissions fetch error:', error);
1445
+ return jsonError(c, 500, 'Failed to fetch column permissions', 'API_ERROR');
1446
+ }
1447
+ });
1448
+ /**
1449
+ * POST /column-permissions
1450
+ * Create or update column permission
1451
+ */
1452
+ app.post('/column-permissions', async (c) => {
1453
+ try {
1454
+ const auth = await requireAccessUser(c);
1455
+ if (auth instanceof Response) {
1456
+ return auth;
1457
+ }
1458
+ const { entity, columnName, role, visible, readable, writable, encrypted, maskValue } = await c.req.json();
1459
+ if (!entity || !columnName) {
1460
+ return jsonError(c, 400, 'Missing entity or columnName', 'VALIDATION_ERROR');
1461
+ }
1462
+ if (!schema.entities[entity]) {
1463
+ return jsonError(c, 404, `Entity not found: ${entity}`, 'NOT_FOUND');
1464
+ }
1465
+ const db = getDb(c);
1466
+ const now = Date.now();
1467
+ const permissionId = `${entity}_${columnName}_${role || 'default'}_${now}`;
1468
+ // Check if permission already exists
1469
+ const existing = await db
1470
+ .prepare('SELECT id FROM column_permissions WHERE entity = ? AND column_name = ? AND role IS ?')
1471
+ .bind(entity, columnName, role || null)
1472
+ .first();
1473
+ if (existing) {
1474
+ // Update existing permission
1475
+ await db
1476
+ .prepare(`
1477
+ UPDATE column_permissions
1478
+ SET visible = ?, readable = ?, writable = ?, encrypted = ?, mask_value = ?, updated_at = ?
1479
+ WHERE entity = ? AND column_name = ? AND role IS ?
1480
+ `)
1481
+ .bind(visible ? 1 : 0, readable ? 1 : 0, writable ? 1 : 0, encrypted ? 1 : 0, maskValue || null, now, entity, columnName, role || null)
1482
+ .run();
1483
+ }
1484
+ else {
1485
+ // Create new permission
1486
+ await db
1487
+ .prepare(`
1488
+ INSERT INTO column_permissions (id, entity, column_name, role, visible, readable, writable, encrypted, mask_value, created_at, updated_at)
1489
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1490
+ `)
1491
+ .bind(permissionId, entity, columnName, role || null, visible ? 1 : 0, readable ? 1 : 0, writable ? 1 : 0, encrypted ? 1 : 0, maskValue || null, now, now)
1492
+ .run();
1493
+ }
1494
+ return c.json({ success: true, id: existing?.id || permissionId });
1495
+ }
1496
+ catch (error) {
1497
+ console.error('Column permission update error:', error);
1498
+ return jsonError(c, 500, 'Failed to update column permission', 'API_ERROR');
1499
+ }
1500
+ });
1501
+ /**
1502
+ * DELETE /column-permissions/:id
1503
+ * Delete a column permission
1504
+ */
1505
+ app.delete('/column-permissions/:id', async (c) => {
1506
+ try {
1507
+ const auth = await requireAccessUser(c);
1508
+ if (auth instanceof Response) {
1509
+ return auth;
1510
+ }
1511
+ const id = c.req.param('id');
1512
+ const db = getDb(c);
1513
+ await db
1514
+ .prepare('DELETE FROM column_permissions WHERE id = ?')
1515
+ .bind(id)
1516
+ .run();
1517
+ return c.json({ success: true });
1518
+ }
1519
+ catch (error) {
1520
+ console.error('Column permission delete error:', error);
1521
+ return jsonError(c, 500, 'Failed to delete column permission', 'API_ERROR');
1522
+ }
1523
+ });
1524
+ /**
1525
+ * POST /files/upload
1526
+ * Upload a file to R2 storage
1527
+ */
1528
+ app.post('/files/upload', async (c) => {
1529
+ try {
1530
+ const auth = await requireAccessUser(c);
1531
+ if (auth instanceof Response) {
1532
+ return auth;
1533
+ }
1534
+ const getR2Bucket = options.getR2Bucket;
1535
+ if (!getR2Bucket) {
1536
+ return jsonError(c, 501, 'File storage not configured', 'NOT_CONFIGURED');
1537
+ }
1538
+ const formData = await c.req.formData();
1539
+ const file = formData.get('file');
1540
+ const entityType = formData.get('entityType');
1541
+ const entityId = formData.get('entityId');
1542
+ const isPublic = formData.get('isPublic') === 'true';
1543
+ if (!file || !(file instanceof Blob)) {
1544
+ return jsonError(c, 400, 'Missing or invalid file', 'VALIDATION_ERROR');
1545
+ }
1546
+ const bucket = getR2Bucket(c);
1547
+ const db = new D1SyncDatabase(getDb(c));
1548
+ const fileManager = new FileStorageManager(bucket, db, {
1549
+ maxFileSize: 100 * 1024 * 1024, // 100MB
1550
+ });
1551
+ const arrayBuffer = await file.arrayBuffer();
1552
+ const metadata = await fileManager.uploadFile(auth.user, arrayBuffer, {
1553
+ fileName: file.name || 'unnamed',
1554
+ mimeType: file.type || 'application/octet-stream',
1555
+ size: file.size,
1556
+ entityType: entityType || undefined,
1557
+ entityId: entityId || undefined,
1558
+ isPublic,
1559
+ });
1560
+ return c.json(metadata);
1561
+ }
1562
+ catch (error) {
1563
+ console.error('File upload error:', error);
1564
+ return jsonError(c, 400, error instanceof Error ? error.message : 'Upload failed', 'API_ERROR');
1565
+ }
1566
+ });
1567
+ /**
1568
+ * GET /files/download/:fileId
1569
+ * Download a file from R2 storage
1570
+ */
1571
+ app.get('/files/download/:fileId', async (c) => {
1572
+ try {
1573
+ const fileId = c.req.param('fileId');
1574
+ const token = c.req.query('token');
1575
+ const getR2Bucket = options.getR2Bucket;
1576
+ if (!getR2Bucket) {
1577
+ return jsonError(c, 501, 'File storage not configured', 'NOT_CONFIGURED');
1578
+ }
1579
+ const bucket = getR2Bucket(c);
1580
+ const db = new D1SyncDatabase(getDb(c));
1581
+ const fileManager = new FileStorageManager(bucket, db);
1582
+ // If token is provided, verify it
1583
+ if (token) {
1584
+ const isValid = await fileManager.verifyToken(fileId, token);
1585
+ if (!isValid) {
1586
+ return jsonError(c, 401, 'Invalid or expired token', 'UNAUTHORIZED');
1587
+ }
1588
+ // Get file and return
1589
+ const metadata = await fileManager.getFileMetadata(fileId);
1590
+ if (!metadata) {
1591
+ return jsonError(c, 404, 'File not found', 'NOT_FOUND');
1592
+ }
1593
+ const file = await bucket.get(metadata.key);
1594
+ if (!file) {
1595
+ return jsonError(c, 404, 'File not found in storage', 'NOT_FOUND');
1596
+ }
1597
+ return new Response(file.body, {
1598
+ headers: {
1599
+ 'Content-Type': metadata.mimeType,
1600
+ 'Content-Disposition': `attachment; filename="${metadata.fileName}"`,
1601
+ 'Content-Length': metadata.size.toString(),
1602
+ },
1603
+ });
1604
+ }
1605
+ // Otherwise, require authentication
1606
+ const auth = await requireAccessUser(c);
1607
+ if (auth instanceof Response) {
1608
+ return auth;
1609
+ }
1610
+ const file = await fileManager.downloadFile(fileId, auth.user);
1611
+ if (!file) {
1612
+ return jsonError(c, 404, 'File not found', 'NOT_FOUND');
1613
+ }
1614
+ const metadata = await fileManager.getFileMetadata(fileId);
1615
+ if (!metadata) {
1616
+ return jsonError(c, 404, 'File metadata not found', 'NOT_FOUND');
1617
+ }
1618
+ return new Response(file.body, {
1619
+ headers: {
1620
+ 'Content-Type': metadata.mimeType,
1621
+ 'Content-Disposition': `attachment; filename="${metadata.fileName}"`,
1622
+ 'Content-Length': metadata.size.toString(),
1623
+ },
1624
+ });
1625
+ }
1626
+ catch (error) {
1627
+ console.error('File download error:', error);
1628
+ return jsonError(c, 400, error instanceof Error ? error.message : 'Download failed', 'API_ERROR');
1629
+ }
1630
+ });
1631
+ /**
1632
+ * DELETE /files/:fileId
1633
+ * Delete a file from R2 storage
1634
+ */
1635
+ app.delete('/files/:fileId', async (c) => {
1636
+ try {
1637
+ const auth = await requireAccessUser(c);
1638
+ if (auth instanceof Response) {
1639
+ return auth;
1640
+ }
1641
+ const getR2Bucket = options.getR2Bucket;
1642
+ if (!getR2Bucket) {
1643
+ return jsonError(c, 501, 'File storage not configured', 'NOT_CONFIGURED');
1644
+ }
1645
+ const fileId = c.req.param('fileId');
1646
+ const bucket = getR2Bucket(c);
1647
+ const db = new D1SyncDatabase(getDb(c));
1648
+ const fileManager = new FileStorageManager(bucket, db);
1649
+ await fileManager.deleteFile(fileId, auth.user);
1650
+ return c.json({ success: true });
1651
+ }
1652
+ catch (error) {
1653
+ console.error('File delete error:', error);
1654
+ return jsonError(c, 400, error instanceof Error ? error.message : 'Delete failed', 'API_ERROR');
1655
+ }
1656
+ });
1657
+ /**
1658
+ * GET /files
1659
+ * List files for the authenticated user
1660
+ */
1661
+ app.get('/files', async (c) => {
1662
+ try {
1663
+ const auth = await requireAccessUser(c);
1664
+ if (auth instanceof Response) {
1665
+ return auth;
1666
+ }
1667
+ const getR2Bucket = options.getR2Bucket;
1668
+ if (!getR2Bucket) {
1669
+ return jsonError(c, 501, 'File storage not configured', 'NOT_CONFIGURED');
1670
+ }
1671
+ const entityType = c.req.query('entityType');
1672
+ const entityId = c.req.query('entityId');
1673
+ const limit = c.req.query('limit') ? parseInt(c.req.query('limit')) : undefined;
1674
+ const offset = c.req.query('offset') ? parseInt(c.req.query('offset')) : undefined;
1675
+ const bucket = getR2Bucket(c);
1676
+ const db = new D1SyncDatabase(getDb(c));
1677
+ const fileManager = new FileStorageManager(bucket, db);
1678
+ const files = await fileManager.listFiles(auth.user, {
1679
+ entityType: entityType || undefined,
1680
+ entityId: entityId || undefined,
1681
+ limit,
1682
+ offset,
1683
+ });
1684
+ return c.json({ files });
1685
+ }
1686
+ catch (error) {
1687
+ console.error('File list error:', error);
1688
+ return jsonError(c, 500, 'Failed to list files', 'API_ERROR');
1689
+ }
1690
+ });
1691
+ /**
1692
+ * POST /files/:fileId/signed-url
1693
+ * Generate a signed URL for temporary access to a file
1694
+ */
1695
+ app.post('/files/:fileId/signed-url', async (c) => {
1696
+ try {
1697
+ const auth = await requireAccessUser(c);
1698
+ if (auth instanceof Response) {
1699
+ return auth;
1700
+ }
1701
+ const getR2Bucket = options.getR2Bucket;
1702
+ if (!getR2Bucket) {
1703
+ return jsonError(c, 501, 'File storage not configured', 'NOT_CONFIGURED');
1704
+ }
1705
+ const fileId = c.req.param('fileId');
1706
+ const { expiresIn } = await c.req.json();
1707
+ const bucket = getR2Bucket(c);
1708
+ const db = new D1SyncDatabase(getDb(c));
1709
+ const fileManager = new FileStorageManager(bucket, db);
1710
+ const url = await fileManager.generateSignedUrl(fileId, auth.user, expiresIn || 3600);
1711
+ return c.json({ url, expiresIn: expiresIn || 3600 });
1712
+ }
1713
+ catch (error) {
1714
+ console.error('Signed URL generation error:', error);
1715
+ return jsonError(c, 400, error instanceof Error ? error.message : 'Failed to generate signed URL', 'API_ERROR');
1716
+ }
1717
+ });
1718
+ /**
1719
+ * POST /webhooks
1720
+ * Register a new webhook
1721
+ */
1722
+ app.post('/webhooks', async (c) => {
1723
+ try {
1724
+ const auth = await requireAccessUser(c);
1725
+ if (auth instanceof Response) {
1726
+ return auth;
1727
+ }
1728
+ if (!webhookManager) {
1729
+ return jsonError(c, 501, 'Webhooks not configured', 'NOT_CONFIGURED');
1730
+ }
1731
+ const { url, events, description, headers } = await c.req.json();
1732
+ const webhook = await webhookManager.registerWebhook(auth.user, {
1733
+ url,
1734
+ events,
1735
+ description,
1736
+ headers,
1737
+ });
1738
+ // Don't expose the secret in the response for security
1739
+ return c.json({ ...webhook, secret: undefined });
1740
+ }
1741
+ catch (error) {
1742
+ console.error('Webhook registration error:', error);
1743
+ return jsonError(c, 400, error instanceof Error ? error.message : 'Failed to register webhook', 'API_ERROR');
1744
+ }
1745
+ });
1746
+ /**
1747
+ * GET /webhooks
1748
+ * List user's webhooks
1749
+ */
1750
+ app.get('/webhooks', async (c) => {
1751
+ try {
1752
+ const auth = await requireAccessUser(c);
1753
+ if (auth instanceof Response) {
1754
+ return auth;
1755
+ }
1756
+ if (!webhookManager) {
1757
+ return jsonError(c, 501, 'Webhooks not configured', 'NOT_CONFIGURED');
1758
+ }
1759
+ const webhooks = await webhookManager.listWebhooks(auth.user.id);
1760
+ // Don't expose secrets
1761
+ const safeWebhooks = webhooks.map((wh) => ({ ...wh, secret: undefined }));
1762
+ return c.json({ webhooks: safeWebhooks });
1763
+ }
1764
+ catch (error) {
1765
+ console.error('Webhook list error:', error);
1766
+ return jsonError(c, 500, 'Failed to list webhooks', 'API_ERROR');
1767
+ }
1768
+ });
1769
+ /**
1770
+ * GET /webhooks/:webhookId
1771
+ * Get a specific webhook
1772
+ */
1773
+ app.get('/webhooks/:webhookId', async (c) => {
1774
+ try {
1775
+ const auth = await requireAccessUser(c);
1776
+ if (auth instanceof Response) {
1777
+ return auth;
1778
+ }
1779
+ if (!webhookManager) {
1780
+ return jsonError(c, 501, 'Webhooks not configured', 'NOT_CONFIGURED');
1781
+ }
1782
+ const webhookId = c.req.param('webhookId');
1783
+ const webhook = await webhookManager.getWebhook(webhookId, auth.user.id);
1784
+ if (!webhook) {
1785
+ return jsonError(c, 404, 'Webhook not found', 'NOT_FOUND');
1786
+ }
1787
+ return c.json({ ...webhook, secret: undefined });
1788
+ }
1789
+ catch (error) {
1790
+ console.error('Webhook fetch error:', error);
1791
+ return jsonError(c, 500, 'Failed to fetch webhook', 'API_ERROR');
1792
+ }
1793
+ });
1794
+ /**
1795
+ * PATCH /webhooks/:webhookId
1796
+ * Update a webhook
1797
+ */
1798
+ app.patch('/webhooks/:webhookId', async (c) => {
1799
+ try {
1800
+ const auth = await requireAccessUser(c);
1801
+ if (auth instanceof Response) {
1802
+ return auth;
1803
+ }
1804
+ if (!webhookManager) {
1805
+ return jsonError(c, 501, 'Webhooks not configured', 'NOT_CONFIGURED');
1806
+ }
1807
+ const webhookId = c.req.param('webhookId');
1808
+ const updates = await c.req.json();
1809
+ const webhook = await webhookManager.updateWebhook(webhookId, auth.user.id, updates);
1810
+ return c.json({ ...webhook, secret: undefined });
1811
+ }
1812
+ catch (error) {
1813
+ console.error('Webhook update error:', error);
1814
+ return jsonError(c, 400, error instanceof Error ? error.message : 'Failed to update webhook', 'API_ERROR');
1815
+ }
1816
+ });
1817
+ /**
1818
+ * DELETE /webhooks/:webhookId
1819
+ * Delete a webhook
1820
+ */
1821
+ app.delete('/webhooks/:webhookId', async (c) => {
1822
+ try {
1823
+ const auth = await requireAccessUser(c);
1824
+ if (auth instanceof Response) {
1825
+ return auth;
1826
+ }
1827
+ if (!webhookManager) {
1828
+ return jsonError(c, 501, 'Webhooks not configured', 'NOT_CONFIGURED');
1829
+ }
1830
+ const webhookId = c.req.param('webhookId');
1831
+ await webhookManager.deleteWebhook(webhookId, auth.user.id);
1832
+ return c.json({ success: true });
1833
+ }
1834
+ catch (error) {
1835
+ console.error('Webhook delete error:', error);
1836
+ return jsonError(c, 500, 'Failed to delete webhook', 'API_ERROR');
1837
+ }
1838
+ });
1839
+ /**
1840
+ * POST /search/:entity
1841
+ * Full-text search for an entity
1842
+ */
1843
+ app.post('/search/:entity', async (c) => {
1844
+ try {
1845
+ const auth = await requireAccessUser(c);
1846
+ if (auth instanceof Response) {
1847
+ return auth;
1848
+ }
1849
+ if (!searchManager) {
1850
+ return jsonError(c, 501, 'Search not configured', 'NOT_CONFIGURED');
1851
+ }
1852
+ const entity = c.req.param('entity');
1853
+ if (!schema.entities[entity]) {
1854
+ return jsonError(c, 404, `Entity not found: ${entity}`, 'NOT_FOUND');
1855
+ }
1856
+ if (!searchManager.hasIndex(entity)) {
1857
+ return jsonError(c, 404, `No search index for entity: ${entity}`, 'NOT_FOUND');
1858
+ }
1859
+ const { query, columns, limit, offset, highlight, rank } = await c.req.json();
1860
+ if (!query || typeof query !== 'string') {
1861
+ return jsonError(c, 400, 'Missing or invalid query', 'VALIDATION_ERROR');
1862
+ }
1863
+ const results = await searchManager.search({
1864
+ entity,
1865
+ query,
1866
+ columns,
1867
+ limit,
1868
+ offset,
1869
+ highlight: highlight !== false, // Default true
1870
+ rank: rank !== false, // Default true
1871
+ });
1872
+ return c.json(results);
1873
+ }
1874
+ catch (error) {
1875
+ console.error('Search error:', error);
1876
+ return jsonError(c, 400, error instanceof Error ? error.message : 'Search failed', 'API_ERROR');
1877
+ }
1878
+ });
1879
+ /**
1880
+ * POST /search/:entity/index
1881
+ * Create or rebuild search index for an entity
1882
+ */
1883
+ app.post('/search/:entity/index', async (c) => {
1884
+ try {
1885
+ const auth = await requireAccessUser(c);
1886
+ if (auth instanceof Response) {
1887
+ return auth;
1888
+ }
1889
+ if (!searchManager) {
1890
+ return jsonError(c, 501, 'Search not configured', 'NOT_CONFIGURED');
1891
+ }
1892
+ const entity = c.req.param('entity');
1893
+ if (!schema.entities[entity]) {
1894
+ return jsonError(c, 404, `Entity not found: ${entity}`, 'NOT_FOUND');
1895
+ }
1896
+ const { columns, tokenize, prefix, rebuild } = await c.req.json();
1897
+ if (!columns || !Array.isArray(columns) || columns.length === 0) {
1898
+ return jsonError(c, 400, 'Missing or invalid columns', 'VALIDATION_ERROR');
1899
+ }
1900
+ // Register index configuration
1901
+ searchManager.registerIndex({
1902
+ entity,
1903
+ columns,
1904
+ tokenize,
1905
+ prefix,
1906
+ });
1907
+ // Create or rebuild index
1908
+ if (rebuild) {
1909
+ const count = await searchManager.rebuildIndex(entity);
1910
+ return c.json({ success: true, documentCount: count, rebuilt: true });
1911
+ }
1912
+ else {
1913
+ await searchManager.createSearchIndex(entity);
1914
+ return c.json({ success: true, created: true });
1915
+ }
1916
+ }
1917
+ catch (error) {
1918
+ console.error('Search index creation error:', error);
1919
+ return jsonError(c, 400, error instanceof Error ? error.message : 'Failed to create search index', 'API_ERROR');
1920
+ }
1921
+ });
1922
+ /**
1923
+ * DELETE /search/:entity/index
1924
+ * Delete search index for an entity
1925
+ */
1926
+ app.delete('/search/:entity/index', async (c) => {
1927
+ try {
1928
+ const auth = await requireAccessUser(c);
1929
+ if (auth instanceof Response) {
1930
+ return auth;
1931
+ }
1932
+ if (!searchManager) {
1933
+ return jsonError(c, 501, 'Search not configured', 'NOT_CONFIGURED');
1934
+ }
1935
+ const entity = c.req.param('entity');
1936
+ await searchManager.deleteSearchIndex(entity);
1937
+ return c.json({ success: true });
1938
+ }
1939
+ catch (error) {
1940
+ console.error('Search index deletion error:', error);
1941
+ return jsonError(c, 500, 'Failed to delete search index', 'API_ERROR');
1942
+ }
1943
+ });
1944
+ /**
1945
+ * GET /search/:entity/stats
1946
+ * Get search index statistics
1947
+ */
1948
+ app.get('/search/:entity/stats', async (c) => {
1949
+ try {
1950
+ const auth = await requireAccessUser(c);
1951
+ if (auth instanceof Response) {
1952
+ return auth;
1953
+ }
1954
+ if (!searchManager) {
1955
+ return jsonError(c, 501, 'Search not configured', 'NOT_CONFIGURED');
1956
+ }
1957
+ const entity = c.req.param('entity');
1958
+ const stats = await searchManager.getIndexStats(entity);
1959
+ return c.json(stats);
1960
+ }
1961
+ catch (error) {
1962
+ console.error('Search stats error:', error);
1963
+ return jsonError(c, 500, 'Failed to get search stats', 'API_ERROR');
1964
+ }
1965
+ });
1966
+ /**
1967
+ * POST /audit/query
1968
+ * Query audit logs with filters
1969
+ */
1970
+ app.post('/audit/query', async (c) => {
1971
+ try {
1972
+ const auth = await requireAccessUser(c);
1973
+ if (auth instanceof Response) {
1974
+ return auth;
1975
+ }
1976
+ if (!auditManager) {
1977
+ return jsonError(c, 501, 'Audit not configured', 'NOT_CONFIGURED');
1978
+ }
1979
+ const query = await c.req.json();
1980
+ const result = await auditManager.queryLogs(query);
1981
+ return c.json(result);
1982
+ }
1983
+ catch (error) {
1984
+ console.error('Audit query error:', error);
1985
+ return jsonError(c, 500, 'Failed to query audit logs', 'API_ERROR');
1986
+ }
1987
+ });
1988
+ /**
1989
+ * GET /audit/:auditId
1990
+ * Get a specific audit log
1991
+ */
1992
+ app.get('/audit/:auditId', async (c) => {
1993
+ try {
1994
+ const auth = await requireAccessUser(c);
1995
+ if (auth instanceof Response) {
1996
+ return auth;
1997
+ }
1998
+ if (!auditManager) {
1999
+ return jsonError(c, 501, 'Audit not configured', 'NOT_CONFIGURED');
2000
+ }
2001
+ const auditId = c.req.param('auditId');
2002
+ const log = await auditManager.getLog(auditId);
2003
+ if (!log) {
2004
+ return jsonError(c, 404, 'Audit log not found', 'NOT_FOUND');
2005
+ }
2006
+ return c.json(log);
2007
+ }
2008
+ catch (error) {
2009
+ console.error('Audit log fetch error:', error);
2010
+ return jsonError(c, 500, 'Failed to fetch audit log', 'API_ERROR');
2011
+ }
2012
+ });
2013
+ /**
2014
+ * GET /audit/record/:entity/:recordId
2015
+ * Get audit history for a specific record
2016
+ */
2017
+ app.get('/audit/record/:entity/:recordId', async (c) => {
2018
+ try {
2019
+ const auth = await requireAccessUser(c);
2020
+ if (auth instanceof Response) {
2021
+ return auth;
2022
+ }
2023
+ if (!auditManager) {
2024
+ return jsonError(c, 501, 'Audit not configured', 'NOT_CONFIGURED');
2025
+ }
2026
+ const entity = c.req.param('entity');
2027
+ const recordId = c.req.param('recordId');
2028
+ const history = await auditManager.getRecordHistory(entity, recordId);
2029
+ return c.json({ history });
2030
+ }
2031
+ catch (error) {
2032
+ console.error('Record history error:', error);
2033
+ return jsonError(c, 500, 'Failed to get record history', 'API_ERROR');
2034
+ }
2035
+ });
2036
+ /**
2037
+ * GET /audit/stats
2038
+ * Get audit statistics
2039
+ */
2040
+ app.get('/audit/stats', async (c) => {
2041
+ try {
2042
+ const auth = await requireAccessUser(c);
2043
+ if (auth instanceof Response) {
2044
+ return auth;
2045
+ }
2046
+ if (!auditManager) {
2047
+ return jsonError(c, 501, 'Audit not configured', 'NOT_CONFIGURED');
2048
+ }
2049
+ const entity = c.req.query('entity');
2050
+ const userId = c.req.query('userId');
2051
+ const startDate = c.req.query('startDate') ? parseInt(c.req.query('startDate')) : undefined;
2052
+ const endDate = c.req.query('endDate') ? parseInt(c.req.query('endDate')) : undefined;
2053
+ const stats = await auditManager.getStatistics({
2054
+ entity: entity || undefined,
2055
+ userId: userId || undefined,
2056
+ startDate,
2057
+ endDate,
2058
+ });
2059
+ return c.json(stats);
2060
+ }
2061
+ catch (error) {
2062
+ console.error('Audit stats error:', error);
2063
+ return jsonError(c, 500, 'Failed to get audit stats', 'API_ERROR');
2064
+ }
2065
+ });
2066
+ /**
2067
+ * POST /encryption/config
2068
+ * Register encryption configuration for an entity
2069
+ */
2070
+ app.post('/encryption/config', async (c) => {
2071
+ try {
2072
+ const auth = await requireAccessUser(c);
2073
+ if (auth instanceof Response) {
2074
+ return auth;
2075
+ }
2076
+ if (!encryptionManager) {
2077
+ return jsonError(c, 501, 'Encryption not configured', 'NOT_CONFIGURED');
2078
+ }
2079
+ const { entity, fields, algorithm, keyRotation } = await c.req.json();
2080
+ if (!entity || !fields || !Array.isArray(fields)) {
2081
+ return jsonError(c, 400, 'Missing or invalid entity or fields', 'VALIDATION_ERROR');
2082
+ }
2083
+ if (!schema.entities[entity]) {
2084
+ return jsonError(c, 404, `Entity not found: ${entity}`, 'NOT_FOUND');
2085
+ }
2086
+ encryptionManager.registerConfig({
2087
+ entity,
2088
+ fields,
2089
+ algorithm: algorithm || 'AES-GCM',
2090
+ keyRotation: keyRotation !== false,
2091
+ });
2092
+ return c.json({
2093
+ success: true,
2094
+ entity,
2095
+ fields,
2096
+ algorithm: algorithm || 'AES-GCM',
2097
+ keyRotation: keyRotation !== false,
2098
+ });
2099
+ }
2100
+ catch (error) {
2101
+ console.error('Encryption config error:', error);
2102
+ return jsonError(c, 400, error instanceof Error ? error.message : 'Failed to configure encryption', 'API_ERROR');
2103
+ }
2104
+ });
2105
+ /**
2106
+ * GET /encryption/config/:entity
2107
+ * Get encryption configuration for an entity
2108
+ */
2109
+ app.get('/encryption/config/:entity', async (c) => {
2110
+ try {
2111
+ const auth = await requireAccessUser(c);
2112
+ if (auth instanceof Response) {
2113
+ return auth;
2114
+ }
2115
+ if (!encryptionManager) {
2116
+ return jsonError(c, 501, 'Encryption not configured', 'NOT_CONFIGURED');
2117
+ }
2118
+ const entity = c.req.param('entity');
2119
+ if (!schema.entities[entity]) {
2120
+ return jsonError(c, 404, `Entity not found: ${entity}`, 'NOT_FOUND');
2121
+ }
2122
+ const config = encryptionManager.getConfig(entity);
2123
+ if (!config) {
2124
+ return c.json({
2125
+ entity,
2126
+ enabled: false,
2127
+ fields: [],
2128
+ });
2129
+ }
2130
+ return c.json({
2131
+ entity,
2132
+ enabled: true,
2133
+ fields: config.fields,
2134
+ algorithm: config.algorithm || 'AES-GCM',
2135
+ keyRotation: config.keyRotation !== false,
2136
+ });
2137
+ }
2138
+ catch (error) {
2139
+ console.error('Encryption config fetch error:', error);
2140
+ return jsonError(c, 500, 'Failed to fetch encryption config', 'API_ERROR');
2141
+ }
2142
+ });
2143
+ /**
2144
+ * POST /encryption/rotate-key
2145
+ * Rotate encryption key (requires master key)
2146
+ */
2147
+ app.post('/encryption/rotate-key', async (c) => {
2148
+ try {
2149
+ const auth = await requireAccessUser(c);
2150
+ if (auth instanceof Response) {
2151
+ return auth;
2152
+ }
2153
+ if (!encryptionManager) {
2154
+ return jsonError(c, 501, 'Encryption not configured', 'NOT_CONFIGURED');
2155
+ }
2156
+ const { newMasterKey } = await c.req.json();
2157
+ if (!newMasterKey || typeof newMasterKey !== 'string') {
2158
+ return jsonError(c, 400, 'Missing or invalid newMasterKey', 'VALIDATION_ERROR');
2159
+ }
2160
+ await encryptionManager.rotateKey(newMasterKey);
2161
+ return c.json({
2162
+ success: true,
2163
+ message: 'Encryption key rotated successfully',
2164
+ timestamp: Date.now(),
2165
+ });
2166
+ }
2167
+ catch (error) {
2168
+ console.error('Key rotation error:', error);
2169
+ return jsonError(c, 400, error instanceof Error ? error.message : 'Failed to rotate key', 'API_ERROR');
2170
+ }
2171
+ });
2172
+ /**
2173
+ * WebSocket upgrade endpoint for real-time subscriptions
2174
+ * GET /realtime
2175
+ */
2176
+ app.get('/realtime', async (c) => {
2177
+ if (!c.req.header('upgrade')?.toLowerCase().includes('websocket')) {
2178
+ return jsonError(c, 400, 'WebSocket upgrade required', 'INVALID_UPGRADE');
2179
+ }
2180
+ // Authenticate the connection
2181
+ const token = getBearerToken(c);
2182
+ if (!token) {
2183
+ return jsonError(c, 401, 'Unauthorized', 'UNAUTHORIZED');
2184
+ }
2185
+ const payload = parseJWT(token);
2186
+ if (!payload || payload.type !== 'access') {
2187
+ return jsonError(c, 401, 'Invalid token', 'UNAUTHORIZED');
2188
+ }
2189
+ const db = getDb(c);
2190
+ const userRow = await db
2191
+ .prepare('SELECT id, email, created_at, updated_at FROM users WHERE id = ?')
2192
+ .bind(payload.userId)
2193
+ .first();
2194
+ if (!userRow) {
2195
+ return jsonError(c, 401, 'User not found', 'UNAUTHORIZED');
2196
+ }
2197
+ const user = {
2198
+ id: userRow.id,
2199
+ email: userRow.email,
2200
+ createdAt: userRow.created_at,
2201
+ updatedAt: userRow.updated_at,
2202
+ };
2203
+ // Initialize managers if not done yet (fallback for non-auto-init)
2204
+ if (!subscriptionManager) {
2205
+ const syncDb = new D1SyncDatabase(db);
2206
+ subscriptionManager = new SubscriptionManager(syncDb);
2207
+ changeNotifier = new ChangeNotifier(subscriptionManager);
2208
+ await subscriptionManager.loadFromDatabase();
2209
+ }
2210
+ const connectionId = `conn_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
2211
+ try {
2212
+ // In Cloudflare Workers, WebSocketPair is available globally
2213
+ const upgradeHeader = c.req.header('upgrade');
2214
+ if (!upgradeHeader?.toLowerCase().includes('websocket')) {
2215
+ return jsonError(c, 400, 'WebSocket upgrade required', 'INVALID_UPGRADE');
2216
+ }
2217
+ // Create WebSocket pair (client/server)
2218
+ // @ts-ignore - WebSocketPair is available in Cloudflare Workers runtime
2219
+ const { 0: client, 1: server } = new WebSocketPair();
2220
+ // Register the WebSocket client
2221
+ const wsClient = {
2222
+ send(message) {
2223
+ server.send(message);
2224
+ },
2225
+ close() {
2226
+ server.close();
2227
+ },
2228
+ isOpen() {
2229
+ return server.readyState === 1; // OPEN
2230
+ },
2231
+ };
2232
+ changeNotifier.registerClient(connectionId, wsClient);
2233
+ // Handle incoming messages using addEventListener (Cloudflare Workers API)
2234
+ server.addEventListener('message', async (event) => {
2235
+ try {
2236
+ const message = JSON.parse(event.data);
2237
+ switch (message.type) {
2238
+ case 'subscribe':
2239
+ await subscriptionManager.subscribe(user.id, connectionId, message.entity, message.filters);
2240
+ server.send(JSON.stringify({
2241
+ type: 'subscribed',
2242
+ subscriptionId: `${message.entity}:${connectionId}`,
2243
+ entity: message.entity,
2244
+ timestamp: Date.now(),
2245
+ }));
2246
+ break;
2247
+ case 'unsubscribe':
2248
+ const subs = subscriptionManager.getSubscriptionsForConnection(connectionId);
2249
+ for (const sub of subs) {
2250
+ if (sub.entity === message.entity && sub.userId === user.id) {
2251
+ await subscriptionManager.unsubscribe(sub.subscriptionId);
2252
+ }
2253
+ }
2254
+ server.send(JSON.stringify({
2255
+ type: 'unsubscribed',
2256
+ entity: message.entity,
2257
+ timestamp: Date.now(),
2258
+ }));
2259
+ break;
2260
+ case 'heartbeat':
2261
+ const userSubs = subscriptionManager.getSubscriptionsForConnection(connectionId);
2262
+ for (const sub of userSubs) {
2263
+ await subscriptionManager.updateHeartbeat(sub.subscriptionId);
2264
+ }
2265
+ break;
2266
+ default:
2267
+ server.send(JSON.stringify({
2268
+ type: 'error',
2269
+ code: 'UNKNOWN_MESSAGE_TYPE',
2270
+ message: `Unknown message type: ${message.type}`,
2271
+ }));
2272
+ }
2273
+ }
2274
+ catch (error) {
2275
+ console.error('WebSocket message error:', error);
2276
+ server.send(JSON.stringify({
2277
+ type: 'error',
2278
+ code: 'MESSAGE_ERROR',
2279
+ message: 'Failed to process message',
2280
+ }));
2281
+ }
2282
+ });
2283
+ // Handle disconnect using addEventListener
2284
+ server.addEventListener('close', async () => {
2285
+ console.log(`Client ${connectionId} disconnected`);
2286
+ await subscriptionManager.disconnectClient(connectionId);
2287
+ changeNotifier.unregisterClient(connectionId);
2288
+ });
2289
+ // Handle errors using addEventListener
2290
+ server.addEventListener('error', (error) => {
2291
+ console.error(`WebSocket error for client ${connectionId}:`, error);
2292
+ changeNotifier.unregisterClient(connectionId);
2293
+ });
2294
+ // Send initial greeting
2295
+ server.send(JSON.stringify({
2296
+ type: 'connected',
2297
+ connectionId,
2298
+ timestamp: Date.now(),
2299
+ }));
2300
+ return new Response(client, {
2301
+ status: 101,
2302
+ statusText: 'Switching Protocols',
2303
+ headers: {
2304
+ 'Upgrade': 'websocket',
2305
+ 'Connection': 'Upgrade',
2306
+ },
2307
+ });
2308
+ }
2309
+ catch (error) {
2310
+ console.error('WebSocket connection error:', error);
2311
+ return jsonError(c, 500, 'Failed to establish WebSocket connection', 'CONNECTION_ERROR');
2312
+ }
2313
+ });
2314
+ /**
2315
+ * Get real-time statistics
2316
+ * GET /realtime/stats
2317
+ */
2318
+ app.get('/realtime/stats', async (c) => {
2319
+ const auth = await requireAccessUser(c);
2320
+ if (auth instanceof Response) {
2321
+ return auth;
2322
+ }
2323
+ if (!subscriptionManager || !changeNotifier) {
2324
+ return c.json({
2325
+ connectedClients: 0,
2326
+ subscriptions: { totalSubscriptions: 0, activeConnections: 0, entitiesWithSubscriptions: 0 },
2327
+ });
2328
+ }
2329
+ return c.json({
2330
+ connectedClients: changeNotifier.getConnectedClientsCount(),
2331
+ subscriptions: subscriptionManager.getStats(),
2332
+ timestamp: Date.now(),
2333
+ });
2334
+ });
2335
+ /**
2336
+ * POST /transactions/begin
2337
+ * Begin a new transaction
2338
+ */
2339
+ app.post('/transactions/begin', async (c) => {
2340
+ const auth = await requireAccessUser(c);
2341
+ if (auth instanceof Response) {
2342
+ return auth;
2343
+ }
2344
+ if (!transactionManager) {
2345
+ return jsonError(c, 503, 'Transaction manager not available', 'SERVICE_UNAVAILABLE');
2346
+ }
2347
+ try {
2348
+ const body = await c.req.json();
2349
+ const isolationLevel = body.isolationLevel || 'READ_COMMITTED';
2350
+ const transactionId = await transactionManager.begin(auth.user, isolationLevel);
2351
+ return c.json({
2352
+ transactionId,
2353
+ userId: auth.user.id,
2354
+ isolationLevel,
2355
+ createdAt: Date.now(),
2356
+ });
2357
+ }
2358
+ catch (error) {
2359
+ console.error('Transaction begin error:', error);
2360
+ return jsonError(c, 400, error instanceof Error ? error.message : 'Failed to begin transaction', 'TRANSACTION_ERROR');
2361
+ }
2362
+ });
2363
+ /**
2364
+ * POST /transactions/:txnId/commit
2365
+ * Commit a transaction
2366
+ */
2367
+ app.post('/transactions/:txnId/commit', async (c) => {
2368
+ const auth = await requireAccessUser(c);
2369
+ if (auth instanceof Response) {
2370
+ return auth;
2371
+ }
2372
+ if (!transactionManager) {
2373
+ return jsonError(c, 503, 'Transaction manager not available', 'SERVICE_UNAVAILABLE');
2374
+ }
2375
+ try {
2376
+ const transactionId = c.req.param('txnId');
2377
+ const transaction = transactionManager.getTransaction(transactionId);
2378
+ if (!transaction) {
2379
+ return jsonError(c, 404, 'Transaction not found', 'NOT_FOUND');
2380
+ }
2381
+ if (transaction.userId !== auth.user.id) {
2382
+ return jsonError(c, 403, 'Cannot commit transaction of another user', 'FORBIDDEN');
2383
+ }
2384
+ const result = await transactionManager.commit(transactionId);
2385
+ return c.json({
2386
+ transactionId,
2387
+ status: 'committed',
2388
+ appliedCount: result.appliedCount,
2389
+ errors: result.errors,
2390
+ timestamp: Date.now(),
2391
+ });
2392
+ }
2393
+ catch (error) {
2394
+ console.error('Transaction commit error:', error);
2395
+ return jsonError(c, 400, error instanceof Error ? error.message : 'Failed to commit transaction', 'TRANSACTION_ERROR');
2396
+ }
2397
+ });
2398
+ /**
2399
+ * POST /transactions/:txnId/rollback
2400
+ * Rollback a transaction
2401
+ */
2402
+ app.post('/transactions/:txnId/rollback', async (c) => {
2403
+ const auth = await requireAccessUser(c);
2404
+ if (auth instanceof Response) {
2405
+ return auth;
2406
+ }
2407
+ if (!transactionManager) {
2408
+ return jsonError(c, 503, 'Transaction manager not available', 'SERVICE_UNAVAILABLE');
2409
+ }
2410
+ try {
2411
+ const transactionId = c.req.param('txnId');
2412
+ const transaction = transactionManager.getTransaction(transactionId);
2413
+ if (!transaction) {
2414
+ return jsonError(c, 404, 'Transaction not found', 'NOT_FOUND');
2415
+ }
2416
+ if (transaction.userId !== auth.user.id) {
2417
+ return jsonError(c, 403, 'Cannot rollback transaction of another user', 'FORBIDDEN');
2418
+ }
2419
+ await transactionManager.rollback(transactionId);
2420
+ return c.json({
2421
+ transactionId,
2422
+ status: 'rolled_back',
2423
+ timestamp: Date.now(),
2424
+ });
2425
+ }
2426
+ catch (error) {
2427
+ console.error('Transaction rollback error:', error);
2428
+ return jsonError(c, 400, error instanceof Error ? error.message : 'Failed to rollback transaction', 'TRANSACTION_ERROR');
2429
+ }
2430
+ });
2431
+ /**
2432
+ * GET /transactions/:txnId
2433
+ * Get transaction status
2434
+ */
2435
+ app.get('/transactions/:txnId', async (c) => {
2436
+ const auth = await requireAccessUser(c);
2437
+ if (auth instanceof Response) {
2438
+ return auth;
2439
+ }
2440
+ if (!transactionManager) {
2441
+ return jsonError(c, 503, 'Transaction manager not available', 'SERVICE_UNAVAILABLE');
2442
+ }
2443
+ try {
2444
+ const transactionId = c.req.param('txnId');
2445
+ const transaction = transactionManager.getTransaction(transactionId);
2446
+ if (!transaction) {
2447
+ return jsonError(c, 404, 'Transaction not found', 'NOT_FOUND');
2448
+ }
2449
+ if (transaction.userId !== auth.user.id) {
2450
+ return jsonError(c, 403, 'Cannot access transaction of another user', 'FORBIDDEN');
2451
+ }
2452
+ return c.json({
2453
+ transactionId: transaction.id,
2454
+ status: transaction.status,
2455
+ isolationLevel: transaction.isolationLevel,
2456
+ changeCount: transaction.changes.length,
2457
+ createdAt: transaction.createdAt,
2458
+ expiresAt: transaction.expiresAt,
2459
+ completedAt: transaction.completedAt,
2460
+ });
2461
+ }
2462
+ catch (error) {
2463
+ console.error('Transaction status error:', error);
2464
+ return jsonError(c, 500, 'Failed to get transaction status', 'TRANSACTION_ERROR');
2465
+ }
2466
+ });
2467
+ return app;
2468
+ }
2469
+ export default createEdgeBaseWorker;
2470
+ //# sourceMappingURL=index.js.map