@edge-base/server 0.1.1

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 (309) hide show
  1. package/admin-build/.gitkeep +0 -0
  2. package/admin-build/_app/env.js +1 -0
  3. package/admin-build/_app/immutable/assets/0.Bm6cF078.css +1 -0
  4. package/admin-build/_app/immutable/assets/1.BfW3pUNa.css +1 -0
  5. package/admin-build/_app/immutable/assets/11.CVmQOewb.css +1 -0
  6. package/admin-build/_app/immutable/assets/12.B1EhbRZT.css +1 -0
  7. package/admin-build/_app/immutable/assets/13.BvwYeuwE.css +1 -0
  8. package/admin-build/_app/immutable/assets/14.CdVfcO0R.css +1 -0
  9. package/admin-build/_app/immutable/assets/15.2yeZ66b-.css +1 -0
  10. package/admin-build/_app/immutable/assets/17.BVg0JEVu.css +1 -0
  11. package/admin-build/_app/immutable/assets/18.Rwnl3x_i.css +1 -0
  12. package/admin-build/_app/immutable/assets/20.DsPWA9AV.css +1 -0
  13. package/admin-build/_app/immutable/assets/21.Dz2RJ56c.css +1 -0
  14. package/admin-build/_app/immutable/assets/22.DwNLk5Ai.css +1 -0
  15. package/admin-build/_app/immutable/assets/23.CFpu0gOO.css +1 -0
  16. package/admin-build/_app/immutable/assets/24.Cy5LBeoJ.css +1 -0
  17. package/admin-build/_app/immutable/assets/25.pUyLVf-h.css +1 -0
  18. package/admin-build/_app/immutable/assets/26.DBcGrlXa.css +1 -0
  19. package/admin-build/_app/immutable/assets/27.BswYyAJD.css +1 -0
  20. package/admin-build/_app/immutable/assets/28.B4ueB1Kf.css +1 -0
  21. package/admin-build/_app/immutable/assets/29.B-qU6PdF.css +1 -0
  22. package/admin-build/_app/immutable/assets/3.Dg81Pgmd.css +1 -0
  23. package/admin-build/_app/immutable/assets/30.CsdWum94.css +1 -0
  24. package/admin-build/_app/immutable/assets/31.U6OwIp50.css +1 -0
  25. package/admin-build/_app/immutable/assets/4.CyawCCux.css +1 -0
  26. package/admin-build/_app/immutable/assets/5.C0YO2HTk.css +1 -0
  27. package/admin-build/_app/immutable/assets/8.Br5jd6kD.css +1 -0
  28. package/admin-build/_app/immutable/assets/Badge.EMYLHBxE.css +1 -0
  29. package/admin-build/_app/immutable/assets/Button.DpzMRTjK.css +1 -0
  30. package/admin-build/_app/immutable/assets/ConfirmDialog.DAnaWRRk.css +1 -0
  31. package/admin-build/_app/immutable/assets/EmptyState.CwKsu57Y.css +1 -0
  32. package/admin-build/_app/immutable/assets/Input.BDUSenmU.css +1 -0
  33. package/admin-build/_app/immutable/assets/Modal.Dm5B0Xie.css +1 -0
  34. package/admin-build/_app/immutable/assets/PageShell.CmU-Xh-b.css +1 -0
  35. package/admin-build/_app/immutable/assets/SchemaFieldEditor.g4NsCdno.css +1 -0
  36. package/admin-build/_app/immutable/assets/Select.BW4Keufm.css +1 -0
  37. package/admin-build/_app/immutable/assets/Skeleton.KWUulTKJ.css +1 -0
  38. package/admin-build/_app/immutable/assets/Tabs.CniGYb67.css +1 -0
  39. package/admin-build/_app/immutable/assets/TimeChart.BTCDAvmT.css +1 -0
  40. package/admin-build/_app/immutable/assets/Toggle.Cy_K12OM.css +1 -0
  41. package/admin-build/_app/immutable/assets/TopList.ClFzmPlA.css +1 -0
  42. package/admin-build/_app/immutable/chunks/7B47DvSx.js +1 -0
  43. package/admin-build/_app/immutable/chunks/7f08Id8e.js +1 -0
  44. package/admin-build/_app/immutable/chunks/8wJeQ7LN.js +1 -0
  45. package/admin-build/_app/immutable/chunks/B-h2afW5.js +1 -0
  46. package/admin-build/_app/immutable/chunks/B8vJP3wz.js +1 -0
  47. package/admin-build/_app/immutable/chunks/BR_fL5Yv.js +1 -0
  48. package/admin-build/_app/immutable/chunks/BY92tFS2.js +1 -0
  49. package/admin-build/_app/immutable/chunks/BcR-Rdj9.js +1 -0
  50. package/admin-build/_app/immutable/chunks/BdrwyZv8.js +1 -0
  51. package/admin-build/_app/immutable/chunks/Bh56EfQ_.js +1 -0
  52. package/admin-build/_app/immutable/chunks/BkrCkgYp.js +1 -0
  53. package/admin-build/_app/immutable/chunks/BmRjiP5k.js +1 -0
  54. package/admin-build/_app/immutable/chunks/BsokvhWC.js +1 -0
  55. package/admin-build/_app/immutable/chunks/C4D51vTW.js +1 -0
  56. package/admin-build/_app/immutable/chunks/C6puvcoR.js +2 -0
  57. package/admin-build/_app/immutable/chunks/CCKNu7m7.js +1 -0
  58. package/admin-build/_app/immutable/chunks/CWj6FrbW.js +1 -0
  59. package/admin-build/_app/immutable/chunks/Ce-ngf4p.js +5 -0
  60. package/admin-build/_app/immutable/chunks/Cs0GwzJA.js +1 -0
  61. package/admin-build/_app/immutable/chunks/CwROoZK0.js +1 -0
  62. package/admin-build/_app/immutable/chunks/CxCPv_Ut.js +1 -0
  63. package/admin-build/_app/immutable/chunks/CxbRue-5.js +1 -0
  64. package/admin-build/_app/immutable/chunks/CyqB6g-D.js +1 -0
  65. package/admin-build/_app/immutable/chunks/D5h5A1cc.js +2 -0
  66. package/admin-build/_app/immutable/chunks/DnyL7Zq-.js +1 -0
  67. package/admin-build/_app/immutable/chunks/DoPXzH7F.js +1 -0
  68. package/admin-build/_app/immutable/chunks/DrQSgw-f.js +1 -0
  69. package/admin-build/_app/immutable/chunks/DttM2zNO.js +1 -0
  70. package/admin-build/_app/immutable/chunks/DuXuUBWN.js +1 -0
  71. package/admin-build/_app/immutable/chunks/MdeqaOQx.js +10 -0
  72. package/admin-build/_app/immutable/chunks/NuUjtcO2.js +1 -0
  73. package/admin-build/_app/immutable/chunks/Q2nPFxS6.js +1 -0
  74. package/admin-build/_app/immutable/chunks/R6arueIl.js +1 -0
  75. package/admin-build/_app/immutable/chunks/UUazaC_N.js +1 -0
  76. package/admin-build/_app/immutable/chunks/cOYbrQxx.js +1 -0
  77. package/admin-build/_app/immutable/chunks/eFQHTGwA.js +1 -0
  78. package/admin-build/_app/immutable/chunks/ehbppgYb.js +1 -0
  79. package/admin-build/_app/immutable/chunks/glwixJlP.js +1 -0
  80. package/admin-build/_app/immutable/chunks/vApWTCBs.js +1 -0
  81. package/admin-build/_app/immutable/chunks/w89G9Xpi.js +1 -0
  82. package/admin-build/_app/immutable/chunks/wJsUhbfZ.js +1 -0
  83. package/admin-build/_app/immutable/chunks/zfauFM8P.js +1 -0
  84. package/admin-build/_app/immutable/entry/app.CcO-Uos3.js +2 -0
  85. package/admin-build/_app/immutable/entry/start.COebYq3I.js +1 -0
  86. package/admin-build/_app/immutable/nodes/0.CjtHKU-6.js +1 -0
  87. package/admin-build/_app/immutable/nodes/1.DEisjlM0.js +1 -0
  88. package/admin-build/_app/immutable/nodes/10.CvhdyWVB.js +1 -0
  89. package/admin-build/_app/immutable/nodes/11.DjHqcOvy.js +1 -0
  90. package/admin-build/_app/immutable/nodes/12.mQLz4Mj_.js +1 -0
  91. package/admin-build/_app/immutable/nodes/13.CBonZZyP.js +110 -0
  92. package/admin-build/_app/immutable/nodes/14.d-oiZL0j.js +3 -0
  93. package/admin-build/_app/immutable/nodes/15.CKPQsUYF.js +1 -0
  94. package/admin-build/_app/immutable/nodes/16.wPzAPQGx.js +1 -0
  95. package/admin-build/_app/immutable/nodes/17.DayhKyEZ.js +1 -0
  96. package/admin-build/_app/immutable/nodes/18.DKwS0Ir0.js +1 -0
  97. package/admin-build/_app/immutable/nodes/19.wPzAPQGx.js +1 -0
  98. package/admin-build/_app/immutable/nodes/2.BKoKrw1i.js +1 -0
  99. package/admin-build/_app/immutable/nodes/20.BvIkkkrW.js +1 -0
  100. package/admin-build/_app/immutable/nodes/21.DMaFhdHk.js +128 -0
  101. package/admin-build/_app/immutable/nodes/22.3xdgwuK1.js +1 -0
  102. package/admin-build/_app/immutable/nodes/23.8Bvgjbsl.js +112 -0
  103. package/admin-build/_app/immutable/nodes/24.DzSSzRhG.js +2 -0
  104. package/admin-build/_app/immutable/nodes/25.9KKYBnAE.js +2 -0
  105. package/admin-build/_app/immutable/nodes/26.Bhn9dfhY.js +1 -0
  106. package/admin-build/_app/immutable/nodes/27.kRLiC24G.js +1 -0
  107. package/admin-build/_app/immutable/nodes/28.BVIN1-7N.js +1 -0
  108. package/admin-build/_app/immutable/nodes/29.3yabZWj4.js +1 -0
  109. package/admin-build/_app/immutable/nodes/3.BFtSOkX7.js +2 -0
  110. package/admin-build/_app/immutable/nodes/30.CyCQlwaP.js +1 -0
  111. package/admin-build/_app/immutable/nodes/31.C4LDXjES.js +1 -0
  112. package/admin-build/_app/immutable/nodes/4.CvbiMlCa.js +1 -0
  113. package/admin-build/_app/immutable/nodes/5.C6BLv2eM.js +1 -0
  114. package/admin-build/_app/immutable/nodes/6.BcXvfl2P.js +1 -0
  115. package/admin-build/_app/immutable/nodes/7.CIuqhPiK.js +1 -0
  116. package/admin-build/_app/immutable/nodes/8.BQOR_JfO.js +1 -0
  117. package/admin-build/_app/immutable/nodes/9.NZqXQxPy.js +1 -0
  118. package/admin-build/_app/version.json +1 -0
  119. package/admin-build/favicon.svg +26 -0
  120. package/admin-build/index.html +45 -0
  121. package/openapi.json +19543 -0
  122. package/package.json +66 -0
  123. package/src/__tests__/admin-assets.test.ts +55 -0
  124. package/src/__tests__/admin-data-routes.test.ts +488 -0
  125. package/src/__tests__/admin-db-target.test.ts +103 -0
  126. package/src/__tests__/admin-routing.test.ts +31 -0
  127. package/src/__tests__/admin-user-management.test.ts +311 -0
  128. package/src/__tests__/analytics-query.test.ts +75 -0
  129. package/src/__tests__/auth-d1.test.ts +749 -0
  130. package/src/__tests__/auth-db-adapter.test.ts +73 -0
  131. package/src/__tests__/auth-jwt.test.ts +440 -0
  132. package/src/__tests__/auth-oauth.test.ts +389 -0
  133. package/src/__tests__/auth-password.test.ts +367 -0
  134. package/src/__tests__/auth-redirect.test.ts +87 -0
  135. package/src/__tests__/backup-restore.test.ts +711 -0
  136. package/src/__tests__/broadcast.test.ts +128 -0
  137. package/src/__tests__/cli.test.ts +178 -0
  138. package/src/__tests__/cloudflare-realtime.test.ts +113 -0
  139. package/src/__tests__/config.test.ts +469 -0
  140. package/src/__tests__/cors.test.ts +154 -0
  141. package/src/__tests__/cron.test.ts +302 -0
  142. package/src/__tests__/d1-handler.test.ts +402 -0
  143. package/src/__tests__/d1-sql.test.ts +120 -0
  144. package/src/__tests__/database-live-config.test.ts +42 -0
  145. package/src/__tests__/database-live-emitter.test.ts +56 -0
  146. package/src/__tests__/database-live-filters.test.ts +63 -0
  147. package/src/__tests__/database-live-route.test.ts +113 -0
  148. package/src/__tests__/db-sql.test.ts +163 -0
  149. package/src/__tests__/do-lifecycle.test.ts +263 -0
  150. package/src/__tests__/do-router.test.ts +729 -0
  151. package/src/__tests__/email-provider.test.ts +128 -0
  152. package/src/__tests__/email-templates.test.ts +528 -0
  153. package/src/__tests__/error-format.test.ts +250 -0
  154. package/src/__tests__/field-ops.test.ts +242 -0
  155. package/src/__tests__/functions-context.test.ts +334 -0
  156. package/src/__tests__/functions-d1-proxy.test.ts +229 -0
  157. package/src/__tests__/functions-registry-runtime-config.test.ts +17 -0
  158. package/src/__tests__/functions-route.test.ts +139 -0
  159. package/src/__tests__/internal-request.test.ts +77 -0
  160. package/src/__tests__/log-writer.test.ts +44 -0
  161. package/src/__tests__/logger.test.ts +58 -0
  162. package/src/__tests__/meta-admin-proxy.test.ts +48 -0
  163. package/src/__tests__/meta-export-coverage.test.ts +191 -0
  164. package/src/__tests__/meta-route-registration.test.ts +47 -0
  165. package/src/__tests__/namespace-dump.test.ts +28 -0
  166. package/src/__tests__/oauth-providers.test.ts +337 -0
  167. package/src/__tests__/openapi-coverage.test.ts +144 -0
  168. package/src/__tests__/pagination.test.ts +59 -0
  169. package/src/__tests__/password-policy.test.ts +191 -0
  170. package/src/__tests__/plugin-migrations.test.ts +379 -0
  171. package/src/__tests__/postgres-batch-compat.test.ts +133 -0
  172. package/src/__tests__/postgres-dialect.test.ts +328 -0
  173. package/src/__tests__/postgres-executor.test.ts +79 -0
  174. package/src/__tests__/postgres-field-ops-compat.test.ts +222 -0
  175. package/src/__tests__/postgres-schema-init.test.ts +105 -0
  176. package/src/__tests__/postgres-table-utils.test.ts +107 -0
  177. package/src/__tests__/presence.test.ts +199 -0
  178. package/src/__tests__/provider.test.ts +550 -0
  179. package/src/__tests__/public-user-profile.test.ts +339 -0
  180. package/src/__tests__/push-handlers.test.ts +179 -0
  181. package/src/__tests__/push-provider.test.ts +80 -0
  182. package/src/__tests__/push-token.test.ts +418 -0
  183. package/src/__tests__/query.test.ts +771 -0
  184. package/src/__tests__/rate-limit.test.ts +260 -0
  185. package/src/__tests__/room-access-policy.test.ts +101 -0
  186. package/src/__tests__/room-handler-context.test.ts +130 -0
  187. package/src/__tests__/room-monitoring.test.ts +138 -0
  188. package/src/__tests__/room-runtime-routing.test.ts +222 -0
  189. package/src/__tests__/room.test.ts +254 -0
  190. package/src/__tests__/route-parser.test.ts +490 -0
  191. package/src/__tests__/rules.test.ts +234 -0
  192. package/src/__tests__/runtime-surface-accounting.test.ts +120 -0
  193. package/src/__tests__/scheduled.test.ts +80 -0
  194. package/src/__tests__/schema.test.ts +1273 -0
  195. package/src/__tests__/security-hardening.test.ts +312 -0
  196. package/src/__tests__/server.unit.test.ts +333 -0
  197. package/src/__tests__/service-key-db-proxy.test.ts +650 -0
  198. package/src/__tests__/service-key-provider-bypass.test.ts +138 -0
  199. package/src/__tests__/service-key.test.ts +757 -0
  200. package/src/__tests__/smoke-skip-report.test.ts +72 -0
  201. package/src/__tests__/sms-provider.test.ts +39 -0
  202. package/src/__tests__/sql-route.test.ts +218 -0
  203. package/src/__tests__/storage-hook-context.test.ts +115 -0
  204. package/src/__tests__/totp.test.ts +200 -0
  205. package/src/__tests__/uuid.test.ts +144 -0
  206. package/src/__tests__/validation.test.ts +773 -0
  207. package/src/__tests__/websocket-pending.test.ts +163 -0
  208. package/src/_functions-registry.ts +51 -0
  209. package/src/bench-entry.ts +9 -0
  210. package/src/cloudflare-test.d.ts +1 -0
  211. package/src/durable-objects/auth-do.ts +49 -0
  212. package/src/durable-objects/database-do.ts +2240 -0
  213. package/src/durable-objects/database-live-do.ts +949 -0
  214. package/src/durable-objects/logs-do.ts +1200 -0
  215. package/src/durable-objects/room-runtime-base.ts +1604 -0
  216. package/src/durable-objects/rooms-do.ts +2191 -0
  217. package/src/generated-config.ts +6 -0
  218. package/src/index.ts +382 -0
  219. package/src/lib/admin-assets.ts +54 -0
  220. package/src/lib/admin-db-target.ts +301 -0
  221. package/src/lib/admin-routing.ts +35 -0
  222. package/src/lib/admin-user-management.ts +464 -0
  223. package/src/lib/analytics-adapter.ts +103 -0
  224. package/src/lib/analytics-query.ts +579 -0
  225. package/src/lib/auth-d1-service.ts +1193 -0
  226. package/src/lib/auth-d1.ts +1056 -0
  227. package/src/lib/auth-db-adapter.ts +289 -0
  228. package/src/lib/auth-redirect.ts +116 -0
  229. package/src/lib/cidr.ts +115 -0
  230. package/src/lib/client-ip.ts +51 -0
  231. package/src/lib/cloudflare-realtime.ts +251 -0
  232. package/src/lib/control-db.ts +36 -0
  233. package/src/lib/cron.ts +163 -0
  234. package/src/lib/d1-handler.ts +1425 -0
  235. package/src/lib/d1-schema-init.ts +255 -0
  236. package/src/lib/d1-sql.ts +33 -0
  237. package/src/lib/database-live-config.ts +24 -0
  238. package/src/lib/database-live-emitter.ts +111 -0
  239. package/src/lib/db-sql.ts +66 -0
  240. package/src/lib/do-retry.ts +36 -0
  241. package/src/lib/do-router.ts +270 -0
  242. package/src/lib/do-sql.ts +73 -0
  243. package/src/lib/email-provider.ts +379 -0
  244. package/src/lib/email-templates.ts +285 -0
  245. package/src/lib/email-translations.ts +422 -0
  246. package/src/lib/errors.ts +151 -0
  247. package/src/lib/functions.ts +2091 -0
  248. package/src/lib/hono.ts +56 -0
  249. package/src/lib/internal-request.ts +56 -0
  250. package/src/lib/jwt.ts +354 -0
  251. package/src/lib/log-writer.ts +272 -0
  252. package/src/lib/namespace-dump.ts +125 -0
  253. package/src/lib/oauth-providers.ts +1225 -0
  254. package/src/lib/op-parser.ts +99 -0
  255. package/src/lib/openapi.ts +146 -0
  256. package/src/lib/pagination.ts +19 -0
  257. package/src/lib/password-policy.ts +102 -0
  258. package/src/lib/password.ts +145 -0
  259. package/src/lib/plugin-migrations.ts +612 -0
  260. package/src/lib/postgres-executor.ts +203 -0
  261. package/src/lib/postgres-handler.ts +1102 -0
  262. package/src/lib/postgres-schema-init.ts +341 -0
  263. package/src/lib/postgres-table-utils.ts +87 -0
  264. package/src/lib/public-user-profile.ts +187 -0
  265. package/src/lib/push-provider.ts +409 -0
  266. package/src/lib/push-token.ts +294 -0
  267. package/src/lib/query-engine.ts +768 -0
  268. package/src/lib/room-monitoring.ts +97 -0
  269. package/src/lib/room-runtime.ts +14 -0
  270. package/src/lib/route-parser.ts +434 -0
  271. package/src/lib/schema.ts +538 -0
  272. package/src/lib/schemas.ts +152 -0
  273. package/src/lib/service-key.ts +419 -0
  274. package/src/lib/sms-provider.ts +230 -0
  275. package/src/lib/startup-config.ts +99 -0
  276. package/src/lib/totp.ts +242 -0
  277. package/src/lib/uuid.ts +87 -0
  278. package/src/lib/validation.ts +205 -0
  279. package/src/lib/version.ts +2 -0
  280. package/src/lib/websocket-pending.ts +40 -0
  281. package/src/middleware/auth.ts +169 -0
  282. package/src/middleware/captcha-verify.ts +217 -0
  283. package/src/middleware/cors.ts +159 -0
  284. package/src/middleware/error-handler.ts +54 -0
  285. package/src/middleware/internal-guard.ts +26 -0
  286. package/src/middleware/logger.ts +126 -0
  287. package/src/middleware/rate-limit.ts +283 -0
  288. package/src/middleware/rules.ts +475 -0
  289. package/src/routes/admin-auth.ts +447 -0
  290. package/src/routes/admin.ts +3501 -0
  291. package/src/routes/analytics-api.ts +290 -0
  292. package/src/routes/auth.ts +4222 -0
  293. package/src/routes/backup.ts +1466 -0
  294. package/src/routes/config.ts +53 -0
  295. package/src/routes/d1.ts +109 -0
  296. package/src/routes/database-live.ts +281 -0
  297. package/src/routes/functions.ts +155 -0
  298. package/src/routes/health.ts +32 -0
  299. package/src/routes/kv.ts +167 -0
  300. package/src/routes/oauth.ts +1055 -0
  301. package/src/routes/push.ts +1465 -0
  302. package/src/routes/room.ts +639 -0
  303. package/src/routes/schema-endpoint.ts +76 -0
  304. package/src/routes/sql.ts +176 -0
  305. package/src/routes/storage.ts +1674 -0
  306. package/src/routes/tables.ts +699 -0
  307. package/src/routes/users.ts +21 -0
  308. package/src/routes/vectorize.ts +372 -0
  309. package/src/types.ts +99 -0
