@edge-base/server 0.2.5 → 0.2.6

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 (125) hide show
  1. package/admin-build/_app/immutable/chunks/{DILS_-VJ.js → B3CvhH3c.js} +1 -1
  2. package/admin-build/_app/immutable/chunks/BN_-k-Ck.js +1 -0
  3. package/admin-build/_app/immutable/chunks/{Dt4vL4Df.js → BYL_uBga.js} +1 -1
  4. package/admin-build/_app/immutable/chunks/{C72lTcG0.js → Bcs4KYNp.js} +1 -1
  5. package/admin-build/_app/immutable/chunks/{B8s_s9QY.js → BkZCgsc3.js} +1 -1
  6. package/admin-build/_app/immutable/chunks/{D2j3I1VQ.js → BvoGcDFV.js} +1 -1
  7. package/admin-build/_app/immutable/chunks/{B2TnDKF7.js → CCUxCptE.js} +1 -1
  8. package/admin-build/_app/immutable/chunks/CLHN9MVr.js +1 -0
  9. package/admin-build/_app/immutable/chunks/{J2Gw0SMu.js → CR37B8DX.js} +1 -1
  10. package/admin-build/_app/immutable/chunks/CbfX3ELZ.js +1 -0
  11. package/admin-build/_app/immutable/chunks/CjcrXziO.js +2 -0
  12. package/admin-build/_app/immutable/chunks/CrwlCAM0.js +1 -0
  13. package/admin-build/_app/immutable/chunks/{B0HRJ657.js → DOOPbWwG.js} +1 -1
  14. package/admin-build/_app/immutable/chunks/DQVP4KC-.js +1 -0
  15. package/admin-build/_app/immutable/chunks/{B6MschND.js → DdvsFblq.js} +1 -1
  16. package/admin-build/_app/immutable/chunks/DemDWbs-.js +128 -0
  17. package/admin-build/_app/immutable/chunks/{CaVKAiCe.js → DmDTovpg.js} +1 -1
  18. package/admin-build/_app/immutable/chunks/{B94PilAN.js → Ff90owjx.js} +1 -1
  19. package/admin-build/_app/immutable/chunks/{BqTb6Mxk.js → LL3ulaxa.js} +1 -1
  20. package/admin-build/_app/immutable/chunks/Q3vAxeY-.js +1 -0
  21. package/admin-build/_app/immutable/chunks/SQVAC3Cv.js +1 -0
  22. package/admin-build/_app/immutable/chunks/{Z41NK6i6.js → bguI1TeA.js} +1 -1
  23. package/admin-build/_app/immutable/chunks/{_teD5ji5.js → nlAMTi52.js} +1 -1
  24. package/admin-build/_app/immutable/chunks/{Cdm5zBRA.js → qBm6xof8.js} +1 -1
  25. package/admin-build/_app/immutable/entry/{app.D3flihMw.js → app.CP83Ni80.js} +2 -2
  26. package/admin-build/_app/immutable/entry/start.DY6YakU0.js +1 -0
  27. package/admin-build/_app/immutable/nodes/{0.CdczqZLK.js → 0.DiRq7puO.js} +1 -1
  28. package/admin-build/_app/immutable/nodes/1.BFeyKLGT.js +1 -0
  29. package/admin-build/_app/immutable/nodes/10.zcee7hJx.js +1 -0
  30. package/admin-build/_app/immutable/nodes/11.BW7wLs2Y.js +1 -0
  31. package/admin-build/_app/immutable/nodes/12.CxJRlYSd.js +1 -0
  32. package/admin-build/_app/immutable/nodes/13.pp0F_5hn.js +110 -0
  33. package/admin-build/_app/immutable/nodes/14.t3AfGiGo.js +3 -0
  34. package/admin-build/_app/immutable/nodes/15.B3agc7NX.js +1 -0
  35. package/admin-build/_app/immutable/nodes/{16.BR7WwQrS.js → 16.C4uG2-i8.js} +1 -1
  36. package/admin-build/_app/immutable/nodes/{17.Cm57KKXV.js → 17.CwGxi1Bn.js} +1 -1
  37. package/admin-build/_app/immutable/nodes/18.CrQyN_gU.js +1 -0
  38. package/admin-build/_app/immutable/nodes/19.NEPUOXl7.js +2 -0
  39. package/admin-build/_app/immutable/nodes/{20.DnHeFlTv.js → 20.DGHO8ipr.js} +1 -1
  40. package/admin-build/_app/immutable/nodes/21.UVKBDvp4.js +1 -0
  41. package/admin-build/_app/immutable/nodes/22.Dri5It7a.js +1 -0
  42. package/admin-build/_app/immutable/nodes/{23.CWSGMcKJ.js → 23.BPQP_Zte.js} +2 -2
  43. package/admin-build/_app/immutable/nodes/24.D580FdSS.js +2 -0
  44. package/admin-build/_app/immutable/nodes/25.BMNPOZwF.js +2 -0
  45. package/admin-build/_app/immutable/nodes/26.XcpEcbiz.js +1 -0
  46. package/admin-build/_app/immutable/nodes/27.C1zHHcYv.js +1 -0
  47. package/admin-build/_app/immutable/nodes/28.CuKzzrY8.js +1 -0
  48. package/admin-build/_app/immutable/nodes/29.nLpBMXnM.js +1 -0
  49. package/admin-build/_app/immutable/nodes/{3.B6q-7qr8.js → 3.5G_aseoL.js} +1 -1
  50. package/admin-build/_app/immutable/nodes/30.CQC4nLoU.js +1 -0
  51. package/admin-build/_app/immutable/nodes/31.Bet8kxOK.js +1 -0
  52. package/admin-build/_app/immutable/nodes/4.nmJDYJpC.js +1 -0
  53. package/admin-build/_app/immutable/nodes/5.CnbYLG4E.js +1 -0
  54. package/admin-build/_app/immutable/nodes/6.KA01b-3y.js +1 -0
  55. package/admin-build/_app/immutable/nodes/7.CP9fkn1L.js +1 -0
  56. package/admin-build/_app/immutable/nodes/8.BTzDb---.js +1 -0
  57. package/admin-build/_app/immutable/nodes/9.DkNJg_J6.js +1 -0
  58. package/admin-build/_app/version.json +1 -1
  59. package/admin-build/index.html +7 -7
  60. package/package.json +3 -3
  61. package/src/__tests__/push-handlers.test.ts +1 -1
  62. package/src/__tests__/room-runtime-routing.test.ts +23 -0
  63. package/src/__tests__/route-parser.test.ts +6 -0
  64. package/src/__tests__/schema.test.ts +15 -6
  65. package/src/durable-objects/database-do.ts +7 -1
  66. package/src/durable-objects/room-runtime-base.ts +49 -40
  67. package/src/durable-objects/rooms-do.ts +32 -1
  68. package/src/index.ts +23 -9
  69. package/src/lib/d1-handler.ts +32 -17
  70. package/src/lib/postgres-handler.ts +24 -12
  71. package/src/lib/route-parser.ts +3 -0
  72. package/src/lib/schemas.ts +12 -2
  73. package/src/middleware/captcha-verify.ts +16 -3
  74. package/src/middleware/error-handler.ts +1 -1
  75. package/src/middleware/rules.ts +28 -9
  76. package/src/routes/admin-auth.ts +3 -3
  77. package/src/routes/admin.ts +13 -8
  78. package/src/routes/analytics-api.ts +3 -3
  79. package/src/routes/auth.ts +1 -1
  80. package/src/routes/backup.ts +1 -1
  81. package/src/routes/d1.ts +14 -7
  82. package/src/routes/database-live.ts +13 -6
  83. package/src/routes/kv.ts +21 -10
  84. package/src/routes/oauth.ts +1 -1
  85. package/src/routes/push.ts +119 -77
  86. package/src/routes/room.ts +215 -7
  87. package/src/routes/schema-endpoint.ts +2 -2
  88. package/src/routes/sql.ts +10 -6
  89. package/src/routes/storage.ts +4 -2
  90. package/src/routes/vectorize.ts +16 -4
  91. package/admin-build/_app/immutable/chunks/6oMK_164.js +0 -1
  92. package/admin-build/_app/immutable/chunks/BEW7Ez_g.js +0 -1
  93. package/admin-build/_app/immutable/chunks/BoOooyH6.js +0 -1
  94. package/admin-build/_app/immutable/chunks/BvHnF5tV.js +0 -1
  95. package/admin-build/_app/immutable/chunks/CoI6jjbg.js +0 -2
  96. package/admin-build/_app/immutable/chunks/CrOZMmdF.js +0 -1
  97. package/admin-build/_app/immutable/chunks/Cw6OYcq-.js +0 -1
  98. package/admin-build/_app/immutable/chunks/DPdQ7z0T.js +0 -128
  99. package/admin-build/_app/immutable/chunks/pUxw8jfq.js +0 -1
  100. package/admin-build/_app/immutable/entry/start.Cl6sLxnz.js +0 -1
  101. package/admin-build/_app/immutable/nodes/1.DxcSsEqS.js +0 -1
  102. package/admin-build/_app/immutable/nodes/10.DuAd4aIm.js +0 -1
  103. package/admin-build/_app/immutable/nodes/11.0jgHQL92.js +0 -1
  104. package/admin-build/_app/immutable/nodes/12.CKNPqmyy.js +0 -1
  105. package/admin-build/_app/immutable/nodes/13.B1p2POXS.js +0 -110
  106. package/admin-build/_app/immutable/nodes/14.Bb-REBND.js +0 -3
  107. package/admin-build/_app/immutable/nodes/15.1uBFCX0X.js +0 -1
  108. package/admin-build/_app/immutable/nodes/18.CoiwfAuQ.js +0 -1
  109. package/admin-build/_app/immutable/nodes/19.B8ZdLlXj.js +0 -2
  110. package/admin-build/_app/immutable/nodes/21.CJFaf0Ia.js +0 -1
  111. package/admin-build/_app/immutable/nodes/22.CItETFzy.js +0 -1
  112. package/admin-build/_app/immutable/nodes/24.CWbEqNMB.js +0 -2
  113. package/admin-build/_app/immutable/nodes/25.DRkLEhKi.js +0 -2
  114. package/admin-build/_app/immutable/nodes/26.BRxO8AYH.js +0 -1
  115. package/admin-build/_app/immutable/nodes/27.BLs-nVHz.js +0 -1
  116. package/admin-build/_app/immutable/nodes/28.G79qkdBK.js +0 -1
  117. package/admin-build/_app/immutable/nodes/29.BOcI6g0N.js +0 -1
  118. package/admin-build/_app/immutable/nodes/30.DAIC7dKd.js +0 -1
  119. package/admin-build/_app/immutable/nodes/31.pl0XXjXF.js +0 -1
  120. package/admin-build/_app/immutable/nodes/4.DOdvVlZj.js +0 -1
  121. package/admin-build/_app/immutable/nodes/5.BW_zlgye.js +0 -1
  122. package/admin-build/_app/immutable/nodes/6.Dxy1CAI2.js +0 -1
  123. package/admin-build/_app/immutable/nodes/7.BG98w_o7.js +0 -1
  124. package/admin-build/_app/immutable/nodes/8.DoG5R2rG.js +0 -1
  125. package/admin-build/_app/immutable/nodes/9.Dmxf6zAC.js +0 -1
