@we8/cms 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (304) hide show
  1. package/README.md +78 -0
  2. package/dist/auth/auth.d.ts +87 -0
  3. package/dist/auth/auth.d.ts.map +1 -0
  4. package/dist/auth/auth.js +90 -0
  5. package/dist/auth/auth.js.map +1 -0
  6. package/dist/auth/routes.admin.d.ts +22 -0
  7. package/dist/auth/routes.admin.d.ts.map +1 -0
  8. package/dist/auth/routes.admin.js +106 -0
  9. package/dist/auth/routes.admin.js.map +1 -0
  10. package/dist/auth/schema.d.ts +1395 -0
  11. package/dist/auth/schema.d.ts.map +1 -0
  12. package/dist/auth/schema.js +76 -0
  13. package/dist/auth/schema.js.map +1 -0
  14. package/dist/env.d.ts +64 -0
  15. package/dist/env.d.ts.map +1 -0
  16. package/dist/env.js +48 -0
  17. package/dist/env.js.map +1 -0
  18. package/dist/index.d.ts +22 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +170 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/keys/middleware.d.ts +54 -0
  23. package/dist/keys/middleware.d.ts.map +1 -0
  24. package/dist/keys/middleware.js +84 -0
  25. package/dist/keys/middleware.js.map +1 -0
  26. package/dist/keys/routes.admin.d.ts +16 -0
  27. package/dist/keys/routes.admin.d.ts.map +1 -0
  28. package/dist/keys/routes.admin.js +34 -0
  29. package/dist/keys/routes.admin.js.map +1 -0
  30. package/dist/keys/service.d.ts +36 -0
  31. package/dist/keys/service.d.ts.map +1 -0
  32. package/dist/keys/service.js +120 -0
  33. package/dist/keys/service.js.map +1 -0
  34. package/dist/lib/bots.d.ts +24 -0
  35. package/dist/lib/bots.d.ts.map +1 -0
  36. package/dist/lib/bots.js +79 -0
  37. package/dist/lib/bots.js.map +1 -0
  38. package/dist/lib/email.d.ts +62 -0
  39. package/dist/lib/email.d.ts.map +1 -0
  40. package/dist/lib/email.js +64 -0
  41. package/dist/lib/email.js.map +1 -0
  42. package/dist/lib/errors.d.ts +12 -0
  43. package/dist/lib/errors.d.ts.map +1 -0
  44. package/dist/lib/errors.js +23 -0
  45. package/dist/lib/errors.js.map +1 -0
  46. package/dist/lib/notify.d.ts +20 -0
  47. package/dist/lib/notify.d.ts.map +1 -0
  48. package/dist/lib/notify.js +60 -0
  49. package/dist/lib/notify.js.map +1 -0
  50. package/dist/lib/retention.d.ts +15 -0
  51. package/dist/lib/retention.d.ts.map +1 -0
  52. package/dist/lib/retention.js +40 -0
  53. package/dist/lib/retention.js.map +1 -0
  54. package/dist/lib/sanitize.d.ts +14 -0
  55. package/dist/lib/sanitize.d.ts.map +1 -0
  56. package/dist/lib/sanitize.js +145 -0
  57. package/dist/lib/sanitize.js.map +1 -0
  58. package/dist/lib/turnstile.d.ts +7 -0
  59. package/dist/lib/turnstile.d.ts.map +1 -0
  60. package/dist/lib/turnstile.js +32 -0
  61. package/dist/lib/turnstile.js.map +1 -0
  62. package/dist/lib/validate.d.ts +11 -0
  63. package/dist/lib/validate.d.ts.map +1 -0
  64. package/dist/lib/validate.js +22 -0
  65. package/dist/lib/validate.js.map +1 -0
  66. package/dist/lib/word-count.d.ts +7 -0
  67. package/dist/lib/word-count.d.ts.map +1 -0
  68. package/dist/lib/word-count.js +14 -0
  69. package/dist/lib/word-count.js.map +1 -0
  70. package/dist/middleware/auth.d.ts +46 -0
  71. package/dist/middleware/auth.d.ts.map +1 -0
  72. package/dist/middleware/auth.js +60 -0
  73. package/dist/middleware/auth.js.map +1 -0
  74. package/dist/middleware/cors.d.ts +33 -0
  75. package/dist/middleware/cors.d.ts.map +1 -0
  76. package/dist/middleware/cors.js +55 -0
  77. package/dist/middleware/cors.js.map +1 -0
  78. package/dist/middleware/module-gate.d.ts +11 -0
  79. package/dist/middleware/module-gate.d.ts.map +1 -0
  80. package/dist/middleware/module-gate.js +18 -0
  81. package/dist/middleware/module-gate.js.map +1 -0
  82. package/dist/middleware/rate-limit.d.ts +13 -0
  83. package/dist/middleware/rate-limit.d.ts.map +1 -0
  84. package/dist/middleware/rate-limit.js +30 -0
  85. package/dist/middleware/rate-limit.js.map +1 -0
  86. package/dist/middleware/request-id.d.ts +8 -0
  87. package/dist/middleware/request-id.d.ts.map +1 -0
  88. package/dist/middleware/request-id.js +11 -0
  89. package/dist/middleware/request-id.js.map +1 -0
  90. package/dist/modules/aeo/checks.d.ts +41 -0
  91. package/dist/modules/aeo/checks.d.ts.map +1 -0
  92. package/dist/modules/aeo/checks.js +127 -0
  93. package/dist/modules/aeo/checks.js.map +1 -0
  94. package/dist/modules/aeo/llms.d.ts +24 -0
  95. package/dist/modules/aeo/llms.d.ts.map +1 -0
  96. package/dist/modules/aeo/llms.js +92 -0
  97. package/dist/modules/aeo/llms.js.map +1 -0
  98. package/dist/modules/aeo/routes.admin.d.ts +12 -0
  99. package/dist/modules/aeo/routes.admin.d.ts.map +1 -0
  100. package/dist/modules/aeo/routes.admin.js +37 -0
  101. package/dist/modules/aeo/routes.admin.js.map +1 -0
  102. package/dist/modules/analytics/db.d.ts +24 -0
  103. package/dist/modules/analytics/db.d.ts.map +1 -0
  104. package/dist/modules/analytics/db.js +24 -0
  105. package/dist/modules/analytics/db.js.map +1 -0
  106. package/dist/modules/analytics/routes.admin.d.ts +6 -0
  107. package/dist/modules/analytics/routes.admin.d.ts.map +1 -0
  108. package/dist/modules/analytics/routes.admin.js +21 -0
  109. package/dist/modules/analytics/routes.admin.js.map +1 -0
  110. package/dist/modules/analytics/routes.public.d.ts +6 -0
  111. package/dist/modules/analytics/routes.public.d.ts.map +1 -0
  112. package/dist/modules/analytics/routes.public.js +33 -0
  113. package/dist/modules/analytics/routes.public.js.map +1 -0
  114. package/dist/modules/analytics/service.d.ts +16 -0
  115. package/dist/modules/analytics/service.d.ts.map +1 -0
  116. package/dist/modules/analytics/service.js +148 -0
  117. package/dist/modules/analytics/service.js.map +1 -0
  118. package/dist/modules/authors/routes.admin.d.ts +6 -0
  119. package/dist/modules/authors/routes.admin.d.ts.map +1 -0
  120. package/dist/modules/authors/routes.admin.js +29 -0
  121. package/dist/modules/authors/routes.admin.js.map +1 -0
  122. package/dist/modules/authors/routes.public.d.ts +6 -0
  123. package/dist/modules/authors/routes.public.d.ts.map +1 -0
  124. package/dist/modules/authors/routes.public.js +11 -0
  125. package/dist/modules/authors/routes.public.js.map +1 -0
  126. package/dist/modules/authors/service.d.ts +26 -0
  127. package/dist/modules/authors/service.d.ts.map +1 -0
  128. package/dist/modules/authors/service.js +116 -0
  129. package/dist/modules/authors/service.js.map +1 -0
  130. package/dist/modules/content/db.d.ts +127 -0
  131. package/dist/modules/content/db.d.ts.map +1 -0
  132. package/dist/modules/content/db.js +203 -0
  133. package/dist/modules/content/db.js.map +1 -0
  134. package/dist/modules/content/publish.d.ts +38 -0
  135. package/dist/modules/content/publish.d.ts.map +1 -0
  136. package/dist/modules/content/publish.js +184 -0
  137. package/dist/modules/content/publish.js.map +1 -0
  138. package/dist/modules/content/routes.admin.d.ts +8 -0
  139. package/dist/modules/content/routes.admin.d.ts.map +1 -0
  140. package/dist/modules/content/routes.admin.js +122 -0
  141. package/dist/modules/content/routes.admin.js.map +1 -0
  142. package/dist/modules/content/routes.public.d.ts +6 -0
  143. package/dist/modules/content/routes.public.d.ts.map +1 -0
  144. package/dist/modules/content/routes.public.js +56 -0
  145. package/dist/modules/content/routes.public.js.map +1 -0
  146. package/dist/modules/content/service.d.ts +137 -0
  147. package/dist/modules/content/service.d.ts.map +1 -0
  148. package/dist/modules/content/service.js +348 -0
  149. package/dist/modules/content/service.js.map +1 -0
  150. package/dist/modules/forms/db.d.ts +42 -0
  151. package/dist/modules/forms/db.d.ts.map +1 -0
  152. package/dist/modules/forms/db.js +62 -0
  153. package/dist/modules/forms/db.js.map +1 -0
  154. package/dist/modules/forms/forms-routes.admin.d.ts +6 -0
  155. package/dist/modules/forms/forms-routes.admin.d.ts.map +1 -0
  156. package/dist/modules/forms/forms-routes.admin.js +37 -0
  157. package/dist/modules/forms/forms-routes.admin.js.map +1 -0
  158. package/dist/modules/forms/forms-service.d.ts +18 -0
  159. package/dist/modules/forms/forms-service.d.ts.map +1 -0
  160. package/dist/modules/forms/forms-service.js +121 -0
  161. package/dist/modules/forms/forms-service.js.map +1 -0
  162. package/dist/modules/forms/routes.public.d.ts +6 -0
  163. package/dist/modules/forms/routes.public.d.ts.map +1 -0
  164. package/dist/modules/forms/routes.public.js +41 -0
  165. package/dist/modules/forms/routes.public.js.map +1 -0
  166. package/dist/modules/forms/submissions-routes.admin.d.ts +6 -0
  167. package/dist/modules/forms/submissions-routes.admin.d.ts.map +1 -0
  168. package/dist/modules/forms/submissions-routes.admin.js +46 -0
  169. package/dist/modules/forms/submissions-routes.admin.js.map +1 -0
  170. package/dist/modules/forms/submissions-service.d.ts +45 -0
  171. package/dist/modules/forms/submissions-service.d.ts.map +1 -0
  172. package/dist/modules/forms/submissions-service.js +174 -0
  173. package/dist/modules/forms/submissions-service.js.map +1 -0
  174. package/dist/modules/media/db.d.ts +15 -0
  175. package/dist/modules/media/db.d.ts.map +1 -0
  176. package/dist/modules/media/db.js +24 -0
  177. package/dist/modules/media/db.js.map +1 -0
  178. package/dist/modules/media/routes.admin.d.ts +6 -0
  179. package/dist/modules/media/routes.admin.d.ts.map +1 -0
  180. package/dist/modules/media/routes.admin.js +30 -0
  181. package/dist/modules/media/routes.admin.js.map +1 -0
  182. package/dist/modules/media/routes.public.d.ts +6 -0
  183. package/dist/modules/media/routes.public.d.ts.map +1 -0
  184. package/dist/modules/media/routes.public.js +35 -0
  185. package/dist/modules/media/routes.public.js.map +1 -0
  186. package/dist/modules/media/service.d.ts +32 -0
  187. package/dist/modules/media/service.d.ts.map +1 -0
  188. package/dist/modules/media/service.js +99 -0
  189. package/dist/modules/media/service.js.map +1 -0
  190. package/dist/modules/openapi/spec.d.ts +21 -0
  191. package/dist/modules/openapi/spec.d.ts.map +1 -0
  192. package/dist/modules/openapi/spec.js +468 -0
  193. package/dist/modules/openapi/spec.js.map +1 -0
  194. package/dist/modules/seo/jsonld.d.ts +25 -0
  195. package/dist/modules/seo/jsonld.d.ts.map +1 -0
  196. package/dist/modules/seo/jsonld.js +67 -0
  197. package/dist/modules/seo/jsonld.js.map +1 -0
  198. package/dist/modules/seo/routes.public.d.ts +6 -0
  199. package/dist/modules/seo/routes.public.d.ts.map +1 -0
  200. package/dist/modules/seo/routes.public.js +39 -0
  201. package/dist/modules/seo/routes.public.js.map +1 -0
  202. package/dist/modules/seo/sitemap.d.ts +3 -0
  203. package/dist/modules/seo/sitemap.d.ts.map +1 -0
  204. package/dist/modules/seo/sitemap.js +39 -0
  205. package/dist/modules/seo/sitemap.js.map +1 -0
  206. package/dist/modules/settings/routes.admin.d.ts +12 -0
  207. package/dist/modules/settings/routes.admin.d.ts.map +1 -0
  208. package/dist/modules/settings/routes.admin.js +27 -0
  209. package/dist/modules/settings/routes.admin.js.map +1 -0
  210. package/dist/modules/site-documents/routes.admin.d.ts +10 -0
  211. package/dist/modules/site-documents/routes.admin.d.ts.map +1 -0
  212. package/dist/modules/site-documents/routes.admin.js +29 -0
  213. package/dist/modules/site-documents/routes.admin.js.map +1 -0
  214. package/dist/modules/site-documents/routes.public.d.ts +6 -0
  215. package/dist/modules/site-documents/routes.public.d.ts.map +1 -0
  216. package/dist/modules/site-documents/routes.public.js +16 -0
  217. package/dist/modules/site-documents/routes.public.js.map +1 -0
  218. package/dist/modules/site-documents/service.d.ts +11 -0
  219. package/dist/modules/site-documents/service.d.ts.map +1 -0
  220. package/dist/modules/site-documents/service.js +42 -0
  221. package/dist/modules/site-documents/service.js.map +1 -0
  222. package/dist/modules/webhooks/dispatch.d.ts +14 -0
  223. package/dist/modules/webhooks/dispatch.d.ts.map +1 -0
  224. package/dist/modules/webhooks/dispatch.js +84 -0
  225. package/dist/modules/webhooks/dispatch.js.map +1 -0
  226. package/dist/shared/constants.d.ts +79 -0
  227. package/dist/shared/constants.d.ts.map +1 -0
  228. package/dist/shared/constants.js +89 -0
  229. package/dist/shared/constants.js.map +1 -0
  230. package/dist/shared/envelope.d.ts +37 -0
  231. package/dist/shared/envelope.d.ts.map +1 -0
  232. package/dist/shared/envelope.js +46 -0
  233. package/dist/shared/envelope.js.map +1 -0
  234. package/dist/shared/hash.d.ts +2 -0
  235. package/dist/shared/hash.d.ts.map +1 -0
  236. package/dist/shared/hash.js +8 -0
  237. package/dist/shared/hash.js.map +1 -0
  238. package/dist/shared/index.d.ts +13 -0
  239. package/dist/shared/index.d.ts.map +1 -0
  240. package/dist/shared/index.js +13 -0
  241. package/dist/shared/index.js.map +1 -0
  242. package/dist/shared/schemas/aeo.d.ts +182 -0
  243. package/dist/shared/schemas/aeo.d.ts.map +1 -0
  244. package/dist/shared/schemas/aeo.js +43 -0
  245. package/dist/shared/schemas/aeo.js.map +1 -0
  246. package/dist/shared/schemas/analytics.d.ts +277 -0
  247. package/dist/shared/schemas/analytics.d.ts.map +1 -0
  248. package/dist/shared/schemas/analytics.js +74 -0
  249. package/dist/shared/schemas/analytics.js.map +1 -0
  250. package/dist/shared/schemas/api-key.d.ts +77 -0
  251. package/dist/shared/schemas/api-key.d.ts.map +1 -0
  252. package/dist/shared/schemas/api-key.js +27 -0
  253. package/dist/shared/schemas/api-key.js.map +1 -0
  254. package/dist/shared/schemas/author.d.ts +147 -0
  255. package/dist/shared/schemas/author.d.ts.map +1 -0
  256. package/dist/shared/schemas/author.js +27 -0
  257. package/dist/shared/schemas/author.js.map +1 -0
  258. package/dist/shared/schemas/form.d.ts +341 -0
  259. package/dist/shared/schemas/form.d.ts.map +1 -0
  260. package/dist/shared/schemas/form.js +117 -0
  261. package/dist/shared/schemas/form.js.map +1 -0
  262. package/dist/shared/schemas/index.d.ts +12 -0
  263. package/dist/shared/schemas/index.d.ts.map +1 -0
  264. package/dist/shared/schemas/index.js +12 -0
  265. package/dist/shared/schemas/index.js.map +1 -0
  266. package/dist/shared/schemas/media.d.ts +42 -0
  267. package/dist/shared/schemas/media.d.ts.map +1 -0
  268. package/dist/shared/schemas/media.js +16 -0
  269. package/dist/shared/schemas/media.js.map +1 -0
  270. package/dist/shared/schemas/pagination.d.ts +34 -0
  271. package/dist/shared/schemas/pagination.d.ts.map +1 -0
  272. package/dist/shared/schemas/pagination.js +21 -0
  273. package/dist/shared/schemas/pagination.js.map +1 -0
  274. package/dist/shared/schemas/post.d.ts +862 -0
  275. package/dist/shared/schemas/post.d.ts.map +1 -0
  276. package/dist/shared/schemas/post.js +187 -0
  277. package/dist/shared/schemas/post.js.map +1 -0
  278. package/dist/shared/schemas/settings.d.ts +839 -0
  279. package/dist/shared/schemas/settings.d.ts.map +1 -0
  280. package/dist/shared/schemas/settings.js +202 -0
  281. package/dist/shared/schemas/settings.js.map +1 -0
  282. package/dist/shared/schemas/site-document.d.ts +38 -0
  283. package/dist/shared/schemas/site-document.d.ts.map +1 -0
  284. package/dist/shared/schemas/site-document.js +21 -0
  285. package/dist/shared/schemas/site-document.js.map +1 -0
  286. package/dist/shared/schemas/webhook.d.ts +39 -0
  287. package/dist/shared/schemas/webhook.d.ts.map +1 -0
  288. package/dist/shared/schemas/webhook.js +20 -0
  289. package/dist/shared/schemas/webhook.js.map +1 -0
  290. package/dist/spa.d.ts +9 -0
  291. package/dist/spa.d.ts.map +1 -0
  292. package/dist/spa.js +57 -0
  293. package/dist/spa.js.map +1 -0
  294. package/migrations/0001_settings.sql +39 -0
  295. package/migrations/0002_content.sql +135 -0
  296. package/migrations/0003_forms.sql +35 -0
  297. package/migrations/0004_analytics.sql +42 -0
  298. package/migrations/0005_media.sql +17 -0
  299. package/migrations/0006_site_documents.sql +18 -0
  300. package/migrations/0007_auth.sql +81 -0
  301. package/migrations/0008_api_keys.sql +20 -0
  302. package/package.json +68 -0
  303. package/scripts/bundle-admin.mjs +54 -0
  304. package/seed/local.sql +63 -0
