@edge-base/server 0.2.4 → 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 (131) 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/{BgDzp0i0.js → BvoGcDFV.js} +1 -1
  7. package/admin-build/_app/immutable/chunks/{BME_U9TJ.js → CCUxCptE.js} +1 -1
  8. package/admin-build/_app/immutable/chunks/CLHN9MVr.js +1 -0
  9. package/admin-build/_app/immutable/chunks/{DYaCRWMA.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/{Dj0QUuOf.js → DdvsFblq.js} +1 -1
  16. package/admin-build/_app/immutable/chunks/DemDWbs-.js +128 -0
  17. package/admin-build/_app/immutable/chunks/{XQM1k9PM.js → DmDTovpg.js} +1 -1
  18. package/admin-build/_app/immutable/chunks/{fYEKMQ-Z.js → Ff90owjx.js} +1 -1
  19. package/admin-build/_app/immutable/chunks/{5RQRbp5q.js → LL3ulaxa.js} +1 -1
  20. package/admin-build/_app/immutable/chunks/{DBsVqhuh.js → Q3vAxeY-.js} +1 -1
  21. package/admin-build/_app/immutable/chunks/{D__dwMuW.js → SQVAC3Cv.js} +1 -1
  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/{BjWZuf8W.js → qBm6xof8.js} +1 -1
  25. package/admin-build/_app/immutable/entry/{app.C8ylfBe6.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.CJJ6HZbp.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.D0xkPUBW.js → 16.C4uG2-i8.js} +1 -1
  36. package/admin-build/_app/immutable/nodes/{17.CebNqPeh.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.DYb-q3W8.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.BLgq21om.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.z8ut3jS-.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__/database-do-route-validation.test.ts +10 -7
  62. package/src/__tests__/meta-route-registration.test.ts +20 -15
  63. package/src/__tests__/push-handlers.test.ts +1 -1
  64. package/src/__tests__/room-auth-state-loss.test.ts +122 -0
  65. package/src/__tests__/room-handler-context.test.ts +4 -4
  66. package/src/__tests__/room-rate-limit-scopes.test.ts +38 -0
  67. package/src/__tests__/room-runtime-routing.test.ts +23 -0
  68. package/src/__tests__/route-parser.test.ts +6 -0
  69. package/src/__tests__/runtime-startup.test.ts +49 -0
  70. package/src/__tests__/schema.test.ts +15 -6
  71. package/src/durable-objects/database-do.ts +21 -1
  72. package/src/durable-objects/database-live-do.ts +15 -0
  73. package/src/durable-objects/room-runtime-base.ts +436 -169
  74. package/src/durable-objects/rooms-do.ts +63 -25
  75. package/src/index.ts +340 -280
  76. package/src/lib/d1-handler.ts +32 -17
  77. package/src/lib/postgres-handler.ts +24 -12
  78. package/src/lib/route-parser.ts +3 -0
  79. package/src/lib/runtime-startup.ts +53 -0
  80. package/src/lib/schemas.ts +12 -2
  81. package/src/middleware/captcha-verify.ts +16 -3
  82. package/src/middleware/error-handler.ts +1 -1
  83. package/src/middleware/rules.ts +28 -9
  84. package/src/routes/admin-auth.ts +3 -3
  85. package/src/routes/admin.ts +13 -8
  86. package/src/routes/analytics-api.ts +3 -3
  87. package/src/routes/auth.ts +1 -1
  88. package/src/routes/backup.ts +1 -1
  89. package/src/routes/d1.ts +14 -7
  90. package/src/routes/database-live.ts +13 -6
  91. package/src/routes/kv.ts +21 -10
  92. package/src/routes/oauth.ts +1 -1
  93. package/src/routes/push.ts +119 -77
  94. package/src/routes/room.ts +215 -7
  95. package/src/routes/schema-endpoint.ts +2 -2
  96. package/src/routes/sql.ts +10 -6
  97. package/src/routes/storage.ts +4 -2
  98. package/src/routes/vectorize.ts +16 -4
  99. package/admin-build/_app/immutable/chunks/BYI6CUvd.js +0 -1
  100. package/admin-build/_app/immutable/chunks/C6lpZLE2.js +0 -1
  101. package/admin-build/_app/immutable/chunks/CoI6jjbg.js +0 -2
  102. package/admin-build/_app/immutable/chunks/D5GswVnI.js +0 -128
  103. package/admin-build/_app/immutable/chunks/Dj-E9-FO.js +0 -1
  104. package/admin-build/_app/immutable/chunks/g_-Kpxu3.js +0 -1
  105. package/admin-build/_app/immutable/chunks/wCNueVYy.js +0 -1
  106. package/admin-build/_app/immutable/entry/start.CtsqDyfj.js +0 -1
  107. package/admin-build/_app/immutable/nodes/1.B4sI5cB4.js +0 -1
  108. package/admin-build/_app/immutable/nodes/10.D6hvCer6.js +0 -1
  109. package/admin-build/_app/immutable/nodes/11.Dx7b8aQ5.js +0 -1
  110. package/admin-build/_app/immutable/nodes/12.Bqmy5KIF.js +0 -1
  111. package/admin-build/_app/immutable/nodes/13.CC6KpXgS.js +0 -110
  112. package/admin-build/_app/immutable/nodes/14.yCo1Ix8E.js +0 -3
  113. package/admin-build/_app/immutable/nodes/15.co0UfPlh.js +0 -1
  114. package/admin-build/_app/immutable/nodes/18.JUoLOZxh.js +0 -1
  115. package/admin-build/_app/immutable/nodes/19.ND8kmQJe.js +0 -2
  116. package/admin-build/_app/immutable/nodes/21.cz3IN9Cc.js +0 -1
  117. package/admin-build/_app/immutable/nodes/22.UOzm8WYV.js +0 -1
  118. package/admin-build/_app/immutable/nodes/24.DN9usmUs.js +0 -2
  119. package/admin-build/_app/immutable/nodes/25.BddRfAyE.js +0 -2
  120. package/admin-build/_app/immutable/nodes/26.Dl6XHIeT.js +0 -1
  121. package/admin-build/_app/immutable/nodes/27.D0iNwALG.js +0 -1
  122. package/admin-build/_app/immutable/nodes/28.9dKQmdGi.js +0 -1
  123. package/admin-build/_app/immutable/nodes/29.wXzfJUXp.js +0 -1
  124. package/admin-build/_app/immutable/nodes/30.BtZETNsL.js +0 -1
  125. package/admin-build/_app/immutable/nodes/31.CYonj2Jh.js +0 -1
  126. package/admin-build/_app/immutable/nodes/4.COtDPQ9b.js +0 -1
  127. package/admin-build/_app/immutable/nodes/5.CTRCeIhp.js +0 -1
  128. package/admin-build/_app/immutable/nodes/6.ChHi3QkR.js +0 -1
  129. package/admin-build/_app/immutable/nodes/7.CCMtr6Ac.js +0 -1
  130. package/admin-build/_app/immutable/nodes/8.DpWJ-X_-.js +0 -1
  131. package/admin-build/_app/immutable/nodes/9.DOkvfmir.js +0 -1
@@ -126,6 +126,21 @@ export async function handleD1Request(
126
126
  return c.json({ code: 405, message: 'Method not allowed' }, 405);
127
127
  }
128
128
 
129
+ function invalidD1BodyMessage(context: string): string {
130
+ return `Invalid JSON body for ${context}. Send application/json with the expected fields.`;
131
+ }
132
+
133
+ function d1RuleRejectedMessage(
134
+ tableName: string,
135
+ action: 'read' | 'insert' | 'delete' | 'list' | 'count' | 'search',
136
+ id?: string,
137
+ ): string {
138
+ if (id) {
139
+ return `Access denied. The '${action}' access rule for table '${tableName}' rejected record '${id}'.`;
140
+ }
141
+ return `Access denied. The '${action}' access rule for table '${tableName}' rejected this request.`;
142
+ }
143
+
129
144
  // ─── D1 Binding Resolution ───
130
145
 
131
146
  function resolveD1Binding(env: Env, namespace: string): D1ResolvedDb {
@@ -395,7 +410,7 @@ async function handleList(
395
410
  ): Promise<Response> {
396
411
  const tableAccess = getTableAccess(tableConfig);
397
412
  if (!isServiceKey && tableAccess?.read === false) {
398
- const error = forbiddenError('Access denied.');
413
+ const error = forbiddenError(d1RuleRejectedMessage(tableName, 'list'));
399
414
  return c.json(error.toJSON(), error.status as 403);
400
415
  }
401
416
 
@@ -478,7 +493,7 @@ async function handleCount(
478
493
  ): Promise<Response> {
479
494
  const tableAccess = getTableAccess(tableConfig);
480
495
  if (!isServiceKey && tableAccess?.read === false) {
481
- const error = forbiddenError('Access denied.');
496
+ const error = forbiddenError(d1RuleRejectedMessage(tableName, 'count'));
482
497
  return c.json(error.toJSON(), error.status as 403);
483
498
  }
484
499
 
@@ -501,7 +516,7 @@ async function handleSearch(
501
516
  ): Promise<Response> {
502
517
  const tableAccess = getTableAccess(tableConfig);
503
518
  if (!isServiceKey && tableAccess?.read === false) {
504
- const error = forbiddenError('Access denied.');
519
+ const error = forbiddenError(d1RuleRejectedMessage(tableName, 'search'));
505
520
  return c.json(error.toJSON(), error.status as 403);
506
521
  }
507
522
 
@@ -595,7 +610,7 @@ async function handleGet(
595
610
  const tableHooks = getTableHooks(tableConfig);
596
611
  if (!isServiceKey && tableAccess?.read !== undefined) {
597
612
  if (!(await evalRowRule(tableAccess.read, auth, row))) {
598
- return c.json({ code: 403, message: 'Access denied.' }, 403);
613
+ return c.json({ code: 403, message: d1RuleRejectedMessage(tableName, 'read', id) }, 403);
599
614
  }
600
615
  }
601
616
 
@@ -627,7 +642,7 @@ async function handleInsert(
627
642
  try {
628
643
  body = await c.req.json();
629
644
  } catch {
630
- return c.json({ code: 400, message: 'Invalid JSON body' }, 400);
645
+ return c.json({ code: 400, message: invalidD1BodyMessage(`inserting into table '${tableName}'`) }, 400);
631
646
  }
632
647
  body = applySchemaFieldAliases(body, tableConfig.schema);
633
648
 
@@ -636,7 +651,7 @@ async function handleInsert(
636
651
  const tableHooks = getTableHooks(tableConfig);
637
652
  if (!isServiceKey && tableAccess?.insert !== undefined) {
638
653
  if (!(await evalInsertRule(tableAccess.insert, auth))) {
639
- return c.json({ code: 403, message: 'Insert not allowed.' }, 403);
654
+ return c.json({ code: 403, message: d1RuleRejectedMessage(tableName, 'insert') }, 403);
640
655
  }
641
656
  }
642
657
 
@@ -798,14 +813,14 @@ async function handleUpdate(
798
813
  try {
799
814
  body = await c.req.json();
800
815
  } catch {
801
- return c.json({ code: 400, message: 'Invalid JSON body' }, 400);
816
+ return c.json({ code: 400, message: invalidD1BodyMessage(`updating table '${tableName}'`) }, 400);
802
817
  }
803
818
  body = applySchemaFieldAliases(body, tableConfig.schema);
804
819
 
805
820
  // Validate against schema
806
821
  const validation = validateUpdate(body, tableConfig.schema);
807
822
  if (!validation.valid) {
808
- return c.json({ code: 400, message: 'Request body failed validation. See data for field-level errors.', data: Object.fromEntries(Object.entries(validation.errors).map(([k, v]) => [k, { code: 'invalid', message: v }])) }, 400);
823
+ return c.json({ code: 400, message: `Update payload for table '${tableName}' failed validation. See data for field-level errors.`, data: Object.fromEntries(Object.entries(validation.errors).map(([k, v]) => [k, { code: 'invalid', message: v }])) }, 400);
809
824
  }
810
825
 
811
826
  // Fetch existing record to check rules
@@ -946,7 +961,7 @@ async function handleDelete(
946
961
  const tableHooks = getTableHooks(tableConfig);
947
962
  if (!isServiceKey && tableAccess?.delete !== undefined) {
948
963
  if (!(await evalRowRule(tableAccess.delete, auth, existingRow))) {
949
- return c.json({ code: 403, message: 'Delete not allowed.' }, 403);
964
+ return c.json({ code: 403, message: d1RuleRejectedMessage(tableName, 'delete', id) }, 403);
950
965
  }
951
966
  }
952
967
 
@@ -1016,7 +1031,7 @@ async function handleBatch(
1016
1031
  try {
1017
1032
  body = await c.req.json();
1018
1033
  } catch {
1019
- return c.json({ code: 400, message: 'Invalid JSON body' }, 400);
1034
+ return c.json({ code: 400, message: invalidD1BodyMessage(`batch operations on table '${tableName}'`) }, 400);
1020
1035
  }
1021
1036
 
1022
1037
  // Batch size limit: 500 total ops
@@ -1030,7 +1045,7 @@ async function handleBatch(
1030
1045
  const tableAccess = getTableAccess(tableConfig);
1031
1046
  if (!isServiceKey && body.inserts?.length && tableAccess?.insert !== undefined) {
1032
1047
  if (!(await evalInsertRule(tableAccess.insert, auth))) {
1033
- return c.json({ code: 403, message: 'Insert not allowed.' }, 403);
1048
+ return c.json({ code: 403, message: d1RuleRejectedMessage(tableName, 'insert') }, 403);
1034
1049
  }
1035
1050
  }
1036
1051
 
@@ -1059,7 +1074,7 @@ async function handleBatch(
1059
1074
  for (const item of body.inserts) {
1060
1075
  const validation = validateInsert(item, tableConfig.schema);
1061
1076
  if (!validation.valid) {
1062
- return c.json({ code: 400, message: 'Batch insert request failed validation. See data for field-level errors.', data: Object.fromEntries(Object.entries(validation.errors).map(([k, v]) => [k, { code: 'invalid', message: v }])) }, 400);
1077
+ return c.json({ code: 400, message: `Batch insert payload for table '${tableName}' failed validation. See data for field-level errors.`, data: Object.fromEntries(Object.entries(validation.errors).map(([k, v]) => [k, { code: 'invalid', message: v }])) }, 400);
1063
1078
  }
1064
1079
  }
1065
1080
  }
@@ -1072,14 +1087,14 @@ async function handleBatch(
1072
1087
  }));
1073
1088
  for (const entry of body.updates) {
1074
1089
  if (!entry.id) {
1075
- return c.json({ code: 400, message: 'Each batch update entry must include an id.' }, 400);
1090
+ return c.json({ code: 400, message: `Each batch update entry for table '${tableName}' must include an id.` }, 400);
1076
1091
  }
1077
1092
  if (!entry.data || typeof entry.data !== 'object') {
1078
- return c.json({ code: 400, message: 'Each batch update entry must include a data object.' }, 400);
1093
+ return c.json({ code: 400, message: `Each batch update entry for table '${tableName}' must include a data object.` }, 400);
1079
1094
  }
1080
1095
  const validation = validateUpdate(entry.data, tableConfig.schema);
1081
1096
  if (!validation.valid) {
1082
- return c.json({ code: 400, message: 'Batch update request failed validation. See data for field-level errors.', data: Object.fromEntries(Object.entries(validation.errors).map(([k, v]) => [k, { code: 'invalid', message: v }])) }, 400);
1097
+ return c.json({ code: 400, message: `Batch update payload for table '${tableName}' failed validation. See data for field-level errors.`, data: Object.fromEntries(Object.entries(validation.errors).map(([k, v]) => [k, { code: 'invalid', message: v }])) }, 400);
1083
1098
  }
1084
1099
  }
1085
1100
  }
@@ -1087,7 +1102,7 @@ async function handleBatch(
1087
1102
  // Check delete rules (table-level)
1088
1103
  if (!isServiceKey && body.deletes?.length && tableAccess?.delete !== undefined) {
1089
1104
  if (!(await evalRowRule(tableAccess.delete, auth, {}))) {
1090
- return c.json({ code: 403, message: 'Delete not allowed.' }, 403);
1105
+ return c.json({ code: 403, message: d1RuleRejectedMessage(tableName, 'delete') }, 403);
1091
1106
  }
1092
1107
  }
1093
1108
 
@@ -1249,7 +1264,7 @@ async function handleBatchByFilter(
1249
1264
  try {
1250
1265
  body = await c.req.json();
1251
1266
  } catch {
1252
- return c.json({ code: 400, message: 'Invalid JSON body' }, 400);
1267
+ return c.json({ code: 400, message: invalidD1BodyMessage(`batch-by-filter on table '${tableName}'`) }, 400);
1253
1268
  }
1254
1269
 
1255
1270
  if (!body.action || !['delete', 'update'].includes(body.action)) {
@@ -61,6 +61,18 @@ interface PgResolvedDb {
61
61
  namespace: string;
62
62
  }
63
63
 
64
+ function postgresInvalidJsonMessage(context: string, tableName: string): string {
65
+ return `Invalid JSON body for ${context} on table '${tableName}'. Send application/json with the expected fields.`;
66
+ }
67
+
68
+ function postgresRuleRejectedMessage(tableName: string, action: 'insert' | 'update' | 'delete'): string {
69
+ return `Access denied by access rules for ${action} on table '${tableName}'.`;
70
+ }
71
+
72
+ function postgresValidationMessage(context: string, tableName: string): string {
73
+ return `Validation failed for ${context} on table '${tableName}'. See data for field-level errors.`;
74
+ }
75
+
64
76
  // ─── Main Handler ───
65
77
 
66
78
  /**
@@ -526,7 +538,7 @@ async function handleInsert(
526
538
  try {
527
539
  body = await c.req.json();
528
540
  } catch {
529
- return c.json({ code: 400, message: 'Invalid JSON body' }, 400);
541
+ return c.json({ code: 400, message: postgresInvalidJsonMessage('insert', tableName) }, 400);
530
542
  }
531
543
 
532
544
  // Check insert rule
@@ -534,7 +546,7 @@ async function handleInsert(
534
546
  const tableHooks = getTableHooks(tableConfig);
535
547
  if (!isServiceKey && tableAccess?.insert !== undefined) {
536
548
  if (!(await evalInsertRule(tableAccess.insert, auth))) {
537
- return c.json({ code: 403, message: 'Insert not allowed.' }, 403);
549
+ return c.json({ code: 403, message: postgresRuleRejectedMessage(tableName, 'insert') }, 403);
538
550
  }
539
551
  }
540
552
 
@@ -663,7 +675,7 @@ async function handleUpdate(
663
675
  try {
664
676
  body = await c.req.json();
665
677
  } catch {
666
- return c.json({ code: 400, message: 'Invalid JSON body' }, 400);
678
+ return c.json({ code: 400, message: postgresInvalidJsonMessage(`update record '${id}'`, tableName) }, 400);
667
679
  }
668
680
 
669
681
  // Validate against schema
@@ -671,7 +683,7 @@ async function handleUpdate(
671
683
  if (!validation.valid) {
672
684
  return c.json({
673
685
  code: 400,
674
- message: 'Validation failed.',
686
+ message: postgresValidationMessage(`update record '${id}'`, tableName),
675
687
  data: toFieldErrorData(validation.errors),
676
688
  errors: validation.errors,
677
689
  }, 400);
@@ -690,7 +702,7 @@ async function handleUpdate(
690
702
  const tableHooks = getTableHooks(tableConfig);
691
703
  if (!isServiceKey && tableAccess?.update !== undefined) {
692
704
  if (!(await evalRowRule(tableAccess.update, auth, existingRow))) {
693
- return c.json({ code: 403, message: 'Update not allowed.' }, 403);
705
+ return c.json({ code: 403, message: postgresRuleRejectedMessage(tableName, 'update') }, 403);
694
706
  }
695
707
  }
696
708
 
@@ -792,7 +804,7 @@ async function handleDelete(
792
804
  const tableHooks = getTableHooks(tableConfig);
793
805
  if (!isServiceKey && tableAccess?.delete !== undefined) {
794
806
  if (!(await evalRowRule(tableAccess.delete, auth, existingRow))) {
795
- return c.json({ code: 403, message: 'Delete not allowed.' }, 403);
807
+ return c.json({ code: 403, message: postgresRuleRejectedMessage(tableName, 'delete') }, 403);
796
808
  }
797
809
  }
798
810
 
@@ -870,7 +882,7 @@ async function handleBatch(
870
882
  try {
871
883
  body = await c.req.json();
872
884
  } catch {
873
- return c.json({ code: 400, message: 'Invalid JSON body' }, 400);
885
+ return c.json({ code: 400, message: postgresInvalidJsonMessage('batch insert', tableName) }, 400);
874
886
  }
875
887
 
876
888
  const inserts = Array.isArray(body.inserts)
@@ -901,7 +913,7 @@ async function handleBatch(
901
913
  const tableAccess = getTableAccess(tableConfig);
902
914
  if (!isServiceKey && inserts.length > 0 && tableAccess?.insert !== undefined) {
903
915
  if (!(await evalInsertRule(tableAccess.insert, auth))) {
904
- return c.json({ code: 403, message: 'Insert not allowed.' }, 403);
916
+ return c.json({ code: 403, message: postgresRuleRejectedMessage(tableName, 'insert') }, 403);
905
917
  }
906
918
  }
907
919
 
@@ -915,7 +927,7 @@ async function handleBatch(
915
927
  if (!validation.valid) {
916
928
  return c.json({
917
929
  code: 400,
918
- message: 'Validation failed.',
930
+ message: postgresValidationMessage('batch insert', tableName),
919
931
  data: toFieldErrorData(validation.errors),
920
932
  errors: validation.errors,
921
933
  }, 400);
@@ -1000,7 +1012,7 @@ async function handleBatchByFilter(
1000
1012
  try {
1001
1013
  body = await c.req.json();
1002
1014
  } catch {
1003
- return c.json({ code: 400, message: 'Invalid JSON body' }, 400);
1015
+ return c.json({ code: 400, message: postgresInvalidJsonMessage('batch-by-filter', tableName) }, 400);
1004
1016
  }
1005
1017
 
1006
1018
  if (!body.action || !['delete', 'update'].includes(body.action)) {
@@ -1038,7 +1050,7 @@ async function handleBatchByFilter(
1038
1050
  const tableAccess = getTableAccess(tableConfig);
1039
1051
  if (!isServiceKey && tableAccess?.delete !== undefined) {
1040
1052
  if (typeof tableAccess.delete === 'boolean' && !tableAccess.delete) {
1041
- return c.json({ code: 403, message: 'Delete not allowed.' }, 403);
1053
+ return c.json({ code: 403, message: postgresRuleRejectedMessage(tableName, 'delete') }, 403);
1042
1054
  }
1043
1055
  }
1044
1056
 
@@ -1071,7 +1083,7 @@ async function handleBatchByFilter(
1071
1083
  const tableAccess = getTableAccess(tableConfig);
1072
1084
  if (!isServiceKey && tableAccess?.update !== undefined) {
1073
1085
  if (typeof tableAccess.update === 'boolean' && !tableAccess.update) {
1074
- return c.json({ code: 403, message: 'Update not allowed.' }, 403);
1086
+ return c.json({ code: 403, message: postgresRuleRejectedMessage(tableName, 'update') }, 403);
1075
1087
  }
1076
1088
  }
1077
1089
 
@@ -360,6 +360,9 @@ export function parseRoute(method: string, path: string): ParsedRoute {
360
360
  if (segments[2] === 'metadata') {
361
361
  result.subcategory = 'metadata';
362
362
  result.operation = 'getMetadata';
363
+ } else if (segments[2] === 'summary') {
364
+ result.subcategory = 'summary';
365
+ result.operation = 'getSummary';
363
366
  } else if (segments[2] === 'connect-check') {
364
367
  result.subcategory = 'connect-check';
365
368
  result.operation = 'connectCheck';
@@ -0,0 +1,53 @@
1
+ // Compile-time constant — injected by wrangler [define] in wrangler.test.toml
2
+ declare const EDGEBASE_TEST_BUILD: boolean | undefined;
3
+
4
+ let startupPromise: Promise<void> | null = null;
5
+
6
+ async function detectWorkersTestRuntime(): Promise<boolean> {
7
+ try {
8
+ await import('cloudflare:test');
9
+ return true;
10
+ } catch {
11
+ return false;
12
+ }
13
+ }
14
+
15
+ export async function ensureServerStartup(): Promise<void> {
16
+ if (startupPromise) {
17
+ return startupPromise;
18
+ }
19
+
20
+ startupPromise = (async () => {
21
+ const [{ resolveStartupConfig }, generatedConfigModule, { initFunctionRegistry }, doRouterModule] = await Promise.all([
22
+ import('./startup-config.js'),
23
+ import('../generated-config.js'),
24
+ import('../_functions-registry.js'),
25
+ import('./do-router.js'),
26
+ ]);
27
+
28
+ try {
29
+ const processEnv = typeof process !== 'undefined' ? process.env : undefined;
30
+ const isTestBuild = typeof EDGEBASE_TEST_BUILD !== 'undefined';
31
+ const preferTestConfig = await detectWorkersTestRuntime() || isTestBuild;
32
+ const existingConfig = doRouterModule.parseConfig();
33
+ const resolvedConfig = await resolveStartupConfig(
34
+ generatedConfigModule.default,
35
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
36
+ async () => import('../../edgebase.test.config.ts' as any),
37
+ processEnv,
38
+ { preferTestConfig },
39
+ );
40
+
41
+ if (resolvedConfig && Object.keys(existingConfig).length === 0) {
42
+ doRouterModule.setConfig(resolvedConfig);
43
+ }
44
+ } catch (err) {
45
+ console.error('[EdgeBase] Failed to initialize config at startup:', err);
46
+ throw err;
47
+ }
48
+
49
+ initFunctionRegistry();
50
+ })();
51
+
52
+ return startupPromise;
53
+ }
@@ -139,13 +139,23 @@ export const trackEventsBodySchema = z.object({
139
139
  * Returns Zod validation errors in the standard { code, message } format.
140
140
  */
141
141
  /* eslint-disable @typescript-eslint/no-explicit-any */
142
- export function zodDefaultHook(result: { success: boolean; error?: { issues?: Array<{ message: string }>; errors?: Array<{ message: string }> } }, c: any) {
142
+ function formatZodIssue(issue: { message?: string; path?: Array<string | number> }): string {
143
+ const message = typeof issue.message === 'string' ? issue.message : 'Invalid value';
144
+ const path = Array.isArray(issue.path) && issue.path.length > 0
145
+ ? issue.path.map((segment) => typeof segment === 'number' ? `[${segment}]` : String(segment)).join('.').replace(/\.\[/g, '[')
146
+ : '';
147
+ return path ? `${path}: ${message}` : message;
148
+ }
149
+
150
+ export function zodDefaultHook(result: { success: boolean; error?: { issues?: Array<{ message: string; path?: Array<string | number> }>; errors?: Array<{ message: string; path?: Array<string | number> }> } }, c: any) {
143
151
  if (!result.success) {
144
152
  // Zod v4 uses `issues`, Zod v3 uses `errors`
145
153
  const items = (result.error as any)?.issues ?? (result.error as any)?.errors ?? [];
146
154
  return c.json({
147
155
  code: 400,
148
- message: items.map((e: any) => e.message).join(', '),
156
+ message: items.length > 0
157
+ ? items.map((e: any) => formatZodIssue(e)).join(', ')
158
+ : 'Request validation failed.',
149
159
  }, 400);
150
160
  }
151
161
  }
@@ -18,6 +18,7 @@ interface CaptchaConfig {
18
18
  }
19
19
 
20
20
  type HonoContext = Context<{ Bindings: Env }>;
21
+ const captchaWarnings = new Set<string>();
21
22
 
22
23
  interface SiteverifyResponse {
23
24
  success: boolean;
@@ -139,7 +140,14 @@ export function captchaMiddleware(expectedAction: string) {
139
140
  try {
140
141
  const config = parseConfig(c.env);
141
142
  if (config?.captcha === true) {
142
- console.warn('⚠️ Captcha skipped: no Turnstile keys provisioned.');
143
+ if (!captchaWarnings.has('missing-turnstile-keys')) {
144
+ captchaWarnings.add('missing-turnstile-keys');
145
+ console.warn(
146
+ '[Auth] CAPTCHA is enabled, but Turnstile keys are missing. '
147
+ + 'Requests will continue without CAPTCHA in local dev. '
148
+ + 'Add captchaSiteKey and TURNSTILE_SECRET, or set captcha: false to silence this warning.',
149
+ );
150
+ }
143
151
  }
144
152
  } catch { /* ignore */ }
145
153
  await next();
@@ -170,7 +178,13 @@ export function captchaMiddleware(expectedAction: string) {
170
178
  // Handle siteverify API failure (timeout, network error)
171
179
  if (result['error-codes']?.includes('timeout-or-network-error')) {
172
180
  if (failMode === 'open') {
173
- console.warn('⚠️ Turnstile siteverify failed (timeout/network). Allowing request (failMode=open).');
181
+ if (!captchaWarnings.has('siteverify-fail-open')) {
182
+ captchaWarnings.add('siteverify-fail-open');
183
+ console.warn(
184
+ '[Auth] Turnstile siteverify failed because of a timeout or network error. '
185
+ + 'The request is being allowed because captcha.failMode is set to "open".',
186
+ );
187
+ }
174
188
  await next();
175
189
  return;
176
190
  }
@@ -214,4 +228,3 @@ export const _test = {
214
228
  hasServiceKey,
215
229
  siteverify,
216
230
  };
217
-
@@ -45,7 +45,7 @@ export const errorHandlerMiddleware: MiddlewareHandler<HonoEnv> = async (c, next
45
45
  return c.json(
46
46
  {
47
47
  code: 500,
48
- message: 'Internal server error.',
48
+ message: `Internal server error while handling '${c.req.path}'. Check the worker logs for the original exception.`,
49
49
  slug: 'internal-error',
50
50
  },
51
51
  500,
@@ -41,6 +41,13 @@ type HonoContext = Context<{ Bindings: Env }>;
41
41
  const WORKER_RULE_TIMEOUT_MS = 50;
42
42
  const DB_ACCESS_RULE_TIMEOUT_MS = 2000;
43
43
 
44
+ function tableRuleRejected(tableName: string, action: string): EdgeBaseError {
45
+ return new EdgeBaseError(
46
+ 403,
47
+ `Access denied. The '${action}' access rule for table '${tableName}' rejected this request.`,
48
+ );
49
+ }
50
+
44
51
  /**
45
52
  * Normalize a raw rule value (function | boolean | string) into a callable.
46
53
  *
@@ -174,7 +181,7 @@ export async function rulesMiddleware(c: HonoContext, next: Next): Promise<Respo
174
181
  return next();
175
182
  }
176
183
  if (keyResult === 'invalid') {
177
- throw new EdgeBaseError(401, 'Unauthorized. Invalid Service Key.');
184
+ throw new EdgeBaseError(401, `Invalid X-EdgeBase-Service-Key for scope '${requiredScope}'.`);
178
185
  }
179
186
  // keyResult === 'missing' → continue to normal rules evaluation
180
187
 
@@ -182,7 +189,10 @@ export async function rulesMiddleware(c: HonoContext, next: Next): Promise<Respo
182
189
  if (!config.databases) {
183
190
  // No databases config → release mode check
184
191
  if (!config.release) return next();
185
- throw new EdgeBaseError(403, `Access denied. No databases config defined.`);
192
+ throw new EdgeBaseError(
193
+ 403,
194
+ 'Access denied. No databases config is defined for this server. Add config.databases or set release: false while developing locally.',
195
+ );
186
196
  }
187
197
 
188
198
  // namespace comes directly from the URL (/api/db/:namespace/...)
@@ -192,13 +202,19 @@ export async function rulesMiddleware(c: HonoContext, next: Next): Promise<Respo
192
202
  if (!dbBlock) {
193
203
  // Namespace not found in config → deny
194
204
  if (!config.release) return next();
195
- throw new EdgeBaseError(403, `Access denied. Namespace '${tableNamespace}' is not configured.`);
205
+ throw new EdgeBaseError(
206
+ 403,
207
+ `Access denied. Namespace '${tableNamespace}' is not configured. Check the API path or add this namespace to config.databases.`,
208
+ );
196
209
  }
197
210
 
198
211
  if (dbBlock.tables && !dbBlock.tables[tableName]) {
199
212
  // Table not defined in this DB block
200
213
  if (!config.release) return next();
201
- throw new EdgeBaseError(403, `Access denied. Table '${tableName}' has no access rules defined.`);
214
+ throw new EdgeBaseError(
215
+ 403,
216
+ `Access denied. Table '${tableName}' is missing access rules. Add access.* rules or disable release mode for local-only development.`,
217
+ );
202
218
  }
203
219
 
204
220
  // ── Step 3: DB-level access check (§4) ──
@@ -232,7 +248,10 @@ export async function rulesMiddleware(c: HonoContext, next: Next): Promise<Respo
232
248
 
233
249
  if (!tableRules) {
234
250
  if (!config.release) return next();
235
- throw new EdgeBaseError(403, `Access denied. No access rules defined for '${tableName}'.`);
251
+ throw new EdgeBaseError(
252
+ 403,
253
+ `Access denied. No access rules are defined for table '${tableName}'. Add access.* rules or disable release mode for local-only development.`,
254
+ );
236
255
  }
237
256
 
238
257
  // Determine action
@@ -259,7 +278,7 @@ export async function rulesMiddleware(c: HonoContext, next: Next): Promise<Respo
259
278
  : !config.release;
260
279
 
261
280
  if (!insertPass && !updatePass) {
262
- throw new EdgeBaseError(403, 'Access denied by access rules.');
281
+ throw tableRuleRejected(tableName, 'upsert');
263
282
  }
264
283
  return next();
265
284
  }
@@ -274,9 +293,9 @@ export async function rulesMiddleware(c: HonoContext, next: Next): Promise<Respo
274
293
  }
275
294
  if (insertRuleFn) {
276
295
  const canInsert = await evalWithTimeout(() => insertRuleFn(auth), WORKER_RULE_TIMEOUT_MS);
277
- if (!canInsert) throw new EdgeBaseError(403, 'Access denied by access rules.');
296
+ if (!canInsert) throw tableRuleRejected(tableName, 'batch insert');
278
297
  } else if (config.release) {
279
- throw new EdgeBaseError(403, 'Access denied by access rules.');
298
+ throw tableRuleRejected(tableName, 'batch insert');
280
299
  }
281
300
  }
282
301
  return next();
@@ -292,7 +311,7 @@ export async function rulesMiddleware(c: HonoContext, next: Next): Promise<Respo
292
311
  throw new EdgeBaseError(403, `Access denied. No 'insert' rule defined for '${tableName}'.`);
293
312
  }
294
313
  const canInsert = await evalWithTimeout(() => insertRuleFn(auth), WORKER_RULE_TIMEOUT_MS);
295
- if (!canInsert) throw new EdgeBaseError(403, 'Access denied by access rules.');
314
+ if (!canInsert) throw tableRuleRejected(tableName, 'insert');
296
315
  return next();
297
316
  }
298
317
 
@@ -51,7 +51,7 @@ adminAuthRoute.onError((err, c) => {
51
51
  return c.json(err.toJSON(), err.code as 400);
52
52
  }
53
53
  console.error('Admin Auth unhandled error:', err);
54
- return c.json({ code: 500, message: 'Internal server error.' }, 500);
54
+ return c.json({ code: 500, message: 'Admin auth request failed unexpectedly. Check the worker logs for the original exception.' }, 500);
55
55
  });
56
56
 
57
57
  // Service Key middleware — scoped validation
@@ -65,10 +65,10 @@ adminAuthRoute.use('*', async (c, next) => {
65
65
  const provided = explicitServiceKey ?? resolveServiceKeyCandidate(c.req, extractBearerToken(c.req));
66
66
  const { result } = validateKey(provided, 'auth:admin:*:*', config, c.env, undefined, buildConstraintCtx(c.env, c.req));
67
67
  if (result === 'missing') {
68
- throw new EdgeBaseError(403, 'Service Key required for admin auth operations.');
68
+ throw new EdgeBaseError(403, 'X-EdgeBase-Service-Key is required for admin auth operations.');
69
69
  }
70
70
  if (result === 'invalid') {
71
- throw new EdgeBaseError(401, 'Unauthorized. Service Key required.');
71
+ throw new EdgeBaseError(401, 'Invalid X-EdgeBase-Service-Key for admin auth operations.');
72
72
  }
73
73
  await ensureAuthSchema(getAuthDb(c));
74
74
  await next();
@@ -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),