@@ -362,7 +362,7 @@ adminRoute.onError((err, c) => {
362
362
  return c.json(err.toJSON(), err.code as 400);
363
363
  }
364
364
  console.error('Admin Dashboard unhandled error:', err);
365
- return c.json({ code: 500, message: 'Internal server error.' }, 500);
365
+ return c.json({ code: 500, message: 'Admin dashboard request failed unexpectedly. Check the worker logs for the original exception.' }, 500);
366
366
  });
367
367
 
368
368
  // ─────────────────────────────────────────────
@@ -2133,7 +2133,12 @@ api.openapi(adminImportTable, async (c) => {
2133
2133
  }
2134
2134
  } catch (err) {
2135
2135
  for (let j = 0; j < chunk.length; j++) {
2136
- errors.push({ row: i + j, message: err instanceof Error ? err.message : 'Unknown error' });
2136
+ errors.push({
2137
+ row: i + j,
2138
+ message: err instanceof Error
2139
+ ? err.message
2140
+ : 'Import failed with an unexpected admin API error. Check worker logs for details.',
2141
+ });
2137
2142
  }
2138
2143
  }
2139
2144
  }
@@ -3436,7 +3441,7 @@ api.openapi(adminDestroyApp, async (c) => {
3436
3441
  const label = `D1 ${r.name}`;
3437
3442
  const result = await cfApi(accountId, apiToken, 'DELETE', `/d1/database/${r.id}`);
3438
3443
  if (result.ok) deleted.push(label);
3439
- else failed.push({ resource: label, error: result.error ?? 'Unknown error' });
3444
+ else failed.push({ resource: label, error: result.error ?? 'Unknown Cloudflare API error while deleting this resource.' });
3440
3445
  }
3441
3446
  }