@@ -0,0 +1,62 @@
1
+ import { formSettingsSchema } from '../../shared/index.js';
2
+ import { HttpError } from '../../lib/errors.js';
3
+ export function parseFormSettings(settingsJson) {
4
+ return formSettingsSchema.parse(JSON.parse(settingsJson));
5
+ }
6
+ export function toFormRecord(row) {
7
+ return {
8
+ id: row.id,
9
+ key: row.form_key,
10
+ name: row.name,
11
+ type: row.type,
12
+ settings: parseFormSettings(row.settings),
13
+ createdAt: row.created_at,
14
+ updatedAt: row.updated_at,
15
+ archivedAt: row.archived_at,
16
+ submissionCount: row.submission_count,
17
+ lastSubmissionAt: row.last_submission_at,
18
+ };
19
+ }
20
+ export function toSubmissionRecord(row) {
21
+ return {
22
+ id: row.id,
23
+ formId: row.form_id,
24
+ formName: row.form_name,
25
+ submittedAt: row.submitted_at,
26
+ payload: JSON.parse(row.payload),
27
+ email: row.email,
28
+ name: row.name,
29
+ status: row.status,
30
+ notes: row.notes,
31
+ sourcePage: row.source_page,
32
+ referrer: row.referrer,
33
+ };
34
+ }
35
+ export async function fetchFormByKey(db, formKey) {
36
+ const row = await db
37
+ .prepare('SELECT * FROM forms WHERE form_key = ?1')
38
+ .bind(formKey)
39
+ .first();
40
+ if (!row)
41
+ throw new HttpError('not_found', 'form not found');
42
+ return row;
43
+ }
44
+ export async function fetchFormById(db, id) {
45
+ const row = await db.prepare('SELECT * FROM forms WHERE id = ?1').bind(id).first();
46
+ if (!row)
47
+ throw new HttpError('not_found', 'form not found');
48
+ return row;
49
+ }
50
+ /** Single submission joined with its form name; 404s if the id is unknown. */
51
+ export async function fetchSubmissionRow(db, id) {
52
+ const row = await db
53
+ .prepare(`SELECT submissions.*, forms.name AS form_name
54
+ FROM submissions LEFT JOIN forms ON forms.id = submissions.form_id
55
+ WHERE submissions.id = ?1`)
56
+ .bind(id)
57
+ .first();
58
+ if (!row)
59
+ throw new HttpError('not_found', 'submission not found');
60
+ return row;
61
+ }
62
+ //# sourceMappingURL=db.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"db.js","sourceRoot":"","sources":["../../../src/modules/forms/db.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAuChD,MAAM,UAAU,iBAAiB,CAAC,YAAoB;IACpD,OAAO,kBAAkB,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC,CAAC;AAC5D,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,GAAgB;IAC3C,OAAO;QACL,EAAE,EAAE,GAAG,CAAC,EAAE;QACV,GAAG,EAAE,GAAG,CAAC,QAAQ;QACjB,IAAI,EAAE,GAAG,CAAC,IAAI;QACd,IAAI,EAAE,GAAG,CAAC,IAAI;QACd,QAAQ,EAAE,iBAAiB,CAAC,GAAG,CAAC,QAAQ,CAAC;QACzC,SAAS,EAAE,GAAG,CAAC,UAAU;QACzB,SAAS,EAAE,GAAG,CAAC,UAAU;QACzB,UAAU,EAAE,GAAG,CAAC,WAAW;QAC3B,eAAe,EAAE,GAAG,CAAC,gBAAgB;QACrC,gBAAgB,EAAE,GAAG,CAAC,kBAAkB;KACzC,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,GAA0B;IAC3D,OAAO;QACL,EAAE,EAAE,GAAG,CAAC,EAAE;QACV,MAAM,EAAE,GAAG,CAAC,OAAO;QACnB,QAAQ,EAAE,GAAG,CAAC,SAAS;QACvB,WAAW,EAAE,GAAG,CAAC,YAAY;QAC7B,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,CAA4B;QAC3D,KAAK,EAAE,GAAG,CAAC,KAAK;QAChB,IAAI,EAAE,GAAG,CAAC,IAAI;QACd,MAAM,EAAE,GAAG,CAAC,MAAM;QAClB,KAAK,EAAE,GAAG,CAAC,KAAK;QAChB,UAAU,EAAE,GAAG,CAAC,WAAW;QAC3B,QAAQ,EAAE,GAAG,CAAC,QAAQ;KACvB,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,EAAc,EAAE,OAAe;IAClE,MAAM,GAAG,GAAG,MAAM,EAAE;SACjB,OAAO,CAAC,yCAAyC,CAAC;SAClD,IAAI,CAAC,OAAO,CAAC;SACb,KAAK,EAAW,CAAC;IACpB,IAAI,CAAC,GAAG;QAAE,MAAM,IAAI,SAAS,CAAC,WAAW,EAAE,gBAAgB,CAAC,CAAC;IAC7D,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,EAAc,EAAE,EAAU;IAC5D,MAAM,GAAG,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,mCAAmC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,KAAK,EAAW,CAAC;IAC5F,IAAI,CAAC,GAAG;QAAE,MAAM,IAAI,SAAS,CAAC,WAAW,EAAE,gBAAgB,CAAC,CAAC;IAC7D,OAAO,GAAG,CAAC;AACb,CAAC;AAED,8EAA8E;AAC9E,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,EAAc,EACd,EAAU;IAEV,MAAM,GAAG,GAAG,MAAM,EAAE;SACjB,OAAO,CACN;;kCAE4B,CAC7B;SACA,IAAI,CAAC,EAAE,CAAC;SACR,KAAK,EAAyB,CAAC;IAClC,IAAI,CAAC,GAAG;QAAE,MAAM,IAAI,SAAS,CAAC,WAAW,EAAE,sBAAsB,CAAC,CAAC;IACnE,OAAO,GAAG,CAAC;AACb,CAAC"}
@@ -0,0 +1,6 @@
1
+ import { Hono } from 'hono';
2
+ import type { Bindings } from '../../env.js';
3
+ export declare const formsAdminRoutes: Hono<{
4
+ Bindings: Bindings;
5
+ }, import("hono/types").BlankSchema, "/">;
6
+ //# sourceMappingURL=forms-routes.admin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"forms-routes.admin.d.ts","sourceRoot":"","sources":["../../../src/modules/forms/forms-routes.admin.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAY7C,eAAO,MAAM,gBAAgB;cAAwB,QAAQ;yCAAK,CAAC"}
@@ -0,0 +1,37 @@
1
+ import { formCreateInputSchema, formListQuerySchema, formUpdateInputSchema, ok } from '../../shared/index.js';
2
+ import { Hono } from 'hono';
3
+ import { parseOrThrow } from '../../lib/validate.js';
4
+ import { archiveForm, createForm, deleteForm, getFormAdmin, listFormsAdmin, unarchiveForm, updateForm, } from './forms-service.js';
5
+ export const formsAdminRoutes = new Hono();
6
+ formsAdminRoutes.get('/', async (c) => {
7
+ const query = parseOrThrow(formListQuerySchema, c.req.query());
8
+ const { items, total } = await listFormsAdmin(c.env.DB, query);
9
+ return c.json(ok({ items, page: query.page, pageSize: query.pageSize, total }));
10
+ });
11
+ formsAdminRoutes.post('/', async (c) => {
12
+ const input = parseOrThrow(formCreateInputSchema, await c.req.json());
13
+ const form = await createForm(c.env.DB, input);
14
+ return c.json(ok({ form }));
15
+ });
16
+ formsAdminRoutes.get('/:id', async (c) => {
17
+ const form = await getFormAdmin(c.env.DB, c.req.param('id'));
18
+ return c.json(ok({ form }));
19
+ });
20
+ formsAdminRoutes.patch('/:id', async (c) => {
21
+ const patch = parseOrThrow(formUpdateInputSchema, await c.req.json());
22
+ const form = await updateForm(c.env.DB, c.req.param('id'), patch);
23
+ return c.json(ok({ form }));
24
+ });
25
+ formsAdminRoutes.post('/:id/archive', async (c) => {
26
+ const form = await archiveForm(c.env.DB, c.req.param('id'));
27
+ return c.json(ok({ form }));
28
+ });
29
+ formsAdminRoutes.post('/:id/unarchive', async (c) => {
30
+ const form = await unarchiveForm(c.env.DB, c.req.param('id'));
31
+ return c.json(ok({ form }));
32
+ });
33
+ formsAdminRoutes.delete('/:id', async (c) => {
34
+ await deleteForm(c.env.DB, c.req.param('id'));
35
+ return c.json(ok({ deleted: true }));
36
+ });
37
+ //# sourceMappingURL=forms-routes.admin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"forms-routes.admin.js","sourceRoot":"","sources":["../../../src/modules/forms/forms-routes.admin.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ import { type FormCreateInput, type FormListQuery, type FormRecord, type FormUpdateInput } from '../../shared/index.js';
2
+ export declare function getFormAdmin(db: D1Database, id: string): Promise<FormRecord>;
3
+ export declare function listFormsAdmin(db: D1Database, query: FormListQuery): Promise<{
4
+ items: FormRecord[];
5
+ total: number;
6
+ }>;
7
+ export declare function createForm(db: D1Database, input: FormCreateInput): Promise<FormRecord>;
8
+ export declare function updateForm(db: D1Database, id: string, patch: FormUpdateInput): Promise<FormRecord>;
9
+ export declare function archiveForm(db: D1Database, id: string): Promise<FormRecord>;
10
+ export declare function unarchiveForm(db: D1Database, id: string): Promise<FormRecord>;
11
+ /**
12
+ * Hard delete, allowed only when the form has no submissions: the inbox is the
13
+ * CRM of record, so a form with leads is archived (intake stops, submissions
14
+ * stay), never deleted. Callers who want a form gone-but-with-history use
15
+ * archiveForm instead.
16
+ */
17
+ export declare function deleteForm(db: D1Database, id: string): Promise<void>;
18
+ //# sourceMappingURL=forms-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"forms-service.d.ts","sourceRoot":"","sources":["../../../src/modules/forms/forms-service.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,eAAe,EACpB,KAAK,aAAa,EAClB,KAAK,UAAU,EACf,KAAK,eAAe,EACrB,MAAM,uBAAuB,CAAC;AA6B/B,wBAAsB,YAAY,CAAC,EAAE,EAAE,UAAU,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAOlF;AAED,wBAAsB,cAAc,CAClC,EAAE,EAAE,UAAU,EACd,KAAK,EAAE,aAAa,GACnB,OAAO,CAAC;IAAE,KAAK,EAAE,UAAU,EAAE,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAgBjD;AAED,wBAAsB,UAAU,CAAC,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,UAAU,CAAC,CA6B5F;AAED,wBAAsB,UAAU,CAC9B,EAAE,EAAE,UAAU,EACd,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,eAAe,GACrB,OAAO,CAAC,UAAU,CAAC,CAiBrB;AAED,wBAAsB,WAAW,CAAC,EAAE,EAAE,UAAU,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CASjF;AAED,wBAAsB,aAAa,CAAC,EAAE,EAAE,UAAU,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAOnF;AAED;;;;;GAKG;AACH,wBAAsB,UAAU,CAAC,EAAE,EAAE,UAAU,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAa1E"}
@@ -0,0 +1,121 @@
1
+ import { formSettingsSchema, } from '../../shared/index.js';
2
+ import { HttpError } from '../../lib/errors.js';
3
+ import { fetchFormById, toFormRecord } from './db.js';
4
+ /**
5
+ * Slugify a form name into a URL/embed-safe key: lowercase, non-alphanumeric
6
+ * runs become single hyphens, trimmed. Empty result (e.g. a name of only
7
+ * punctuation) falls back to "form" so a key always exists.
8
+ */
9
+ function slugify(name) {
10
+ const slug = name
11
+ .toLowerCase()
12
+ .replace(/[^a-z0-9]+/g, '-')
13
+ .replace(/^-+|-+$/g, '');
14
+ return slug || 'form';
15
+ }
16
+ async function keyExists(db, key) {
17
+ const row = await db.prepare('SELECT 1 FROM forms WHERE form_key = ?1').bind(key).first();
18
+ return row !== null;
19
+ }
20
+ /**
21
+ * The submission-aggregate query shared by list and get: LEFT JOIN so a form
22
+ * with zero submissions still returns a row (count 0, last null).
23
+ */
24
+ const FORM_SELECT = `SELECT forms.*, COUNT(submissions.id) AS submission_count, MAX(submissions.submitted_at) AS last_submission_at
25
+ FROM forms LEFT JOIN submissions ON submissions.form_id = forms.id`;
26
+ export async function getFormAdmin(db, id) {
27
+ const row = await db
28
+ .prepare(`${FORM_SELECT} WHERE forms.id = ?1 GROUP BY forms.id`)
29
+ .bind(id)
30
+ .first();
31
+ if (!row)
32
+ throw new HttpError('not_found', 'form not found');
33
+ return toFormRecord(row);
34
+ }
35
+ export async function listFormsAdmin(db, query) {
36
+ const offset = (query.page - 1) * query.pageSize;
37
+ const where = query.includeArchived ? '' : 'WHERE forms.archived_at IS NULL';
38
+ const { results } = await db
39
+ .prepare(`${FORM_SELECT} ${where} GROUP BY forms.id ORDER BY forms.created_at DESC LIMIT ?1 OFFSET ?2`)
40
+ .bind(query.pageSize, offset)
41
+ .all();
42
+ const countRow = await db
43
+ .prepare(`SELECT COUNT(*) as count FROM forms ${where}`)
44
+ .first();
45
+ return { items: results.map(toFormRecord), total: countRow?.count ?? 0 };
46
+ }
47
+ export async function createForm(db, input) {
48
+ // An explicit key must be unique as given (surprising the caller with a
49
+ // suffix would break the embed contract they typed); a derived key silently
50
+ // suffixes until free so create never fails on a name collision.
51
+ let key;
52
+ if (input.key) {
53
+ if (await keyExists(db, input.key)) {
54
+ throw new HttpError('validation_error', `a form with key "${input.key}" already exists`);
55
+ }
56
+ key = input.key;
57
+ }
58
+ else {
59
+ const base = slugify(input.name);
60
+ key = base;
61
+ for (let n = 2; await keyExists(db, key); n++) {
62
+ key = `${base}-${n}`;
63
+ }
64
+ }
65
+ const settings = formSettingsSchema.parse(input.settings ?? {});
66
+ const id = crypto.randomUUID();
67
+ await db
68
+ .prepare(`INSERT INTO forms (id, form_key, name, type, settings, updated_at)
69
+ VALUES (?1, ?2, ?3, ?4, ?5, datetime('now'))`)
70
+ .bind(id, key, input.name, input.type, JSON.stringify(settings))
71
+ .run();
72
+ return getFormAdmin(db, id);
73
+ }
74
+ export async function updateForm(db, id, patch) {
75
+ const row = await fetchFormById(db, id);
76
+ // Replace-wholesale when settings is present (the repo's nested-update
77
+ // convention, R2; reaffirmed by the R7 review): the schema validated the
78
+ // full object, and an omitted optional field (notifyEmails, successMessage)
79
+ // means "cleared". A field-level merge would make clearing impossible over
80
+ // JSON, since undefined-valued keys vanish in JSON.stringify.
81
+ const settings = patch.settings ?? formSettingsSchema.parse(JSON.parse(row.settings));
82
+ await db
83
+ .prepare(`UPDATE forms SET name = ?2, type = ?3, settings = ?4, updated_at = datetime('now') WHERE id = ?1`)
84
+ .bind(id, patch.name ?? row.name, patch.type ?? row.type, JSON.stringify(settings))
85
+ .run();
86
+ return getFormAdmin(db, id);
87
+ }
88
+ export async function archiveForm(db, id) {
89
+ await fetchFormById(db, id);
90
+ await db
91
+ .prepare(`UPDATE forms SET archived_at = datetime('now'), updated_at = datetime('now') WHERE id = ?1`)
92
+ .bind(id)
93
+ .run();
94
+ return getFormAdmin(db, id);
95
+ }
96
+ export async function unarchiveForm(db, id) {
97
+ await fetchFormById(db, id);
98
+ await db
99
+ .prepare(`UPDATE forms SET archived_at = NULL, updated_at = datetime('now') WHERE id = ?1`)
100
+ .bind(id)
101
+ .run();
102
+ return getFormAdmin(db, id);
103
+ }
104
+ /**
105
+ * Hard delete, allowed only when the form has no submissions: the inbox is the
106
+ * CRM of record, so a form with leads is archived (intake stops, submissions
107
+ * stay), never deleted. Callers who want a form gone-but-with-history use
108
+ * archiveForm instead.
109
+ */
110
+ export async function deleteForm(db, id) {
111
+ await fetchFormById(db, id);
112
+ const countRow = await db
113
+ .prepare('SELECT COUNT(*) as count FROM submissions WHERE form_id = ?1')
114
+ .bind(id)
115
+ .first();
116
+ if ((countRow?.count ?? 0) > 0) {
117
+ throw new HttpError('validation_error', 'this form has submissions; archive it instead of deleting');
118
+ }
119
+ await db.prepare('DELETE FROM forms WHERE id = ?1').bind(id).run();
120
+ }
121
+ //# sourceMappingURL=forms-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"forms-service.js","sourceRoot":"","sources":["../../../src/modules/forms/forms-service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ import { Hono } from 'hono';
2
+ import { type Bindings } from '../../env.js';
3
+ export declare const formsPublicRoutes: Hono<{
4
+ Bindings: Bindings;
5
+ }, import("hono/types").BlankSchema, "/">;
6
+ //# sourceMappingURL=routes.public.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.public.d.ts","sourceRoot":"","sources":["../../../src/modules/forms/routes.public.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,EAAe,KAAK,QAAQ,EAAE,MAAM,cAAc,CAAC;AAQ1D,eAAO,MAAM,iBAAiB;cAAwB,QAAQ;yCAAK,CAAC"}
@@ -0,0 +1,41 @@
1
+ import { ok, sha256Hex } from '../../shared/index.js';
2
+ import { Hono } from 'hono';
3
+ import { getSettings } from '../../env.js';
4
+ import { resolveEmailSender, sendBestEffort } from '../../lib/email.js';
5
+ import { buildSubmissionNotification } from '../../lib/notify.js';
6
+ import { corsMiddleware, enforceOrigin } from '../../middleware/cors.js';
7
+ import { rateLimit } from '../../middleware/rate-limit.js';
8
+ import { dispatchWebhook } from '../webhooks/dispatch.js';
9
+ import { submitForm } from './submissions-service.js';
10
+ export const formsPublicRoutes = new Hono();
11
+ formsPublicRoutes.use('/:formKey/submissions', corsMiddleware('publicWrite'), enforceOrigin, rateLimit('formsBurst'), rateLimit('formsHourly'));
12
+ formsPublicRoutes.post('/:formKey/submissions', async (c) => {
13
+ const body = (await c.req.json().catch(() => ({})));
14
+ const ip = c.req.header('CF-Connecting-IP');
15
+ const result = await submitForm(c.env.DB, c.req.param('formKey'), body, c.env.TURNSTILE_SECRET, {
16
+ ipHash: ip ? await sha256Hex(ip) : null,
17
+ userAgent: c.req.header('User-Agent') ?? null,
18
+ referrer: c.req.header('Referer') ?? null,
19
+ });
20
+ if (result.id) {
21
+ const settings = await getSettings(c.env.DB);
22
+ // A form's own notifyEmails overrides the site-wide default when set.
23
+ const notifyEmails = result.notifyEmailsOverride ?? settings.notifyEmails;
24
+ // Best effort by construction: the submission is already stored, so the
25
+ // visitor gets their success regardless of what the mail transport does.
26
+ // With no sender configured this drops the message and moves on.
27
+ await sendBestEffort(resolveEmailSender(c.env), buildSubmissionNotification(notifyEmails, c.env.SITE_URL, {
28
+ email: result.email,
29
+ name: result.name,
30
+ message: typeof body['message'] === 'string' ? body['message'] : null,
31
+ sourcePage: result.sourcePage,
32
+ }));
33
+ await dispatchWebhook(c.env.DB, c.executionCtx, c.env.SITE_URL, 'form.submission.created', {
34
+ submissionId: result.id,
35
+ formId: result.formId,
36
+ email: result.email,
37
+ });
38
+ }
39
+ return c.json(ok({ id: result.id }));
40
+ });
41
+ //# sourceMappingURL=routes.public.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.public.js","sourceRoot":"","sources":["../../../src/modules/forms/routes.public.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ import { Hono } from 'hono';
2
+ import type { Bindings } from '../../env.js';
3
+ export declare const submissionsAdminRoutes: Hono<{
4
+ Bindings: Bindings;
5
+ }, import("hono/types").BlankSchema, "/">;
6
+ //# sourceMappingURL=submissions-routes.admin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"submissions-routes.admin.d.ts","sourceRoot":"","sources":["../../../src/modules/forms/submissions-routes.admin.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAW7C,eAAO,MAAM,sBAAsB;cAAwB,QAAQ;yCAAK,CAAC"}
@@ -0,0 +1,46 @@
1
+ import { ok, statsQuerySchema, submissionListQuerySchema, submissionUpdateInputSchema, SUBMISSIONS_STATS_MAX_DAYS, } from '../../shared/index.js';
2
+ import { Hono } from 'hono';
3
+ import { requireOwner } from '../../middleware/auth.js';
4
+ import { parseOrThrow } from '../../lib/validate.js';
5
+ import { exportSubmissionsCsv, getSubmissionAdmin, getSubmissionsStats, listSubmissionsAdmin, updateSubmissionAdmin, } from './submissions-service.js';
6
+ export const submissionsAdminRoutes = new Hono();
7
+ // The whole inbox is other people's personal data (list, detail, stats, CSV
8
+ // export, status update), so it is OWNER-ONLY. Applied at the router so EVERY
9
+ // route here, including the bare `/` list, is gated independently of the mount
10
+ // prefix. sessionAuth has already authenticated the request; this narrows which
11
+ // authenticated user.
12
+ submissionsAdminRoutes.use('*', requireOwner);
13
+ submissionsAdminRoutes.get('/', async (c) => {
14
+ const query = parseOrThrow(submissionListQuerySchema, c.req.query());
15
+ const { items, total } = await listSubmissionsAdmin(c.env.DB, query);
16
+ return c.json(ok({ items, page: query.page, pageSize: query.pageSize, total }));
17
+ });
18
+ // Registered BEFORE /:id below, same reasoning as /export.csv further down:
19
+ // an unconstrained :id would otherwise swallow the literal "stats" segment.
20
+ submissionsAdminRoutes.get('/stats', async (c) => {
21
+ const query = parseOrThrow(statsQuerySchema(SUBMISSIONS_STATS_MAX_DAYS), c.req.query());
22
+ const stats = await getSubmissionsStats(c.env.DB, query.days);
23
+ return c.json(ok(stats));
24
+ });
25
+ submissionsAdminRoutes.get('/export.csv', async (c) => {
26
+ const query = c.req.query();
27
+ const csv = await exportSubmissionsCsv(c.env.DB, {
28
+ status: query['status'],
29
+ search: query['search'],
30
+ formId: query['formId'],
31
+ });
32
+ return c.body(csv, 200, {
33
+ 'Content-Type': 'text/csv; charset=UTF-8',
34
+ 'Content-Disposition': 'attachment; filename="submissions.csv"',
35
+ });
36
+ });
37
+ submissionsAdminRoutes.get('/:id', async (c) => {
38
+ const submission = await getSubmissionAdmin(c.env.DB, c.req.param('id'));
39
+ return c.json(ok({ submission }));
40
+ });
41
+ submissionsAdminRoutes.patch('/:id', async (c) => {
42
+ const patch = parseOrThrow(submissionUpdateInputSchema, await c.req.json());
43
+ const submission = await updateSubmissionAdmin(c.env.DB, c.req.param('id'), patch);
44
+ return c.json(ok({ submission }));
45
+ });
46
+ //# sourceMappingURL=submissions-routes.admin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"submissions-routes.admin.js","sourceRoot":"","sources":["../../../src/modules/forms/submissions-routes.admin.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,45 @@
1
+ import { type SubmissionListQuery, type SubmissionRecord, type SubmissionsStats, type SubmissionUpdateInput } from '../../shared/index.js';
2
+ export interface SubmissionContext {
3
+ ipHash: string | null;
4
+ userAgent: string | null;
5
+ referrer: string | null;
6
+ }
7
+ export interface SubmitFormResult {
8
+ /** null when the honeypot triggered: accepted silently, nothing was stored. */
9
+ id: string | null;
10
+ formId: string;
11
+ email: string | null;
12
+ name: string | null;
13
+ sourcePage: string | null;
14
+ /** This form's settings.notifyEmails override, if configured; undefined falls back to site-wide notifyEmails. */
15
+ notifyEmailsOverride: string[] | undefined;
16
+ }
17
+ /**
18
+ * The full public submission flow, in spec order: honeypot (before schema
19
+ * validation, so a bot's malformed payload is still swallowed silently rather
20
+ * than surfaced as a 400) -> schema validation -> payload size cap -> Turnstile
21
+ * (if the form has it enabled) -> 5-minute email-or-IP dedupe -> store. An
22
+ * archived form 404s like an unknown key (intake stops, existing submissions
23
+ * stay). Resend notification and webhook dispatch are NOT here: both are
24
+ * HTTP/env-layer side effects fired by the route handler after this returns.
25
+ */
26
+ export declare function submitForm(db: D1Database, formKey: string, rawBody: Record<string, unknown>, turnstileSecret: string | undefined, context: SubmissionContext): Promise<SubmitFormResult>;
27
+ export declare function getSubmissionAdmin(db: D1Database, id: string): Promise<SubmissionRecord>;
28
+ export declare function listSubmissionsAdmin(db: D1Database, query: SubmissionListQuery): Promise<{
29
+ items: SubmissionRecord[];
30
+ total: number;
31
+ }>;
32
+ export declare function updateSubmissionAdmin(db: D1Database, id: string, patch: SubmissionUpdateInput): Promise<SubmissionRecord>;
33
+ /**
34
+ * The inbox's own stats: getVisitStats's daily-aggregate pattern applied to
35
+ * submissions instead of page_visits, so the two "stats over a day window"
36
+ * queries in this package stay structurally identical.
37
+ */
38
+ export declare function getSubmissionsStats(db: D1Database, days: number): Promise<SubmissionsStats>;
39
+ /** All submissions matching the same filters as listSubmissionsAdmin, unpaginated, as CSV text. */
40
+ export declare function exportSubmissionsCsv(db: D1Database, filters: {
41
+ status?: string | undefined;
42
+ search?: string | undefined;
43
+ formId?: string | undefined;
44
+ }): Promise<string>;
45
+ //# sourceMappingURL=submissions-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"submissions-service.d.ts","sourceRoot":"","sources":["../../../src/modules/forms/submissions-service.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,mBAAmB,EACxB,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,KAAK,qBAAqB,EAC3B,MAAM,uBAAuB,CAAC;AAe/B,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED,MAAM,WAAW,gBAAgB;IAC/B,+EAA+E;IAC/E,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,iHAAiH;IACjH,oBAAoB,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;CAC5C;AAED;;;;;;;;GAQG;AACH,wBAAsB,UAAU,CAC9B,EAAE,EAAE,UAAU,EACd,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAChC,eAAe,EAAE,MAAM,GAAG,SAAS,EACnC,OAAO,EAAE,iBAAiB,GACzB,OAAO,CAAC,gBAAgB,CAAC,CA8E3B;AAED,wBAAsB,kBAAkB,CAAC,EAAE,EAAE,UAAU,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAE9F;AA4BD,wBAAsB,oBAAoB,CACxC,EAAE,EAAE,UAAU,EACd,KAAK,EAAE,mBAAmB,GACzB,OAAO,CAAC;IAAE,KAAK,EAAE,gBAAgB,EAAE,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAoBvD;AAED,wBAAsB,qBAAqB,CACzC,EAAE,EAAE,UAAU,EACd,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,qBAAqB,GAC3B,OAAO,CAAC,gBAAgB,CAAC,CAO3B;AAED;;;;GAIG;AACH,wBAAsB,mBAAmB,CAAC,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAgCjG;AASD,mGAAmG;AACnG,wBAAsB,oBAAoB,CACxC,EAAE,EAAE,UAAU,EACd,OAAO,EAAE;IACP,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC7B,GACA,OAAO,CAAC,MAAM,CAAC,CA+BjB"}
@@ -0,0 +1,174 @@
1
+ import { submissionCreateInputSchema, } from '../../shared/index.js';
2
+ import { HttpError } from '../../lib/errors.js';
3
+ import { verifyTurnstile } from '../../lib/turnstile.js';
4
+ import { parseOrThrow } from '../../lib/validate.js';
5
+ import { fetchFormByKey, fetchSubmissionRow, parseFormSettings, toSubmissionRecord, } from './db.js';
6
+ const MAX_PAYLOAD_BYTES = 10_000;
7
+ const DEDUPE_WINDOW_MINUTES = 5;
8
+ /**
9
+ * The full public submission flow, in spec order: honeypot (before schema
10
+ * validation, so a bot's malformed payload is still swallowed silently rather
11
+ * than surfaced as a 400) -> schema validation -> payload size cap -> Turnstile
12
+ * (if the form has it enabled) -> 5-minute email-or-IP dedupe -> store. An
13
+ * archived form 404s like an unknown key (intake stops, existing submissions
14
+ * stay). Resend notification and webhook dispatch are NOT here: both are
15
+ * HTTP/env-layer side effects fired by the route handler after this returns.
16
+ */
17
+ export async function submitForm(db, formKey, rawBody, turnstileSecret, context) {
18
+ const form = await fetchFormByKey(db, formKey);
19
+ if (form.archived_at)
20
+ throw new HttpError('not_found', 'form not found');
21
+ const settings = parseFormSettings(form.settings);
22
+ const honeypotValue = rawBody[settings.honeypotField];
23
+ if (typeof honeypotValue === 'string' && honeypotValue.trim().length > 0) {
24
+ return {
25
+ id: null,
26
+ formId: form.id,
27
+ email: null,
28
+ name: null,
29
+ sourcePage: null,
30
+ notifyEmailsOverride: settings.notifyEmails,
31
+ };
32
+ }
33
+ const input = parseOrThrow(submissionCreateInputSchema, rawBody);
34
+ const payloadJson = JSON.stringify(rawBody);
35
+ if (payloadJson.length > MAX_PAYLOAD_BYTES) {
36
+ throw new HttpError('payload_too_large', 'submission payload too large');
37
+ }
38
+ if (settings.turnstileEnabled) {
39
+ const verified = await verifyTurnstile(turnstileSecret, input.turnstileToken, context.ipHash ?? undefined);
40
+ if (!verified) {
41
+ throw new HttpError('validation_error', 'Turnstile verification failed');
42
+ }
43
+ }
44
+ const dedupeCondition = context.ipHash ? 'email = ?1 OR ip_hash = ?2' : 'email = ?1';
45
+ const dedupeBindings = context.ipHash ? [input.email, context.ipHash] : [input.email];
46
+ const duplicate = await db
47
+ .prepare(`SELECT id FROM submissions WHERE (${dedupeCondition}) AND submitted_at > datetime('now', ?${dedupeBindings.length + 1})`)
48
+ .bind(...dedupeBindings, `-${DEDUPE_WINDOW_MINUTES} minutes`)
49
+ .first();
50
+ if (duplicate) {
51
+ throw new HttpError('rate_limited', 'a submission from this email or IP was already received recently');
52
+ }
53
+ const id = crypto.randomUUID();
54
+ await db
55
+ .prepare(`INSERT INTO submissions
56
+ (id, form_id, payload, email, name, source_page, referrer, ip_hash, user_agent)
57
+ VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)`)
58
+ .bind(id, form.id, payloadJson, input.email, input.name ?? null, input.sourcePage ?? null, context.referrer, context.ipHash, context.userAgent)
59
+ .run();
60
+ return {
61
+ id,
62
+ formId: form.id,
63
+ email: input.email,
64
+ name: input.name ?? null,
65
+ sourcePage: input.sourcePage ?? null,
66
+ notifyEmailsOverride: settings.notifyEmails,
67
+ };
68
+ }
69
+ export async function getSubmissionAdmin(db, id) {
70
+ return toSubmissionRecord(await fetchSubmissionRow(db, id));
71
+ }
72
+ /** Builds the shared WHERE clause + bindings for the list and CSV export queries. */
73
+ function submissionFilters(filters) {
74
+ const conditions = [];
75
+ const bindings = [];
76
+ if (filters.status) {
77
+ conditions.push(`submissions.status = ?${bindings.length + 1}`);
78
+ bindings.push(filters.status);
79
+ }
80
+ if (filters.formId) {
81
+ conditions.push(`submissions.form_id = ?${bindings.length + 1}`);
82
+ bindings.push(filters.formId);
83
+ }
84
+ if (filters.search) {
85
+ conditions.push(`(submissions.email LIKE ?${bindings.length + 1} OR submissions.name LIKE ?${bindings.length + 1})`);
86
+ bindings.push(`%${filters.search}%`);
87
+ }
88
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
89
+ return { where, bindings };
90
+ }
91
+ export async function listSubmissionsAdmin(db, query) {
92
+ const offset = (query.page - 1) * query.pageSize;
93
+ const { where, bindings } = submissionFilters(query);
94
+ const { results } = await db
95
+ .prepare(`SELECT submissions.*, forms.name AS form_name
96
+ FROM submissions LEFT JOIN forms ON forms.id = submissions.form_id
97
+ ${where}
98
+ ORDER BY submissions.submitted_at DESC LIMIT ?${bindings.length + 1} OFFSET ?${bindings.length + 2}`)
99
+ .bind(...bindings, query.pageSize, offset)
100
+ .all();
101
+ const countRow = await db
102
+ .prepare(`SELECT COUNT(*) as count FROM submissions ${where}`)
103
+ .bind(...bindings)
104
+ .first();
105
+ return { items: results.map(toSubmissionRecord), total: countRow?.count ?? 0 };
106
+ }
107
+ export async function updateSubmissionAdmin(db, id, patch) {
108
+ const row = await fetchSubmissionRow(db, id);
109
+ await db
110
+ .prepare('UPDATE submissions SET status=?2, notes=?3 WHERE id=?1')
111
+ .bind(id, patch.status ?? row.status, patch.notes !== undefined ? patch.notes : row.notes)
112
+ .run();
113
+ return getSubmissionAdmin(db, id);
114
+ }
115
+ /**
116
+ * The inbox's own stats: getVisitStats's daily-aggregate pattern applied to
117
+ * submissions instead of page_visits, so the two "stats over a day window"
118
+ * queries in this package stay structurally identical.
119
+ */
120
+ export async function getSubmissionsStats(db, days) {
121
+ const since = `-${days} days`;
122
+ const totals = await db
123
+ .prepare(`SELECT COUNT(*) as total FROM submissions WHERE submitted_at > datetime('now', ?1)`)
124
+ .bind(since)
125
+ .first();
126
+ const byForm = (await db
127
+ .prepare(`SELECT submissions.form_id as formId, forms.name as formName, COUNT(*) as count
128
+ FROM submissions LEFT JOIN forms ON forms.id = submissions.form_id
129
+ WHERE submissions.submitted_at > datetime('now', ?1)
130
+ GROUP BY submissions.form_id ORDER BY count DESC`)
131
+ .bind(since)
132
+ .all()).results;
133
+ const daily = (await db
134
+ .prepare(`SELECT date(submitted_at) as date, COUNT(*) as count FROM submissions
135
+ WHERE submitted_at > datetime('now', ?1)
136
+ GROUP BY date(submitted_at) ORDER BY date ASC`)
137
+ .bind(since)
138
+ .all()).results;
139
+ return { total: totals?.total ?? 0, byForm, daily };
140
+ }
141
+ function csvEscape(value) {
142
+ if (/[",\n]/.test(value)) {
143
+ return `"${value.replace(/"/g, '""')}"`;
144
+ }
145
+ return value;
146
+ }
147
+ /** All submissions matching the same filters as listSubmissionsAdmin, unpaginated, as CSV text. */
148
+ export async function exportSubmissionsCsv(db, filters) {
149
+ const { where, bindings } = submissionFilters(filters);
150
+ const { results } = await db
151
+ .prepare(`SELECT submissions.*, forms.name AS form_name
152
+ FROM submissions LEFT JOIN forms ON forms.id = submissions.form_id
153
+ ${where} ORDER BY submissions.submitted_at DESC`)
154
+ .bind(...bindings)
155
+ .all();
156
+ const header = ['id', 'submittedAt', 'form', 'email', 'name', 'status', 'sourcePage', 'notes'];
157
+ const lines = [header.join(',')];
158
+ for (const row of results) {
159
+ lines.push([
160
+ row.id,
161
+ row.submitted_at,
162
+ row.form_name ?? '',
163
+ row.email ?? '',
164
+ row.name ?? '',
165
+ row.status,
166
+ row.source_page ?? '',
167
+ row.notes ?? '',
168
+ ]
169
+ .map((value) => csvEscape(String(value)))
170
+ .join(','));
171
+ }
172
+ return lines.join('\n');
173
+ }
174
+ //# sourceMappingURL=submissions-service.js.map