@@ -0,0 +1,422 @@
1
+ /**
2
+ * Built-in email translations for authentication flows.
3
+ *
4
+ * Supported locales: en, ko, ja, zh, es, fr, de, pt
5
+ * Each locale provides translated strings for all 5 email types.
6
+ * Use getStrings(locale, type) to resolve with fallback chain:
7
+ * exact locale → base language (e.g. 'zh' from 'zh-TW') → 'en'
8
+ */
9
+
10
+ export const SUPPORTED_LOCALES = ['en', 'ko', 'ja', 'zh', 'es', 'fr', 'de', 'pt'] as const;
11
+ export type SupportedLocale = (typeof SUPPORTED_LOCALES)[number];
12
+
13
+ export interface EmailStrings {
14
+ subject: string; // with {{appName}} placeholder
15
+ heading: string;
16
+ subheading?: string; // optional second line
17
+ cta?: string; // button text
18
+ tokenLabel?: string; // "Or enter the code manually:"
19
+ instruction?: string; // for OTP/emailChange
20
+ expires: string; // with {{expiresInHours}} or {{expiresInMinutes}} placeholder
21
+ ignore: string;
22
+ }
23
+
24
+ export type EmailType = 'verification' | 'passwordReset' | 'magicLink' | 'emailOtp' | 'emailChange';
25
+
26
+ // ─── English ───
27
+
28
+ const en: Record<EmailType, EmailStrings> = {
29
+ verification: {
30
+ subject: '[{{appName}}] Verify your email',
31
+ heading: 'Please verify your email address.',
32
+ subheading: 'Click the button below or enter the verification code to verify your email:',
33
+ cta: 'Verify Email',
34
+ tokenLabel: 'Or enter the verification token manually:',
35
+ expires: 'This link expires in {{expiresInHours}} hours.',
36
+ ignore: "If you didn't request this, please ignore this email.",
37
+ },
38
+ passwordReset: {
39
+ subject: '[{{appName}}] Reset your password',
40
+ heading: 'You requested a password reset.',
41
+ subheading: 'Click the button below to set a new password:',
42
+ cta: 'Reset Password',
43
+ tokenLabel: 'Or enter the reset token manually:',
44
+ expires: 'This link expires in {{expiresInMinutes}} minutes.',
45
+ ignore: "If you didn't request this, please ignore this email. Your password will not be changed.",
46
+ },
47
+ magicLink: {
48
+ subject: '[{{appName}}] Your login link',
49
+ heading: 'A login link was requested for your account.',
50
+ subheading: 'Click the button below to sign in:',
51
+ cta: 'Sign In',
52
+ expires: 'This link expires in {{expiresInMinutes}} minutes.',
53
+ ignore: "If you didn't request this, please ignore this email.",
54
+ },
55
+ emailOtp: {
56
+ subject: '[{{appName}}] Your login code',
57
+ heading: 'Here is your login verification code.',
58
+ instruction: 'Enter the code below to complete your sign-in:',
59
+ expires: 'This code expires in {{expiresInMinutes}} minutes.',
60
+ ignore: "If you didn't request this, please ignore this email.",
61
+ },
62
+ emailChange: {
63
+ subject: '[{{appName}}] Confirm email change',
64
+ heading: 'An email address change was requested for your account.',
65
+ instruction: 'To change your email to <strong>{{newEmail}}</strong>, click the button below:',
66
+ cta: 'Confirm Email Change',
67
+ tokenLabel: 'Or enter the verification token manually:',
68
+ expires: 'This link expires in {{expiresInHours}} hours.',
69
+ ignore: "If you didn't request this, please ignore this email. Your email address will not be changed.",
70
+ },
71
+ };
72
+
73
+ // ─── Korean (한국어) ───
74
+
75
+ const ko: Record<EmailType, EmailStrings> = {
76
+ verification: {
77
+ subject: '[{{appName}}] 이메일 인증',
78
+ heading: '이메일 주소를 인증해주세요.',
79
+ subheading: '아래 버튼을 클릭하거나 인증 코드를 입력하여 이메일을 인증하세요:',
80
+ cta: '이메일 인증',
81
+ tokenLabel: '또는 인증 코드를 직접 입력하세요:',
82
+ expires: '이 링크는 {{expiresInHours}}시간 후에 만료됩니다.',
83
+ ignore: '요청하지 않으셨다면 이 이메일을 무시하세요.',
84
+ },
85
+ passwordReset: {
86
+ subject: '[{{appName}}] 비밀번호 재설정',
87
+ heading: '비밀번호 재설정을 요청하셨습니다.',
88
+ subheading: '아래 버튼을 클릭하여 새 비밀번호를 설정하세요:',
89
+ cta: '비밀번호 재설정',
90
+ tokenLabel: '또는 재설정 코드를 직접 입력하세요:',
91
+ expires: '이 링크는 {{expiresInMinutes}}분 후에 만료됩니다.',
92
+ ignore: '요청하지 않으셨다면 이 이메일을 무시하세요. 비밀번호는 변경되지 않습니다.',
93
+ },
94
+ magicLink: {
95
+ subject: '[{{appName}}] 로그인 링크',
96
+ heading: '계정 로그인 링크가 요청되었습니다.',
97
+ subheading: '아래 버튼을 클릭하여 로그인하세요:',
98
+ cta: '로그인',
99
+ expires: '이 링크는 {{expiresInMinutes}}분 후에 만료됩니다.',
100
+ ignore: '요청하지 않으셨다면 이 이메일을 무시하세요.',
101
+ },
102
+ emailOtp: {
103
+ subject: '[{{appName}}] 로그인 인증 코드',
104
+ heading: '로그인 인증 코드입니다.',
105
+ instruction: '아래 코드를 입력하여 로그인을 완료하세요:',
106
+ expires: '이 코드는 {{expiresInMinutes}}분 후에 만료됩니다.',
107
+ ignore: '요청하지 않으셨다면 이 이메일을 무시하세요.',
108
+ },
109
+ emailChange: {
110
+ subject: '[{{appName}}] 이메일 변경 확인',
111
+ heading: '계정의 이메일 변경이 요청되었습니다.',
112
+ instruction: '이메일을 <strong>{{newEmail}}</strong>(으)로 변경하려면 아래 버튼을 클릭하세요:',
113
+ cta: '이메일 변경 확인',
114
+ tokenLabel: '또는 인증 코드를 직접 입력하세요:',
115
+ expires: '이 링크는 {{expiresInHours}}시간 후에 만료됩니다.',
116
+ ignore: '요청하지 않으셨다면 이 이메일을 무시하세요. 이메일 주소는 변경되지 않습니다.',
117
+ },
118
+ };
119
+
120
+ // ─── Japanese (日本語) ───
121
+
122
+ const ja: Record<EmailType, EmailStrings> = {
123
+ verification: {
124
+ subject: '[{{appName}}] メール認証',
125
+ heading: 'メールアドレスを認証してください。',
126
+ subheading: '下のボタンをクリックするか、認証コードを入力してメールを認証してください:',
127
+ cta: 'メール認証',
128
+ tokenLabel: 'または認証コードを直接入力してください:',
129
+ expires: 'このリンクは{{expiresInHours}}時間後に期限切れになります。',
130
+ ignore: 'リクエストしていない場合は、このメールを無視してください。',
131
+ },
132
+ passwordReset: {
133
+ subject: '[{{appName}}] パスワードリセット',
134
+ heading: 'パスワードリセットがリクエストされました。',
135
+ subheading: '下のボタンをクリックして新しいパスワードを設定してください:',
136
+ cta: 'パスワードリセット',
137
+ tokenLabel: 'またはリセットコードを直接入力してください:',
138
+ expires: 'このリンクは{{expiresInMinutes}}分後に期限切れになります。',
139
+ ignore: 'リクエストしていない場合は、このメールを無視してください。パスワードは変更されません。',
140
+ },
141
+ magicLink: {
142
+ subject: '[{{appName}}] ログインリンク',
143
+ heading: 'アカウントのログインリンクがリクエストされました。',
144
+ subheading: '下のボタンをクリックしてログインしてください:',
145
+ cta: 'ログイン',
146
+ expires: 'このリンクは{{expiresInMinutes}}分後に期限切れになります。',
147
+ ignore: 'リクエストしていない場合は、このメールを無視してください。',
148
+ },
149
+ emailOtp: {
150
+ subject: '[{{appName}}] ログイン認証コード',
151
+ heading: 'ログイン認証コードです。',
152
+ instruction: '下のコードを入力してログインを完了してください:',
153
+ expires: 'このコードは{{expiresInMinutes}}分後に期限切れになります。',
154
+ ignore: 'リクエストしていない場合は、このメールを無視してください。',
155
+ },
156
+ emailChange: {
157
+ subject: '[{{appName}}] メール変更の確認',
158
+ heading: 'アカウントのメールアドレス変更がリクエストされました。',
159
+ instruction: 'メールを<strong>{{newEmail}}</strong>に変更するには、下のボタンをクリックしてください:',
160
+ cta: 'メール変更を確認',
161
+ tokenLabel: 'または認証コードを直接入力してください:',
162
+ expires: 'このリンクは{{expiresInHours}}時間後に期限切れになります。',
163
+ ignore: 'リクエストしていない場合は、このメールを無視してください。メールアドレスは変更されません。',
164
+ },
165
+ };
166
+
167
+ // ─── Chinese (中文) ───
168
+
169
+ const zh: Record<EmailType, EmailStrings> = {
170
+ verification: {
171
+ subject: '[{{appName}}] 邮箱验证',
172
+ heading: '请验证您的邮箱地址。',
173
+ subheading: '点击下面的按钮或输入验证码来验证您的邮箱:',
174
+ cta: '验证邮箱',
175
+ tokenLabel: '或手动输入验证码:',
176
+ expires: '此链接将在{{expiresInHours}}小时后过期。',
177
+ ignore: '如果您没有发起此请求,请忽略此邮件。',
178
+ },
179
+ passwordReset: {
180
+ subject: '[{{appName}}] 重置密码',
181
+ heading: '您请求了密码重置。',
182
+ subheading: '点击下面的按钮设置新密码:',
183
+ cta: '重置密码',
184
+ tokenLabel: '或手动输入重置码:',
185
+ expires: '此链接将在{{expiresInMinutes}}分钟后过期。',
186
+ ignore: '如果您没有发起此请求,请忽略此邮件。您的密码不会被更改。',
187
+ },
188
+ magicLink: {
189
+ subject: '[{{appName}}] 登录链接',
190
+ heading: '您的账户收到了登录链接请求。',
191
+ subheading: '点击下面的按钮登录:',
192
+ cta: '登录',
193
+ expires: '此链接将在{{expiresInMinutes}}分钟后过期。',
194
+ ignore: '如果您没有发起此请求,请忽略此邮件。',
195
+ },
196
+ emailOtp: {
197
+ subject: '[{{appName}}] 登录验证码',
198
+ heading: '以下是您的登录验证码。',
199
+ instruction: '输入以下验证码完成登录:',
200
+ expires: '此验证码将在{{expiresInMinutes}}分钟后过期。',
201
+ ignore: '如果您没有发起此请求,请忽略此邮件。',
202
+ },
203
+ emailChange: {
204
+ subject: '[{{appName}}] 确认更改邮箱',
205
+ heading: '您的账户请求了邮箱地址更改。',
206
+ instruction: '要将邮箱更改为<strong>{{newEmail}}</strong>,请点击下面的按钮:',
207
+ cta: '确认更改邮箱',
208
+ tokenLabel: '或手动输入验证码:',
209
+ expires: '此链接将在{{expiresInHours}}小时后过期。',
210
+ ignore: '如果您没有发起此请求,请忽略此邮件。您的邮箱地址不会被更改。',
211
+ },
212
+ };
213
+
214
+ // ─── Spanish (Español) ───
215
+
216
+ const es: Record<EmailType, EmailStrings> = {
217
+ verification: {
218
+ subject: '[{{appName}}] Verifica tu correo',
219
+ heading: 'Por favor verifica tu dirección de correo electrónico.',
220
+ subheading: 'Haz clic en el botón de abajo o ingresa el código de verificación:',
221
+ cta: 'Verificar correo',
222
+ tokenLabel: 'O ingresa el código de verificación manualmente:',
223
+ expires: 'Este enlace expira en {{expiresInHours}} horas.',
224
+ ignore: 'Si no solicitaste esto, ignora este correo.',
225
+ },
226
+ passwordReset: {
227
+ subject: '[{{appName}}] Restablecer contraseña',
228
+ heading: 'Solicitaste un restablecimiento de contraseña.',
229
+ subheading: 'Haz clic en el botón de abajo para establecer una nueva contraseña:',
230
+ cta: 'Restablecer contraseña',
231
+ tokenLabel: 'O ingresa el código de restablecimiento manualmente:',
232
+ expires: 'Este enlace expira en {{expiresInMinutes}} minutos.',
233
+ ignore: 'Si no solicitaste esto, ignora este correo. Tu contraseña no será modificada.',
234
+ },
235
+ magicLink: {
236
+ subject: '[{{appName}}] Tu enlace de inicio de sesión',
237
+ heading: 'Se solicitó un enlace de inicio de sesión para tu cuenta.',
238
+ subheading: 'Haz clic en el botón de abajo para iniciar sesión:',
239
+ cta: 'Iniciar sesión',
240
+ expires: 'Este enlace expira en {{expiresInMinutes}} minutos.',
241
+ ignore: 'Si no solicitaste esto, ignora este correo.',
242
+ },
243
+ emailOtp: {
244
+ subject: '[{{appName}}] Tu código de verificación',
245
+ heading: 'Aquí está tu código de verificación de inicio de sesión.',
246
+ instruction: 'Ingresa el siguiente código para completar tu inicio de sesión:',
247
+ expires: 'Este código expira en {{expiresInMinutes}} minutos.',
248
+ ignore: 'Si no solicitaste esto, ignora este correo.',
249
+ },
250
+ emailChange: {
251
+ subject: '[{{appName}}] Confirmar cambio de correo',
252
+ heading: 'Se solicitó un cambio de dirección de correo electrónico para tu cuenta.',
253
+ instruction: 'Para cambiar tu correo a <strong>{{newEmail}}</strong>, haz clic en el botón de abajo:',
254
+ cta: 'Confirmar cambio de correo',
255
+ tokenLabel: 'O ingresa el código de verificación manualmente:',
256
+ expires: 'Este enlace expira en {{expiresInHours}} horas.',
257
+ ignore: 'Si no solicitaste esto, ignora este correo. Tu dirección de correo no será modificada.',
258
+ },
259
+ };
260
+
261
+ // ─── French (Français) ───
262
+
263
+ const fr: Record<EmailType, EmailStrings> = {
264
+ verification: {
265
+ subject: '[{{appName}}] Vérifiez votre adresse e-mail',
266
+ heading: 'Veuillez vérifier votre adresse e-mail.',
267
+ subheading: 'Cliquez sur le bouton ci-dessous ou entrez le code de vérification :',
268
+ cta: 'Vérifier l\'e-mail',
269
+ tokenLabel: 'Ou entrez le code de vérification manuellement :',
270
+ expires: 'Ce lien expire dans {{expiresInHours}} heures.',
271
+ ignore: 'Si vous n\'avez pas fait cette demande, veuillez ignorer cet e-mail.',
272
+ },
273
+ passwordReset: {
274
+ subject: '[{{appName}}] Réinitialisation du mot de passe',
275
+ heading: 'Vous avez demandé une réinitialisation de mot de passe.',
276
+ subheading: 'Cliquez sur le bouton ci-dessous pour définir un nouveau mot de passe :',
277
+ cta: 'Réinitialiser le mot de passe',
278
+ tokenLabel: 'Ou entrez le code de réinitialisation manuellement :',
279
+ expires: 'Ce lien expire dans {{expiresInMinutes}} minutes.',
280
+ ignore: 'Si vous n\'avez pas fait cette demande, veuillez ignorer cet e-mail. Votre mot de passe ne sera pas modifié.',
281
+ },
282
+ magicLink: {
283
+ subject: '[{{appName}}] Votre lien de connexion',
284
+ heading: 'Un lien de connexion a été demandé pour votre compte.',
285
+ subheading: 'Cliquez sur le bouton ci-dessous pour vous connecter :',
286
+ cta: 'Se connecter',
287
+ expires: 'Ce lien expire dans {{expiresInMinutes}} minutes.',
288
+ ignore: 'Si vous n\'avez pas fait cette demande, veuillez ignorer cet e-mail.',
289
+ },
290
+ emailOtp: {
291
+ subject: '[{{appName}}] Votre code de connexion',
292
+ heading: 'Voici votre code de vérification de connexion.',
293
+ instruction: 'Entrez le code ci-dessous pour terminer votre connexion :',
294
+ expires: 'Ce code expire dans {{expiresInMinutes}} minutes.',
295
+ ignore: 'Si vous n\'avez pas fait cette demande, veuillez ignorer cet e-mail.',
296
+ },
297
+ emailChange: {
298
+ subject: '[{{appName}}] Confirmer le changement d\'e-mail',
299
+ heading: 'Un changement d\'adresse e-mail a été demandé pour votre compte.',
300
+ instruction: 'Pour changer votre e-mail en <strong>{{newEmail}}</strong>, cliquez sur le bouton ci-dessous :',
301
+ cta: 'Confirmer le changement',
302
+ tokenLabel: 'Ou entrez le code de vérification manuellement :',
303
+ expires: 'Ce lien expire dans {{expiresInHours}} heures.',
304
+ ignore: 'Si vous n\'avez pas fait cette demande, veuillez ignorer cet e-mail. Votre adresse e-mail ne sera pas modifiée.',
305
+ },
306
+ };
307
+
308
+ // ─── German (Deutsch) ───
309
+
310
+ const de: Record<EmailType, EmailStrings> = {
311
+ verification: {
312
+ subject: '[{{appName}}] E-Mail bestätigen',
313
+ heading: 'Bitte bestätigen Sie Ihre E-Mail-Adresse.',
314
+ subheading: 'Klicken Sie auf die Schaltfläche unten oder geben Sie den Bestätigungscode ein:',
315
+ cta: 'E-Mail bestätigen',
316
+ tokenLabel: 'Oder geben Sie den Bestätigungscode manuell ein:',
317
+ expires: 'Dieser Link läuft in {{expiresInHours}} Stunden ab.',
318
+ ignore: 'Wenn Sie dies nicht angefordert haben, ignorieren Sie bitte diese E-Mail.',
319
+ },
320
+ passwordReset: {
321
+ subject: '[{{appName}}] Passwort zurücksetzen',
322
+ heading: 'Sie haben ein Zurücksetzen des Passworts angefordert.',
323
+ subheading: 'Klicken Sie auf die Schaltfläche unten, um ein neues Passwort festzulegen:',
324
+ cta: 'Passwort zurücksetzen',
325
+ tokenLabel: 'Oder geben Sie den Zurücksetzungscode manuell ein:',
326
+ expires: 'Dieser Link läuft in {{expiresInMinutes}} Minuten ab.',
327
+ ignore: 'Wenn Sie dies nicht angefordert haben, ignorieren Sie bitte diese E-Mail. Ihr Passwort wird nicht geändert.',
328
+ },
329
+ magicLink: {
330
+ subject: '[{{appName}}] Ihr Anmeldelink',
331
+ heading: 'Ein Anmeldelink wurde für Ihr Konto angefordert.',
332
+ subheading: 'Klicken Sie auf die Schaltfläche unten, um sich anzumelden:',
333
+ cta: 'Anmelden',
334
+ expires: 'Dieser Link läuft in {{expiresInMinutes}} Minuten ab.',
335
+ ignore: 'Wenn Sie dies nicht angefordert haben, ignorieren Sie bitte diese E-Mail.',
336
+ },
337
+ emailOtp: {
338
+ subject: '[{{appName}}] Ihr Anmeldecode',
339
+ heading: 'Hier ist Ihr Anmelde-Bestätigungscode.',
340
+ instruction: 'Geben Sie den folgenden Code ein, um Ihre Anmeldung abzuschließen:',
341
+ expires: 'Dieser Code läuft in {{expiresInMinutes}} Minuten ab.',
342
+ ignore: 'Wenn Sie dies nicht angefordert haben, ignorieren Sie bitte diese E-Mail.',
343
+ },
344
+ emailChange: {
345
+ subject: '[{{appName}}] E-Mail-Änderung bestätigen',
346
+ heading: 'Eine Änderung der E-Mail-Adresse wurde für Ihr Konto angefordert.',
347
+ instruction: 'Um Ihre E-Mail zu <strong>{{newEmail}}</strong> zu ändern, klicken Sie auf die Schaltfläche unten:',
348
+ cta: 'E-Mail-Änderung bestätigen',
349
+ tokenLabel: 'Oder geben Sie den Bestätigungscode manuell ein:',
350
+ expires: 'Dieser Link läuft in {{expiresInHours}} Stunden ab.',
351
+ ignore: 'Wenn Sie dies nicht angefordert haben, ignorieren Sie bitte diese E-Mail. Ihre E-Mail-Adresse wird nicht geändert.',
352
+ },
353
+ };
354
+
355
+ // ─── Portuguese (Português) ───
356
+
357
+ const pt: Record<EmailType, EmailStrings> = {
358
+ verification: {
359
+ subject: '[{{appName}}] Verifique seu e-mail',
360
+ heading: 'Por favor, verifique seu endereço de e-mail.',
361
+ subheading: 'Clique no botão abaixo ou insira o código de verificação:',
362
+ cta: 'Verificar e-mail',
363
+ tokenLabel: 'Ou insira o código de verificação manualmente:',
364
+ expires: 'Este link expira em {{expiresInHours}} horas.',
365
+ ignore: 'Se você não solicitou isso, ignore este e-mail.',
366
+ },
367
+ passwordReset: {
368
+ subject: '[{{appName}}] Redefinir senha',
369
+ heading: 'Você solicitou uma redefinição de senha.',
370
+ subheading: 'Clique no botão abaixo para definir uma nova senha:',
371
+ cta: 'Redefinir senha',
372
+ tokenLabel: 'Ou insira o código de redefinição manualmente:',
373
+ expires: 'Este link expira em {{expiresInMinutes}} minutos.',
374
+ ignore: 'Se você não solicitou isso, ignore este e-mail. Sua senha não será alterada.',
375
+ },
376
+ magicLink: {
377
+ subject: '[{{appName}}] Seu link de login',
378
+ heading: 'Um link de login foi solicitado para sua conta.',
379
+ subheading: 'Clique no botão abaixo para fazer login:',
380
+ cta: 'Fazer login',
381
+ expires: 'Este link expira em {{expiresInMinutes}} minutos.',
382
+ ignore: 'Se você não solicitou isso, ignore este e-mail.',
383
+ },
384
+ emailOtp: {
385
+ subject: '[{{appName}}] Seu código de login',
386
+ heading: 'Aqui está seu código de verificação de login.',
387
+ instruction: 'Insira o código abaixo para concluir seu login:',
388
+ expires: 'Este código expira em {{expiresInMinutes}} minutos.',
389
+ ignore: 'Se você não solicitou isso, ignore este e-mail.',
390
+ },
391
+ emailChange: {
392
+ subject: '[{{appName}}] Confirmar alteração de e-mail',
393
+ heading: 'Uma alteração de endereço de e-mail foi solicitada para sua conta.',
394
+ instruction: 'Para alterar seu e-mail para <strong>{{newEmail}}</strong>, clique no botão abaixo:',
395
+ cta: 'Confirmar alteração',
396
+ tokenLabel: 'Ou insira o código de verificação manualmente:',
397
+ expires: 'Este link expira em {{expiresInHours}} horas.',
398
+ ignore: 'Se você não solicitou isso, ignore este e-mail. Seu endereço de e-mail não será alterado.',
399
+ },
400
+ };
401
+
402
+ // ─── Translation Map ───
403
+
404
+ const translations: Record<string, Record<EmailType, EmailStrings>> = {
405
+ en, ko, ja, zh, es, fr, de, pt,
406
+ };
407
+
408
+ /**
409
+ * Resolve translation strings with fallback chain:
410
+ * exact locale → base language (e.g. 'zh' from 'zh-TW') → 'en'
411
+ */
412
+ export function getStrings(locale: string, type: EmailType): EmailStrings {
413
+ const base = locale.split('-')[0];
414
+ return translations[locale]?.[type] ?? translations[base]?.[type] ?? translations.en[type];
415
+ }
416
+
417
+ /**
418
+ * Get translated default subject with {{appName}} placeholder.
419
+ */
420
+ export function getDefaultSubject(locale: string, type: EmailType): string {
421
+ return getStrings(locale, type).subject;
422
+ }
@@ -0,0 +1,151 @@
1
+ import { EdgeBaseError } from '@edge-base/shared';
2
+ import type { ErrorResponse, FieldError } from '@edge-base/shared';
3
+
4
+ /**
5
+ * Create a validation error (400).
6
+ */
7
+ export function validationError(
8
+ message: string,
9
+ fields?: Record<string, FieldError>,
10
+ slug?: string,
11
+ ): EdgeBaseError {
12
+ return new EdgeBaseError(400, message, fields, slug || 'validation-failed');
13
+ }
14
+
15
+ /**
16
+ * Create an unauthorized error (401).
17
+ */
18
+ export function unauthorizedError(message = 'Unauthorized.', slug?: string): EdgeBaseError {
19
+ return new EdgeBaseError(401, message, undefined, slug || 'unauthenticated');
20
+ }
21
+
22
+ /**
23
+ * Create a forbidden error (403).
24
+ */
25
+ export function forbiddenError(message = 'Access denied.', slug?: string): EdgeBaseError {
26
+ return new EdgeBaseError(403, message, undefined, slug || 'forbidden');
27
+ }
28
+
29
+ /**
30
+ * Create a not found error (404).
31
+ */
32
+ export function notFoundError(message = 'Not found.', slug?: string): EdgeBaseError {
33
+ return new EdgeBaseError(404, message, undefined, slug || 'not-found');
34
+ }
35
+
36
+ /**
37
+ * Create a method not allowed error (405).
38
+ */
39
+ export function methodNotAllowedError(message = 'Method not allowed.'): EdgeBaseError {
40
+ return new EdgeBaseError(405, message, undefined, 'method-not-allowed');
41
+ }
42
+
43
+ /**
44
+ * Create a rate limit error (429).
45
+ */
46
+ export function rateLimitError(retryAfter: number): EdgeBaseError {
47
+ return new EdgeBaseError(429, `Too many requests. Retry after ${retryAfter} seconds.`, undefined, 'rate-limited');
48
+ }
49
+
50
+ /**
51
+ * Normalize user-defined hook errors into client-facing EdgeBase errors.
52
+ * Hooks often throw plain `Error` instances; those should reject the request
53
+ * with a 4xx instead of surfacing as a 500.
54
+ *
55
+ * Prefixes the error message with the hook source so developers know
56
+ * where to look when debugging.
57
+ */
58
+ export function hookRejectedError(
59
+ error: unknown,
60
+ fallbackMessage = 'Hook rejected the request.',
61
+ hookName?: string,
62
+ ): EdgeBaseError {
63
+ if (error instanceof EdgeBaseError) return error;
64
+
65
+ const message = error instanceof Error ? error.message.trim() : '';
66
+ const rawMessage = message || fallbackMessage;
67
+ // Prefix with hook source so developers know where to look
68
+ const safeMessage = hookName
69
+ ? `Hook '${hookName}' rejected: ${rawMessage}`
70
+ : rawMessage;
71
+
72
+ if (/auth(entication)? required|unauthorized/i.test(rawMessage)) {
73
+ return unauthorizedError(safeMessage, 'hook-rejected');
74
+ }
75
+ if (/forbidden|denied|blocked|not allowed|only .* can|owner/i.test(rawMessage)) {
76
+ return forbiddenError(safeMessage, 'hook-rejected');
77
+ }
78
+ if (/not found|unknown/i.test(rawMessage)) {
79
+ return notFoundError(safeMessage, 'hook-rejected');
80
+ }
81
+ if (/already exists|already registered|duplicate|conflict/i.test(rawMessage)) {
82
+ return new EdgeBaseError(409, safeMessage, undefined, 'hook-rejected');
83
+ }
84
+ if (/too many|rate limit|thrott/i.test(rawMessage)) {
85
+ return new EdgeBaseError(429, safeMessage, undefined, 'hook-rejected');
86
+ }
87
+
88
+ return new EdgeBaseError(400, safeMessage, undefined, 'hook-rejected');
89
+ }
90
+
91
+ /**
92
+ * Normalize low-level database constraint failures into client-facing 4xx errors.
93
+ * Extracts column/table info from the error message when available.
94
+ */
95
+ export function normalizeDatabaseError(error: unknown): EdgeBaseError | null {
96
+ if (error instanceof EdgeBaseError) return error;
97
+
98
+ const objectMessage = (
99
+ error
100
+ && typeof error === 'object'
101
+ && typeof (error as { message?: unknown }).message === 'string'
102
+ )
103
+ ? (error as { message: string }).message.trim()
104
+ : '';
105
+ const causeMessage = (
106
+ error
107
+ && typeof error === 'object'
108
+ && typeof (error as { cause?: { message?: unknown } }).cause?.message === 'string'
109
+ )
110
+ ? (error as { cause: { message: string } }).cause.message.trim()
111
+ : '';
112
+ const message = error instanceof Error
113
+ ? error.message.trim()
114
+ : typeof error === 'string'
115
+ ? error.trim()
116
+ : objectMessage || causeMessage;
117
+
118
+ if (!message) return null;
119
+
120
+ const haystack = `${message}\n${causeMessage}`.trim();
121
+
122
+ if (/foreign key constraint failed/i.test(haystack)) {
123
+ // Try to extract column name from D1/SQLite error: "FOREIGN KEY constraint failed: tableName.columnName"
124
+ const colMatch = haystack.match(/foreign key constraint failed[:\s]*(?:\w+\.)?(\w+)/i);
125
+ const detail = colMatch?.[1]
126
+ ? `Referenced record does not exist (column: '${colMatch[1]}').`
127
+ : 'Referenced record does not exist. Check that all foreign key references point to existing records.';
128
+ return new EdgeBaseError(400, detail, undefined, 'foreign-key-failed');
129
+ }
130
+
131
+ if (/unique constraint failed|duplicate key value violates unique constraint/i.test(haystack)) {
132
+ // Try to extract column name: "UNIQUE constraint failed: tableName.columnName"
133
+ const colMatch = haystack.match(/(?:unique constraint failed|duplicate key)[:\s]*(?:\w+\.)?(\w+)/i);
134
+ const detail = colMatch?.[1]
135
+ ? `Record already exists (duplicate value in column: '${colMatch[1]}').`
136
+ : 'Record already exists. A unique constraint was violated.';
137
+ return new EdgeBaseError(409, detail, undefined, 'record-already-exists');
138
+ }
139
+
140
+ if (/not null constraint failed|check constraint failed/i.test(haystack)) {
141
+ const colMatch = haystack.match(/(?:not null|check) constraint failed[:\s]*(?:\w+\.)?(\w+)/i);
142
+ const detail = colMatch?.[1]
143
+ ? `Database constraint violated (column: '${colMatch[1]}'). Ensure all required fields are provided.`
144
+ : 'Request violates a database constraint. Ensure all required fields are provided.';
145
+ return new EdgeBaseError(400, detail, undefined, 'constraint-failed');
146
+ }
147
+
148
+ return null;
149
+ }
150
+
151
+ export type { ErrorResponse, FieldError };