3442
3447
 
@@ -3446,7 +3451,7 @@ api.openapi(adminDestroyApp, async (c) => {
3446
3451
  const label = `Vectorize ${indexName}`;
3447
3452
  const result = await cfApi(accountId, apiToken, 'DELETE', `/vectorize/v2/indexes/${indexName}`);
3448
3453
  if (result.ok) deleted.push(label);
3449
- else failed.push({ resource: label, error: result.error ?? 'Unknown error' });
3454
+ else failed.push({ resource: label, error: result.error ?? 'Unknown Cloudflare API error while deleting this resource.' });
3450
3455
  }
3451
3456
  }
3452
3457
 
@@ -3455,7 +3460,7 @@ api.openapi(adminDestroyApp, async (c) => {
3455
3460
  const label = `Hyperdrive ${r.name}`;
3456
3461
  const result = await cfApi(accountId, apiToken, 'DELETE', `/hyperdrive/configs/${r.id}`);
3457
3462
  if (result.ok) deleted.push(label);
3458
- else failed.push({ resource: label, error: result.error ?? 'Unknown error' });
3463
+ else failed.push({ resource: label, error: result.error ?? 'Unknown Cloudflare API error while deleting this resource.' });
3459
3464
  }
3460
3465
  }
3461
3466
 
@@ -3492,7 +3497,7 @@ api.openapi(adminDestroyApp, async (c) => {
3492
3497
  // Turnstile uses zone-level API, not account
3493
3498
  const result = await cfApi(accountId, apiToken, 'DELETE', `/challenges/widgets/${r.id}`);
3494
3499
  if (result.ok) deleted.push(label);
3495
- else failed.push({ resource: label, error: result.error ?? 'Unknown error' });
3500
+ else failed.push({ resource: label, error: result.error ?? 'Unknown Cloudflare API error while deleting this resource.' });
3496
3501
  }
3497
3502
  }
3498
3503
 
@@ -3503,7 +3508,7 @@ api.openapi(adminDestroyApp, async (c) => {
3503
3508
  if (result.ok) {
3504
3509
  deleted.push(label);
3505
3510
  } else {
3506
- failed.push({ resource: label, error: result.error ?? 'Unknown error' });
3511
+ failed.push({ resource: label, error: result.error ?? 'Unknown Cloudflare API error while deleting this resource.' });
3507
3512
  }
3508
3513
  }
3509
3514
 
@@ -3513,7 +3518,7 @@ api.openapi(adminDestroyApp, async (c) => {
3513
3518
  const label = `KV ${r.name}`;
3514
3519
  const result = await cfApi(accountId, apiToken, 'DELETE', `/storage/kv/namespaces/${r.id}`);
3515
3520
  if (result.ok) deleted.push(label);
3516
- else failed.push({ resource: label, error: result.error ?? 'Unknown error' });
3521
+ else failed.push({ resource: label, error: result.error ?? 'Unknown Cloudflare API error while deleting this resource.' });
3517
3522
  }
3518
3523
  }
3519
3524
 
@@ -29,10 +29,10 @@ function requireServiceKey(c: { env: Env; req: { header: (name: string) => strin
29
29
  const constraintCtx = buildConstraintCtx(c.env as never, c.req);
30
30
  const { result } = validateKey(provided, 'analytics:*:*:*', config, c.env as never, undefined, constraintCtx);
31
31
  if (result === 'missing') {
32
- throw new EdgeBaseError(403, 'Service Key required for analytics queries.');
32
+ throw new EdgeBaseError(403, 'X-EdgeBase-Service-Key is required for analytics queries.');
33
33
  }
34
34
  if (result === 'invalid') {
35
- throw new EdgeBaseError(401, 'Invalid Service Key.');
35
+ throw new EdgeBaseError(401, 'Invalid X-EdgeBase-Service-Key for analytics queries.');
36
36
  }
37
37
  }
38
38
 
@@ -107,7 +107,7 @@ analyticsApi.openapi(trackEvents, async (c) => {
107
107
  try {
108
108
  body = await c.req.json();
109
109
  } catch {
110
- throw new EdgeBaseError(400, 'Invalid JSON body.');
110
+ throw new EdgeBaseError(400, 'Invalid JSON body for analytics tracking. Send application/json with { events: [...] }.');
111
111
  }
112
112
 
113
113
  const events = body.events;
@@ -1238,7 +1238,7 @@ authRoute.openapi(signinAnonymous, async (c) => {
1238
1238
 
1239
1239
  const user = await authService.getUserById(db, userId);
1240
1240
  if (user) {
1241
- syncUserPublic(c.env, c.executionCtx, userId, authService.buildPublicUserData(user));
1241
+ await syncUserPublic(c.env, c.executionCtx, userId, authService.buildPublicUserData(user), true);
1242
1242
 
1243
1243
  return c.json({
1244
1244
  user: authService.sanitizeUser(user),
@@ -86,7 +86,7 @@ backupRoute.onError((err, c) => {
86
86
  return c.json(err.toJSON(), err.code as 400);
87
87
  }
88
88
  console.error('Backup API error:', err);
89
- return c.json({ code: 500, message: 'Internal server error.' }, 500);
89
+ return c.json({ code: 500, message: 'Backup operation failed unexpectedly. Check the worker logs for the original exception.' }, 500);
90
90
  });
91
91
 
92
92
  // ─── DO Name Helpers ───
package/src/routes/d1.ts CHANGED
@@ -22,6 +22,10 @@ import { zodDefaultHook, d1BodySchema, jsonResponseSchema, errorResponseSchema }
22
22
 
23
23
  export const d1Route = new OpenAPIHono<HonoEnv>({ defaultHook: zodDefaultHook });
24
24
 
25
+ function invalidD1JsonMessage(): string {
26
+ return 'Invalid JSON body. Send application/json with { query, params? }.';
27
+ }
28
+
25
29
  /**
26
30
  * POST /api/d1/:database
27
31
  * Body: { query: string, params?: unknown[] }
@@ -51,12 +55,12 @@ d1Route.openapi(executeD1Query, async (c) => {
51
55
  try {
52
56
  body = await c.req.json();
53
57
  } catch {
54
- return c.json({ code: 400, message: 'Invalid JSON body' }, 400);
58
+ return c.json({ code: 400, message: invalidD1JsonMessage() }, 400);
55
59
  }
56
60
 
57
61
  const { query, params } = body;
58
62
  if (!query || typeof query !== 'string') {
59
- return c.json({ code: 400, message: 'query is required' }, 400);
63
+ return c.json({ code: 400, message: "Missing required field 'query'. Send the SQL string in the request body." }, 400);
60
64
  }
61
65
 
62
66
  // §2 Allowlist: validate database is declared in config
@@ -76,16 +80,19 @@ d1Route.openapi(executeD1Query, async (c) => {
76
80
  buildConstraintCtx(c.env, c.req),
77
81
  );
78
82
  if (skResult === 'missing') {
79
- return c.json({ code: 403, message: 'Service Key required to access D1' }, 403);
83
+ return c.json({ code: 403, message: `X-EdgeBase-Service-Key is required to execute raw SQL on D1 database '${nameParam}'.` }, 403);
80
84
  }
81
85
  if (skResult === 'invalid') {
82
- return c.json({ code: 401, message: 'Unauthorized. Invalid Service Key.' }, 401);
86
+ return c.json({ code: 401, message: `Invalid X-EdgeBase-Service-Key for D1 database '${nameParam}'.` }, 401);
83
87
  }
84
88
 
85
89
  // §1 Env type — dynamic binding access via type assertion
86
90
  const binding = (c.env as unknown as Record<string, unknown>)[d1Config.binding] as D1Database | undefined;
87
91
  if (!binding) {
88
- return c.json({ code: 500, message: `D1 binding '${d1Config.binding}' not available.` }, 500);
92
+ return c.json({
93
+ code: 500,
94
+ message: `D1 binding '${d1Config.binding}' is unavailable. Check the binding name in edgebase.config.ts and wrangler.toml.`,
95
+ }, 500);
89
96
  }
90
97
 
91
98
  // Execute D1 query — all SQL allowed (DDL included), ? bind variables enforced
@@ -103,7 +110,7 @@ d1Route.openapi(executeD1Query, async (c) => {
103
110
  },
104
111
  });
105
112
  } catch (err) {
106
- const message = err instanceof Error ? err.message : 'D1 query execution failed';
107
- throw new EdgeBaseError(400, message);
113
+ const message = err instanceof Error ? err.message : 'Unknown D1 query error';
114
+ throw new EdgeBaseError(400, `D1 query failed for '${nameParam}': ${message}`);
108
115
  }
109
116
  });
@@ -26,6 +26,10 @@ import {
26
26
 
27
27
  export const databaseLiveRoute = new OpenAPIHono<HonoEnv>({ defaultHook: zodDefaultHook });
28
28
 
29
+ function invalidDatabaseLiveJsonMessage(): string {
30
+ return 'Invalid JSON body for database-live broadcast. Send application/json with { channel, event, payload? }.';
31
+ }
32
+
29
33
  const MAX_PENDING_PER_IP = 5;
30
34
  const PENDING_TTL_SECONDS = 60;
31
35
  const dbLiveQuerySchema = z.object({
@@ -269,15 +273,15 @@ databaseLiveRoute.openapi(databaseLiveBroadcast, async (c) => {
269
273
  try {
270
274
  body = await c.req.json();
271
275
  } catch {
272
- return c.json({ code: 400, message: 'Invalid JSON body' }, 400);
276
+ return c.json({ code: 400, message: invalidDatabaseLiveJsonMessage() }, 400);
273
277
  }
274
278
 
275
279
  const { channel, event, payload } = body;
276
280
  if (!channel || typeof channel !== 'string') {
277
- return c.json({ code: 400, message: 'channel is required' }, 400);
281
+ return c.json({ code: 400, message: "Missing required field 'channel' for database-live broadcast." }, 400);
278
282
  }
279
283
  if (!event || typeof event !== 'string') {
280
- return c.json({ code: 400, message: 'event is required' }, 400);
284
+ return c.json({ code: 400, message: "Missing required field 'event' for database-live broadcast." }, 400);
281
285
  }
282
286
 
283
287
  // Service Key required AND validated
@@ -291,10 +295,10 @@ databaseLiveRoute.openapi(databaseLiveBroadcast, async (c) => {
291
295
  buildConstraintCtx(c.env, c.req),
292
296
  );
293
297
  if (skResult === 'missing') {
294
- return c.json({ code: 403, message: 'Service Key required for server broadcast' }, 403);
298
+ return c.json({ code: 403, message: `X-EdgeBase-Service-Key is required to broadcast to database-live channel '${channel}'.` }, 403);
295
299
  }
296
300
  if (skResult === 'invalid') {
297
- return c.json({ code: 401, message: 'Unauthorized. Invalid Service Key.' }, 401);
301
+ return c.json({ code: 401, message: `Invalid X-EdgeBase-Service-Key for database-live channel '${channel}'.` }, 401);
298
302
  }
299
303
 
300
304
  // Route broadcast through the DatabaseLiveDO hub
@@ -308,7 +312,10 @@ databaseLiveRoute.openapi(databaseLiveBroadcast, async (c) => {
308
312
  }));
309
313
 
310
314
  if (!doResponse.ok) {
311
- return c.json({ code: doResponse.status, message: 'Broadcast failed' }, doResponse.status as 400 | 500);
315
+ return c.json({
316
+ code: doResponse.status,
317
+ message: `Broadcast to database-live channel '${channel}' failed for event '${event}'.`,
318
+ }, doResponse.status as 400 | 500);
312
319
  }
313
320
 
314
321
  return c.json({ ok: true });
package/src/routes/kv.ts CHANGED
@@ -20,6 +20,14 @@ import { zodDefaultHook, kvBodySchema, jsonResponseSchema, errorResponseSchema }
20
20
 
21
21
  export const kvRoute = new OpenAPIHono<HonoEnv>({ defaultHook: zodDefaultHook });
22
22
 
23
+ function invalidKvJsonMessage(): string {
24
+ return 'Invalid JSON body. Send application/json with a KV operation payload like { action, key, value }.';
25
+ }
26
+
27
+ function missingKvFieldMessage(field: string, action: string): string {
28
+ return `Missing required field '${field}' for KV action '${action}'.`;
29
+ }
30
+
23
31
  function normalizeKvBindingError(action: string, error: unknown): EdgeBaseError {
24
32
  const message = error instanceof Error ? error.message : String(error);
25
33
  const lowered = message.toLowerCase();
@@ -75,12 +83,12 @@ kvRoute.openapi(kvOperation, async (c) => {
75
83
  try {
76
84
  body = await c.req.json();
77
85
  } catch {
78
- return c.json({ code: 400, message: 'Invalid JSON body' }, 400);
86
+ return c.json({ code: 400, message: invalidKvJsonMessage() }, 400);
79
87
  }
80
88
 
81
89
  const { action } = body;
82
90
  if (!action || !['get', 'set', 'delete', 'list'].includes(action)) {
83
- return c.json({ code: 400, message: "action must be one of: 'get', 'set', 'delete', 'list'" }, 400);
91
+ return c.json({ code: 400, message: "Invalid KV action. Expected one of: 'get', 'set', 'delete', 'list'." }, 400);
84
92
  }
85
93
 
86
94
  // §2 Allowlist: validate namespace is declared in config
@@ -105,28 +113,31 @@ kvRoute.openapi(kvOperation, async (c) => {
105
113
  buildConstraintCtx(c.env, c.req),
106
114
  );
107
115
  if (skResult === 'missing') {
108
- return c.json({ code: 403, message: 'Service Key required to access KV' }, 403);
116
+ return c.json({ code: 403, message: `X-EdgeBase-Service-Key is required to access KV namespace '${nameParam}'.` }, 403);
109
117
  }
110
118
  if (skResult === 'invalid') {
111
- return c.json({ code: 401, message: 'Unauthorized. Invalid Service Key.' }, 401);
119
+ return c.json({ code: 401, message: `Invalid X-EdgeBase-Service-Key for KV namespace '${nameParam}'.` }, 401);
112
120
  }
113
121
 
114
122
  // §1 Env type — dynamic binding access via type assertion
115
123
  const binding = (c.env as unknown as Record<string, unknown>)[kvConfig.binding] as KVNamespace | undefined;
116
124
  if (!binding) {
117
- return c.json({ code: 500, message: `KV binding '${kvConfig.binding}' not available.` }, 500);
125
+ return c.json({
126
+ code: 500,
127
+ message: `KV binding '${kvConfig.binding}' is unavailable. Check the binding name in edgebase.config.ts and wrangler.toml.`,
128
+ }, 500);
118
129
  }
119
130
 
120
131
  // Execute KV operation
121
132
  switch (action) {
122
133
  case 'get': {
123
- if (!body.key) return c.json({ code: 400, message: 'key is required for get' }, 400);
134
+ if (!body.key) return c.json({ code: 400, message: missingKvFieldMessage('key', 'get') }, 400);
124
135
  const value = await binding.get(body.key);
125
136
  return c.json({ value });
126
137
  }
127
138
  case 'set': {
128
- if (!body.key) return c.json({ code: 400, message: 'key is required for set' }, 400);
129
- if (body.value === undefined) return c.json({ code: 400, message: 'value is required for set' }, 400);
139
+ if (!body.key) return c.json({ code: 400, message: missingKvFieldMessage('key', 'set') }, 400);
140
+ if (body.value === undefined) return c.json({ code: 400, message: missingKvFieldMessage('value', 'set') }, 400);
130
141
  const putOptions: KVNamespacePutOptions = {};
131
142
  if (body.ttl) putOptions.expirationTtl = body.ttl;
132
143
  try {
@@ -137,7 +148,7 @@ kvRoute.openapi(kvOperation, async (c) => {
137
148
  return c.json({ ok: true });
138
149
  }
139
150
  case 'delete': {
140
- if (!body.key) return c.json({ code: 400, message: 'key is required for delete' }, 400);
151
+ if (!body.key) return c.json({ code: 400, message: missingKvFieldMessage('key', 'delete') }, 400);
141
152
  try {
142
153
  await binding.delete(body.key);
143
154
  } catch (error) {
@@ -162,6 +173,6 @@ kvRoute.openapi(kvOperation, async (c) => {
162
173
  });
163
174
  }
164
175
  default:
165
- return c.json({ code: 400, message: 'Unknown action' }, 400);
176
+ return c.json({ code: 400, message: `Unsupported KV action '${action}'.` }, 400);
166
177
  }
167
178
  });
@@ -86,7 +86,7 @@ oauthRoute.onError((err, c) => {
86
86
  return c.json(err.toJSON(), err.code as 400);
87
87
  }
88
88
  console.error('OAuth unhandled error:', err);
89
- return c.json({ code: 500, message: 'OAuth error.' }, 500);
89
+ return c.json({ code: 500, message: 'OAuth flow failed unexpectedly. Check the worker logs for the original exception.' }, 500);
90
90
  });
91
91
 
92
92
  // ─── Helpers ───