@brandlab-digital/site-next 3.13.0 → 3.15.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 (274) hide show
  1. package/README.md +79 -0
  2. package/dist/analytics.d.ts +1 -1
  3. package/dist/analytics.d.ts.map +1 -1
  4. package/dist/analytics.js +2 -0
  5. package/dist/analytics.js.map +1 -1
  6. package/dist/contact/delivery.d.ts +81 -0
  7. package/dist/contact/delivery.d.ts.map +1 -0
  8. package/dist/contact/delivery.js +138 -0
  9. package/dist/contact/delivery.js.map +1 -0
  10. package/dist/contact/resend-webhook.d.ts +72 -0
  11. package/dist/contact/resend-webhook.d.ts.map +1 -0
  12. package/dist/contact/resend-webhook.js +300 -0
  13. package/dist/contact/resend-webhook.js.map +1 -0
  14. package/dist/contact/route-handler.d.ts +16 -0
  15. package/dist/contact/route-handler.d.ts.map +1 -1
  16. package/dist/contact/route-handler.js +8 -0
  17. package/dist/contact/route-handler.js.map +1 -1
  18. package/dist/contact/server-entry.d.ts +2 -0
  19. package/dist/contact/server-entry.d.ts.map +1 -1
  20. package/dist/contact/server-entry.js +2 -0
  21. package/dist/contact/server-entry.js.map +1 -1
  22. package/dist/contact/server.d.ts +26 -0
  23. package/dist/contact/server.d.ts.map +1 -1
  24. package/dist/contact/server.js +48 -3
  25. package/dist/contact/server.js.map +1 -1
  26. package/dist/navigation/constants.d.ts +11 -2
  27. package/dist/navigation/constants.d.ts.map +1 -1
  28. package/dist/navigation/constants.js +11 -2
  29. package/dist/navigation/constants.js.map +1 -1
  30. package/dist/navigation/index.d.ts +1 -1
  31. package/dist/navigation/index.d.ts.map +1 -1
  32. package/dist/navigation/index.js +1 -1
  33. package/dist/navigation/index.js.map +1 -1
  34. package/dist/navigation/use-back-to-top.d.ts.map +1 -1
  35. package/dist/navigation/use-back-to-top.js +12 -4
  36. package/dist/navigation/use-back-to-top.js.map +1 -1
  37. package/dist/navigation/use-header-metrics.d.ts +1 -1
  38. package/dist/navigation/use-header-metrics.js +1 -1
  39. package/dist/navigation/use-header-metrics.js.map +1 -1
  40. package/dist/navigation/use-sticky-call-bar.d.ts +2 -1
  41. package/dist/navigation/use-sticky-call-bar.d.ts.map +1 -1
  42. package/dist/navigation/use-sticky-call-bar.js +11 -2
  43. package/dist/navigation/use-sticky-call-bar.js.map +1 -1
  44. package/dist/next-config.d.ts +17 -1
  45. package/dist/next-config.d.ts.map +1 -1
  46. package/dist/next-config.js +36 -2
  47. package/dist/next-config.js.map +1 -1
  48. package/dist/ui-kit/brandlab-skin.d.ts +50 -1
  49. package/dist/ui-kit/brandlab-skin.d.ts.map +1 -1
  50. package/dist/ui-kit/brandlab-skin.js.map +1 -1
  51. package/dist/ui-kit/form/photo-upload.d.ts +4 -1
  52. package/dist/ui-kit/form/photo-upload.d.ts.map +1 -1
  53. package/dist/ui-kit/form/photo-upload.js +4 -4
  54. package/dist/ui-kit/form/photo-upload.js.map +1 -1
  55. package/dist/ui-kit/form/quote-form.d.ts +4 -1
  56. package/dist/ui-kit/form/quote-form.d.ts.map +1 -1
  57. package/dist/ui-kit/form/quote-form.js.map +1 -1
  58. package/dist/ui-kit/form/thank-you.d.ts +4 -1
  59. package/dist/ui-kit/form/thank-you.d.ts.map +1 -1
  60. package/dist/ui-kit/form/thank-you.js +2 -2
  61. package/dist/ui-kit/form/thank-you.js.map +1 -1
  62. package/dist/ui-kit/icon-glyphs.d.ts +41 -0
  63. package/dist/ui-kit/icon-glyphs.d.ts.map +1 -0
  64. package/dist/ui-kit/icon-glyphs.js +138 -0
  65. package/dist/ui-kit/icon-glyphs.js.map +1 -0
  66. package/dist/ui-kit/icon-sprite.d.ts +19 -0
  67. package/dist/ui-kit/icon-sprite.d.ts.map +1 -0
  68. package/dist/ui-kit/icon-sprite.js +44 -0
  69. package/dist/ui-kit/icon-sprite.js.map +1 -0
  70. package/dist/ui-kit/icons.d.ts +38 -3
  71. package/dist/ui-kit/icons.d.ts.map +1 -1
  72. package/dist/ui-kit/icons.js +47 -64
  73. package/dist/ui-kit/icons.js.map +1 -1
  74. package/dist/ui-kit/prefetch.d.ts +19 -10
  75. package/dist/ui-kit/prefetch.d.ts.map +1 -1
  76. package/dist/ui-kit/prefetch.js +23 -10
  77. package/dist/ui-kit/prefetch.js.map +1 -1
  78. package/dist/ui-kit/primitives/badge.d.ts +25 -0
  79. package/dist/ui-kit/primitives/badge.d.ts.map +1 -0
  80. package/dist/ui-kit/primitives/badge.js +21 -0
  81. package/dist/ui-kit/primitives/badge.js.map +1 -0
  82. package/dist/ui-kit/primitives/breadcrumbs.d.ts +27 -0
  83. package/dist/ui-kit/primitives/breadcrumbs.d.ts.map +1 -0
  84. package/dist/ui-kit/primitives/breadcrumbs.js +40 -0
  85. package/dist/ui-kit/primitives/breadcrumbs.js.map +1 -0
  86. package/dist/ui-kit/primitives/button.d.ts +32 -0
  87. package/dist/ui-kit/primitives/button.d.ts.map +1 -0
  88. package/dist/ui-kit/primitives/button.js +8 -0
  89. package/dist/ui-kit/primitives/button.js.map +1 -0
  90. package/dist/ui-kit/primitives/card.d.ts +40 -0
  91. package/dist/ui-kit/primitives/card.d.ts.map +1 -0
  92. package/dist/ui-kit/primitives/card.js +8 -0
  93. package/dist/ui-kit/primitives/card.js.map +1 -0
  94. package/dist/ui-kit/primitives/index.d.ts +11 -2
  95. package/dist/ui-kit/primitives/index.d.ts.map +1 -1
  96. package/dist/ui-kit/primitives/index.js +11 -2
  97. package/dist/ui-kit/primitives/index.js.map +1 -1
  98. package/dist/ui-kit/primitives/layout.d.ts +12 -0
  99. package/dist/ui-kit/primitives/layout.d.ts.map +1 -1
  100. package/dist/ui-kit/primitives/layout.js +20 -2
  101. package/dist/ui-kit/primitives/layout.js.map +1 -1
  102. package/dist/ui-kit/primitives/links.d.ts +15 -0
  103. package/dist/ui-kit/primitives/links.d.ts.map +1 -1
  104. package/dist/ui-kit/primitives/links.js +6 -2
  105. package/dist/ui-kit/primitives/links.js.map +1 -1
  106. package/dist/ui-kit/primitives/lists.d.ts +51 -0
  107. package/dist/ui-kit/primitives/lists.d.ts.map +1 -0
  108. package/dist/ui-kit/primitives/lists.js +34 -0
  109. package/dist/ui-kit/primitives/lists.js.map +1 -0
  110. package/dist/ui-kit/primitives/media-frame.d.ts +28 -0
  111. package/dist/ui-kit/primitives/media-frame.d.ts.map +1 -0
  112. package/dist/ui-kit/primitives/media-frame.js +58 -0
  113. package/dist/ui-kit/primitives/media-frame.js.map +1 -0
  114. package/dist/ui-kit/primitives/media.d.ts +38 -0
  115. package/dist/ui-kit/primitives/media.d.ts.map +1 -0
  116. package/dist/ui-kit/primitives/media.js +9 -0
  117. package/dist/ui-kit/primitives/media.js.map +1 -0
  118. package/dist/ui-kit/primitives/quote-button.d.ts +25 -2
  119. package/dist/ui-kit/primitives/quote-button.d.ts.map +1 -1
  120. package/dist/ui-kit/primitives/quote-button.js +25 -3
  121. package/dist/ui-kit/primitives/quote-button.js.map +1 -1
  122. package/dist/ui-kit/primitives/stat.d.ts +25 -0
  123. package/dist/ui-kit/primitives/stat.d.ts.map +1 -0
  124. package/dist/ui-kit/primitives/stat.js +7 -0
  125. package/dist/ui-kit/primitives/stat.js.map +1 -0
  126. package/dist/ui-kit/repeated-slots.d.ts +153 -0
  127. package/dist/ui-kit/repeated-slots.d.ts.map +1 -0
  128. package/dist/ui-kit/repeated-slots.js +97 -0
  129. package/dist/ui-kit/repeated-slots.js.map +1 -0
  130. package/dist/ui-kit/sections/accordion.d.ts +4 -1
  131. package/dist/ui-kit/sections/accordion.d.ts.map +1 -1
  132. package/dist/ui-kit/sections/accordion.js +2 -2
  133. package/dist/ui-kit/sections/accordion.js.map +1 -1
  134. package/dist/ui-kit/sections/area-map.d.ts +64 -0
  135. package/dist/ui-kit/sections/area-map.d.ts.map +1 -0
  136. package/dist/ui-kit/sections/area-map.js +142 -0
  137. package/dist/ui-kit/sections/area-map.js.map +1 -0
  138. package/dist/ui-kit/sections/before-after.d.ts +59 -0
  139. package/dist/ui-kit/sections/before-after.d.ts.map +1 -0
  140. package/dist/ui-kit/sections/before-after.js +42 -0
  141. package/dist/ui-kit/sections/before-after.js.map +1 -0
  142. package/dist/ui-kit/sections/contact-block.d.ts +4 -1
  143. package/dist/ui-kit/sections/contact-block.d.ts.map +1 -1
  144. package/dist/ui-kit/sections/contact-block.js +3 -3
  145. package/dist/ui-kit/sections/contact-block.js.map +1 -1
  146. package/dist/ui-kit/sections/council-rules.d.ts +64 -0
  147. package/dist/ui-kit/sections/council-rules.d.ts.map +1 -0
  148. package/dist/ui-kit/sections/council-rules.js +46 -0
  149. package/dist/ui-kit/sections/council-rules.js.map +1 -0
  150. package/dist/ui-kit/sections/coverage.d.ts +53 -0
  151. package/dist/ui-kit/sections/coverage.d.ts.map +1 -0
  152. package/dist/ui-kit/sections/coverage.js +57 -0
  153. package/dist/ui-kit/sections/coverage.js.map +1 -0
  154. package/dist/ui-kit/sections/emergency-band.d.ts +60 -0
  155. package/dist/ui-kit/sections/emergency-band.d.ts.map +1 -0
  156. package/dist/ui-kit/sections/emergency-band.js +43 -0
  157. package/dist/ui-kit/sections/emergency-band.js.map +1 -0
  158. package/dist/ui-kit/sections/faqs.d.ts +4 -1
  159. package/dist/ui-kit/sections/faqs.d.ts.map +1 -1
  160. package/dist/ui-kit/sections/faqs.js +4 -5
  161. package/dist/ui-kit/sections/faqs.js.map +1 -1
  162. package/dist/ui-kit/sections/gallery.d.ts +60 -0
  163. package/dist/ui-kit/sections/gallery.d.ts.map +1 -0
  164. package/dist/ui-kit/sections/gallery.js +39 -0
  165. package/dist/ui-kit/sections/gallery.js.map +1 -0
  166. package/dist/ui-kit/sections/hero.d.ts +118 -0
  167. package/dist/ui-kit/sections/hero.d.ts.map +1 -0
  168. package/dist/ui-kit/sections/hero.js +58 -0
  169. package/dist/ui-kit/sections/hero.js.map +1 -0
  170. package/dist/ui-kit/sections/index.d.ts +16 -1
  171. package/dist/ui-kit/sections/index.d.ts.map +1 -1
  172. package/dist/ui-kit/sections/index.js +16 -1
  173. package/dist/ui-kit/sections/index.js.map +1 -1
  174. package/dist/ui-kit/sections/licences.d.ts +72 -0
  175. package/dist/ui-kit/sections/licences.d.ts.map +1 -0
  176. package/dist/ui-kit/sections/licences.js +46 -0
  177. package/dist/ui-kit/sections/licences.js.map +1 -0
  178. package/dist/ui-kit/sections/local-facts.d.ts +78 -0
  179. package/dist/ui-kit/sections/local-facts.d.ts.map +1 -0
  180. package/dist/ui-kit/sections/local-facts.js +32 -0
  181. package/dist/ui-kit/sections/local-facts.js.map +1 -0
  182. package/dist/ui-kit/sections/nearby-areas.d.ts +50 -0
  183. package/dist/ui-kit/sections/nearby-areas.d.ts.map +1 -0
  184. package/dist/ui-kit/sections/nearby-areas.js +49 -0
  185. package/dist/ui-kit/sections/nearby-areas.js.map +1 -0
  186. package/dist/ui-kit/sections/nearby-jobs.d.ts +66 -0
  187. package/dist/ui-kit/sections/nearby-jobs.d.ts.map +1 -0
  188. package/dist/ui-kit/sections/nearby-jobs.js +49 -0
  189. package/dist/ui-kit/sections/nearby-jobs.js.map +1 -0
  190. package/dist/ui-kit/sections/price-guide.d.ts +4 -1
  191. package/dist/ui-kit/sections/price-guide.d.ts.map +1 -1
  192. package/dist/ui-kit/sections/price-guide.js +3 -3
  193. package/dist/ui-kit/sections/price-guide.js.map +1 -1
  194. package/dist/ui-kit/sections/price-table.d.ts +7 -9
  195. package/dist/ui-kit/sections/price-table.d.ts.map +1 -1
  196. package/dist/ui-kit/sections/price-table.js +8 -8
  197. package/dist/ui-kit/sections/price-table.js.map +1 -1
  198. package/dist/ui-kit/sections/projects.d.ts +79 -0
  199. package/dist/ui-kit/sections/projects.d.ts.map +1 -0
  200. package/dist/ui-kit/sections/projects.js +69 -0
  201. package/dist/ui-kit/sections/projects.js.map +1 -0
  202. package/dist/ui-kit/sections/proof-strip.d.ts +4 -1
  203. package/dist/ui-kit/sections/proof-strip.d.ts.map +1 -1
  204. package/dist/ui-kit/sections/proof-strip.js +2 -2
  205. package/dist/ui-kit/sections/proof-strip.js.map +1 -1
  206. package/dist/ui-kit/sections/quote-checklist.d.ts +43 -0
  207. package/dist/ui-kit/sections/quote-checklist.d.ts.map +1 -0
  208. package/dist/ui-kit/sections/quote-checklist.js +19 -0
  209. package/dist/ui-kit/sections/quote-checklist.js.map +1 -0
  210. package/dist/ui-kit/sections/related-services.d.ts +41 -0
  211. package/dist/ui-kit/sections/related-services.d.ts.map +1 -0
  212. package/dist/ui-kit/sections/related-services.js +52 -0
  213. package/dist/ui-kit/sections/related-services.js.map +1 -0
  214. package/dist/ui-kit/sections/reviews-band.d.ts +16 -2
  215. package/dist/ui-kit/sections/reviews-band.d.ts.map +1 -1
  216. package/dist/ui-kit/sections/reviews-band.js +13 -2
  217. package/dist/ui-kit/sections/reviews-band.js.map +1 -1
  218. package/dist/ui-kit/sections/services-grid.d.ts +82 -0
  219. package/dist/ui-kit/sections/services-grid.d.ts.map +1 -0
  220. package/dist/ui-kit/sections/services-grid.js +90 -0
  221. package/dist/ui-kit/sections/services-grid.js.map +1 -0
  222. package/dist/ui-kit/sections/site-footer.d.ts +4 -1
  223. package/dist/ui-kit/sections/site-footer.d.ts.map +1 -1
  224. package/dist/ui-kit/sections/site-footer.js +6 -6
  225. package/dist/ui-kit/sections/site-footer.js.map +1 -1
  226. package/dist/ui-kit/sections/work-photos.d.ts +15 -0
  227. package/dist/ui-kit/sections/work-photos.d.ts.map +1 -0
  228. package/dist/ui-kit/sections/work-photos.js +24 -0
  229. package/dist/ui-kit/sections/work-photos.js.map +1 -0
  230. package/dist/ui-kit/shell/back-to-top.js +1 -1
  231. package/dist/ui-kit/shell/back-to-top.js.map +1 -1
  232. package/dist/ui-kit/shell/current-link.d.ts +21 -5
  233. package/dist/ui-kit/shell/current-link.d.ts.map +1 -1
  234. package/dist/ui-kit/shell/current-link.js +16 -5
  235. package/dist/ui-kit/shell/current-link.js.map +1 -1
  236. package/dist/ui-kit/shell/drawer-client.js +1 -1
  237. package/dist/ui-kit/shell/drawer-client.js.map +1 -1
  238. package/dist/ui-kit/shell/intent-prefetch.d.ts.map +1 -1
  239. package/dist/ui-kit/shell/intent-prefetch.js +3 -2
  240. package/dist/ui-kit/shell/intent-prefetch.js.map +1 -1
  241. package/dist/ui-kit/shell/mobile-call-bar.d.ts +6 -1
  242. package/dist/ui-kit/shell/mobile-call-bar.d.ts.map +1 -1
  243. package/dist/ui-kit/shell/mobile-call-bar.js +6 -3
  244. package/dist/ui-kit/shell/mobile-call-bar.js.map +1 -1
  245. package/dist/ui-kit/shell/mobile-drawer.d.ts +7 -3
  246. package/dist/ui-kit/shell/mobile-drawer.d.ts.map +1 -1
  247. package/dist/ui-kit/shell/mobile-drawer.js +21 -5
  248. package/dist/ui-kit/shell/mobile-drawer.js.map +1 -1
  249. package/dist/ui-kit/shell/shell-quote.d.ts +10 -0
  250. package/dist/ui-kit/shell/shell-quote.d.ts.map +1 -0
  251. package/dist/ui-kit/shell/shell-quote.js +13 -0
  252. package/dist/ui-kit/shell/shell-quote.js.map +1 -0
  253. package/dist/ui-kit/shell/site-header.d.ts +5 -2
  254. package/dist/ui-kit/shell/site-header.d.ts.map +1 -1
  255. package/dist/ui-kit/shell/site-header.js +9 -9
  256. package/dist/ui-kit/shell/site-header.js.map +1 -1
  257. package/dist/ui-kit/shell/social-links.d.ts +1 -2
  258. package/dist/ui-kit/shell/social-links.d.ts.map +1 -1
  259. package/dist/ui-kit/shell/social-links.js +6 -3
  260. package/dist/ui-kit/shell/social-links.js.map +1 -1
  261. package/dist/ui-kit/shell/utility-bar.d.ts +4 -1
  262. package/dist/ui-kit/shell/utility-bar.d.ts.map +1 -1
  263. package/dist/ui-kit/shell/utility-bar.js +2 -2
  264. package/dist/ui-kit/shell/utility-bar.js.map +1 -1
  265. package/dist/ui-kit/skin-index.d.ts +2 -1
  266. package/dist/ui-kit/skin-index.d.ts.map +1 -1
  267. package/dist/ui-kit/skin-index.js +2 -1
  268. package/dist/ui-kit/skin-index.js.map +1 -1
  269. package/dist/ui-kit/skin.d.ts +21 -4
  270. package/dist/ui-kit/skin.d.ts.map +1 -1
  271. package/dist/ui-kit/skin.js +14 -1
  272. package/dist/ui-kit/skin.js.map +1 -1
  273. package/dist/ui-kit/structure.css +793 -10
  274. package/package.json +6 -2
package/README.md CHANGED
@@ -55,6 +55,85 @@ check W3 (full tier, from 3.13.0) fails an estate family that refreshes more oft
55
55
  estate that shows reviews adopts `on-change` with the route, or records the family in `siteSystem.refreshExceptions`
56
56
  in brandlab.config.ts: `{ family, revalidateSeconds, reason, decisionId }`.
57
57
 
58
+ ## Contact delivery: accepted is not delivered (3.14.0)
59
+
60
+ When Resend accepts an enquiry it is queued, not delivered. Resend can still bounce it, drop it because the business
61
+ inbox is on the account's suppression list, or have it reported as spam, while the site has already said "sent". That
62
+ is how BL Roofing lost enquiries. `contact_form` keeps its analytics-v4 meaning (Resend accepted the enquiry) and fires
63
+ exactly as before; delivery is now tracked beside it.
64
+
65
+ **Before sending.** `createBrandlabContactRouteHandler` asks Resend's suppression list about each business inbox
66
+ (`GET /suppressions/{email}`, cached for 5 minutes per inbox, at most 2.5 seconds per lookup). The check only
67
+ records and alerts; it never blocks a send. When an inbox is suppressed the route logs and alerts
68
+ `recipient_suppressed`, and the enquiry still goes to the client's own inbox (never rerouted, never copied to Brandlab)
69
+ with the normal "sent" result and `contact_form` as usual. Keeping the inbox and its email billing in order is the
70
+ client's responsibility; the alert lets Brandlab tell them the same day. A site can opt in to refusing instead with
71
+ `refuseWhenAllSuppressed: true`: when every inbox is suppressed the visitor then gets the form's "could not send,
72
+ please call" answer and no conversion fires. A failed or refused lookup never stops an enquiry: it is
73
+ sent as before and logged once per 5 minutes as `brandlab_contact_suppression_check_unavailable`. The lookup needs a
74
+ full-access key: set `RESEND_LOOKUP_API_KEY` so `RESEND_API_KEY` can stay sending-only (without it the lookup uses
75
+ `RESEND_API_KEY`). A sending-only key is refused with `restricted_api_key`, and the warning line names the missing
76
+ permission (`"needs":"full-access key (RESEND_LOOKUP_API_KEY)"`). `suppressionCheck: false` turns the check off; a
77
+ route that replaces the provider with `createResendSend` (synthetic tests) has it off unless it passes one.
78
+
79
+ **After sending.** A signed webhook route records what happened to the business copy. Add
80
+ `src/app/api/brandlab/resend-webhook/route.ts`:
81
+
82
+ ```ts
83
+ import { createBrandlabResendWebhookRoute } from '@brandlab-digital/site-next/contact/server'
84
+ import { contactContract } from '@/config/contact'
85
+
86
+ export const { POST } = createBrandlabResendWebhookRoute({ projectId: contactContract.projectId })
87
+ ```
88
+
89
+ Then, in plain steps:
90
+
91
+ 1. Deploy the route.
92
+ 2. In Resend, open Webhooks and add an endpoint: `https://<the site's domain>/api/brandlab/resend-webhook`.
93
+ 3. Tick the email events: delivered, delivery delayed, bounced, complained, suppressed and failed.
94
+ 4. Open the new webhook and copy its signing secret (it starts with `whsec_`).
95
+ 5. In the site's Vercel project, add `RESEND_WEBHOOK_SECRET` with that value for Production (and Preview if previews
96
+ send real enquiries). Never put it in git or `.env.example` values.
97
+ 6. Redeploy, send a test enquiry (`?bl_test=1`), and check the logs show a `brandlab_contact_delivery_status` line
98
+ with `"state":"delivered"`.
99
+
100
+ Resend webhooks cover the whole account, so each site's route records only the emails its own contact route sent (the
101
+ `project_id` tag) and answers 200 to the rest. The signature is checked the Svix way Resend documents
102
+ (HMAC-SHA256 over `<svix-id>.<svix-timestamp>.<raw body>`, 5 minutes' clock tolerance); a bad signature gets 401, a
103
+ missing secret 503. Failed attempts are rate-limited per `x-real-ip` (10 in 10 minutes, then 429 with `Retry-After`;
104
+ `rateLimit` adjusts it or `false` turns it off); a correctly signed request never counts. A retry with an `svix-id` the
105
+ instance already recorded answers 200 `duplicate: true` without logging again; that list is small and per instance,
106
+ so it is best effort and the monitor still de-duplicates.
107
+
108
+ Both halves accept `onDeliveryFailure(failure)`, a same-day alert hook (email, Slack or a durable store). It gets the
109
+ code, source, enquiry reference and Resend email id, never an address or form value. If it throws on a webhook event
110
+ the route answers 500 so Resend retries the event.
111
+
112
+ | Environment variable | Where | What |
113
+ | --- | --- | --- |
114
+ | `RESEND_API_KEY` | Vercel, Production and Preview | Sends enquiries; can stay a sending-only key |
115
+ | `RESEND_LOOKUP_API_KEY` | Vercel, Production and Preview (optional) | A full-access key used only for the suppression lookup; without it the lookup uses `RESEND_API_KEY` |
116
+ | `RESEND_WEBHOOK_SECRET` | Vercel, Production | The webhook's `whsec_` signing secret |
117
+ | the contract's `delivery.recipientEnv` | Vercel, Production and Preview | The business inbox |
118
+
119
+ **For the daily delivery monitor (P26).** Read these JSON lines from the site's logs:
120
+
121
+ | Line | Meaning | Action |
122
+ | --- | --- | --- |
123
+ | `{"event":"brandlab_contact_delivery_failed",…,"code":"recipient_suppressed","source":"pre-send","recipients":"all"}` | The inbox is suppressed; the enquiry was still sent (Resend will drop it) and the visitor saw "sent" (unless the site opted in to `refuseWhenAllSuppressed: true`, when it was not sent and the visitor was asked to call) | Same day: tell the client to fix the inbox, remove it from Resend → Suppressions, tell Romie |
124
+ | same, `"recipients":"some"` | One of several inboxes is suppressed; the others got it | Same day: fix or remove that inbox |
125
+ | `…"code":"recipient_bounced"/"recipient_complained"/"recipient_suppressed"/"provider_failed","source":"webhook"` | Resend accepted it but the business copy did not arrive (`detail` gives Resend's bounce type, suppression type or failure reason) | Same day: the enquiry counted as a `contact_form` but the business never saw it; find it by `submission` (the first 16 characters of the confirmation id) or `emailId` in Resend and forward it |
126
+ | `{"event":"brandlab_contact_delivery_status","state":"delivered"}` / `"state":"delayed"` | Delivered, or Resend is retrying a soft bounce | None; a `delayed` with no later `delivered` or failure within a day needs a look |
127
+ | `brandlab_contact_suppression_check_unavailable` | The pre-send check could not ask Resend (`code` says why; `needs` names a missing permission) | Set a full-access `RESEND_LOOKUP_API_KEY`; enquiries still send |
128
+ | `brandlab_contact_delivery_alert_failed` | The alert hook failed | Check the hook |
129
+
130
+ De-duplicate webhook lines on `webhookId` (Resend retries an event with the same id). Compare each day's
131
+ `contact_form` count in GA4 with `delivered` lines: every accepted enquiry should end in exactly one `delivered` or one
132
+ failure. Ready-gate check H13 (enquiry tier, from 3.14.0) checks the same inbox before launch: a real sender, MX records,
133
+ the sender domain verified in Resend and the inbox not suppressed. Run `brandlab-ready --launch` with the inbox variable
134
+ and a full-access key (`RESEND_LOOKUP_API_KEY`) set in the shell for that run, never written to a file. Without them H13
135
+ reports "not run" and names what to set: in an ordinary run that does not block the tier, and under `--launch` it fails.
136
+
58
137
  ## Contact route: abuse limits and the test marker (3.10.0)
59
138
 
60
139
  `createBrandlabContactRouteHandler` limits distinct enquiries per client IP and per email address where enquiries are
@@ -31,7 +31,7 @@ export interface SafeContactAnalyticsParams {
31
31
  service_slug?: string;
32
32
  location_slug?: string;
33
33
  }
34
- export declare const PHONE_CALL_PLACEMENTS: readonly ["header", "footer", "hero", "cta", "fab", "contact", "location", "page"];
34
+ export declare const PHONE_CALL_PLACEMENTS: readonly ["header", "footer", "hero", "cta", "fab", "contact", "location", "page", "emergency"];
35
35
  export type PhoneCallPlacement = (typeof PHONE_CALL_PLACEMENTS)[number];
36
36
  export declare function trackPhoneLinkClick(input: {
37
37
  pagePath: string;
@@ -1 +1 @@
1
- {"version":3,"file":"analytics.d.ts","sourceRoot":"","sources":["../src/analytics.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,yBAAyB,EAC9B,KAAK,aAAa,EACnB,MAAM,uCAAuC,CAAA;AAE9C,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAA;AACnE,OAAO,EACL,cAAc,EAKf,MAAM,oBAAoB,CAAA;AAE3B,OAAO,EAAE,cAAc,EAAE,CAAA;AAEzB,MAAM,WAAW,8BAA8B;IAC7C,OAAO,EAAE,OAAO,CAAA;IAChB,sBAAsB,CAAC,EAAE,OAAO,CAAA;IAChC,0BAA0B,CAAC,EAAE,OAAO,CAAA;IACpC,0EAA0E;IAC1E,0BAA0B,CAAC,EAAE,OAAO,CAAA;IACpC,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,qBAAqB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrC,wBAAwB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxC,IAAI,CAAC,EAAE,aAAa,CAAA;CACrB;AAED;;;;GAIG;AACH,MAAM,WAAW,oCAAoC;IACnD,iFAAiF;IACjF,qBAAqB,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAA;IAChD,aAAa,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAA;IACxC,qBAAqB,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAA;IACjD,wBAAwB,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAA;IACpD,mCAAmC,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAA;CAChE;AAED,MAAM,WAAW,0BAA0B;IACzC,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,eAAO,MAAM,qBAAqB,oFASxB,CAAA;AAEV,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,qBAAqB,CAAC,CAAC,MAAM,CAAC,CAAA;AAEvE,wBAAgB,mBAAmB,CACjC,KAAK,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,kBAAkB,CAAA;CAAE,EAC1D,MAAM,EAAE,IAAI,CAAC,8BAA8B,EAAE,SAAS,GAAG,eAAe,GAAG,MAAM,CAAC,WAanF;AAED,eAAO,MAAM,sBAAsB,8DAKzB,CAAA;AACV,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,sBAAsB,CAAC,CAAC,MAAM,CAAC,CAAA;AAExE;;;;;;;;;;GAUG;AACH,eAAO,MAAM,uBAAuB,oEAM1B,CAAA;AACV,MAAM,MAAM,mBAAmB,GAAG,CAAC,OAAO,uBAAuB,CAAC,CAAC,MAAM,CAAC,CAAA;AAE1E,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,kBAAkB,CAAA;IACzB,UAAU,EAAE,MAAM,CAAA;IAClB,iEAAiE;IACjE,MAAM,CAAC,EAAE,mBAAmB,CAAA;IAC5B,sGAAsG;IACtG,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,eAAO,MAAM,mBAAmB,oFAQtB,CAAA;AACV,MAAM,MAAM,gBAAgB,GAAG,CAAC,OAAO,mBAAmB,CAAC,CAAC,MAAM,CAAC,CAAA;AAEnE,KAAK,oBAAoB,GAAG,IAAI,CAC9B,8BAA8B,EAC9B,SAAS,GAAG,wBAAwB,GAAG,eAAe,GAAG,MAAM,CAChE,CAAA;AAED,wBAAgB,kBAAkB,CAChC,KAAK,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE,EAClE,MAAM,EAAE,oBAAoB,WAS7B;AAED,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,kBAAkB,EACzB,MAAM,EAAE,oBAAoB,WAsB7B;AAED,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE;IACL,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,gBAAgB,CAAA;CAC5B,EACD,MAAM,EAAE,oBAAoB,WAS7B;AAwCD,wBAAgB,yBAAyB,CACvC,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,MAAM,EAAE,IAAI,CAAC,8BAA8B,EAAE,SAAS,GAAG,eAAe,GAAG,MAAM,CAAC,GAAG;IACnF,QAAQ,EAAE,yBAAyB,CAAA;CACpC,WAqBF;AAMD,KAAK,aAAa,GAAG,cAAc,GAAG,eAAe,CAAA;AAKrD;;;;;;;;;GASG;AACH,MAAM,WAAW,6BAA6B;IAC5C,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,MAAM,CAAA;IAChB,0DAA0D;IAC1D,MAAM,CAAC,EAAE,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,aAAa,EAAE,MAAM,CAAC,CAAC,CAAC,CAAA;IACzD;;;;OAIG;IACH,MAAM,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAAC,CAAC,CAAA;CAC9D;AA2HD,gEAAgE;AAChE,MAAM,WAAW,mBAAmB;IAClC,mEAAmE;IACnE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,uEAAuE;IACvE,QAAQ,CAAC,UAAU,EAAE,0BAA0B,CAAA;IAC/C,uEAAuE;IACvE,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAA;CAC5B;AAED,MAAM,WAAW,+BAA+B;IAC9C;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,IAAI,EAAE,mBAAmB,KAAK,MAAM,GAAG,SAAS,CAAC,CAAA;IACpE,iDAAiD;IACjD,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED;;;;;GAKG;AACH,MAAM,WAAW,qBAAqB;IACpC,mBAAmB,CAAC,EAAE,+BAA+B,CAAA;CACtD;AAmDD,MAAM,WAAW,8BAA8B;IAC7C,QAAQ,EAAE,yBAAyB,CAAA;IACnC,YAAY,EAAE,oCAAoC,CAAA;IAClD,6EAA6E;IAC7E,aAAa,EAAE,SAAS,6BAA6B,EAAE,CAAA;IACvD,iFAAiF;IACjF,KAAK,CAAC,EAAE,qBAAqB,CAAA;CAC9B;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,QAAQ,EAAE,yBAAyB,CAAA;IAC5C,qDAAqD;IACrD,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,gGAAgG;IAChG,QAAQ,CAAC,SAAS,EAAE,uBAAuB,CAAA;IAC3C,0FAA0F;IAC1F,QAAQ,CAAC,MAAM,EAAE,8BAA8B,CAAA;IAC/C,6EAA6E;IAC7E,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,0BAA0B,CAAA;IACxD,mBAAmB,CAAC,KAAK,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,kBAAkB,CAAA;KAAE,GAAG,OAAO,CAAA;IACxF,kBAAkB,CAAC,KAAK,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAA;IACxE,kBAAkB,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAA;IACtD,iBAAiB,CAAC,KAAK,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,gBAAgB,CAAA;KAAE,GAAG,OAAO,CAAA;IACnG,oGAAoG;IACpG,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAA;CACxE;AAyDD;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,8BAA8B,GAAG,iBAAiB,CAoElG"}
1
+ {"version":3,"file":"analytics.d.ts","sourceRoot":"","sources":["../src/analytics.ts"],"names":[],"mappings":"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"}
package/dist/analytics.js CHANGED
@@ -11,6 +11,8 @@ export const PHONE_CALL_PLACEMENTS = [
11
11
  'contact',
12
12
  'location',
13
13
  'page',
14
+ /** The after-hours `EmergencyBand` (3.15.0). */
15
+ 'emergency',
14
16
  ];
15
17
  export function trackPhoneLinkClick(input, config) {
16
18
  if (!PHONE_CALL_PLACEMENTS.includes(input.placement))
@@ -1 +1 @@
1
- {"version":3,"file":"analytics.js","sourceRoot":"","sources":["../src/analytics.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n defineAnalyticsContract,\n sendBrandlabAnalyticsEvent,\n type BrandlabAnalyticsContract,\n type GtagTransport,\n} from '@brandlab-digital/site-core/analytics'\nimport { resolveBrandlabDeployment } from '@brandlab-digital/site-core/site'\nimport type { BrandlabGoogleTagConfig } from './google-tag-plan.js'\nimport {\n getBrowserGtag,\n registerLeadBinding,\n safePagePath,\n safeSlug,\n type LeadAdsParams,\n} from './lead-registry.js'\n\nexport { getBrowserGtag }\n\nexport interface BrandlabBrowserAnalyticsConfig {\n enabled: boolean\n leadDiagnosticsEnabled?: boolean\n enhancedConversionsEnabled?: boolean\n /** Disclosed Enhanced Conversions: ad_user_data is granted by default. */\n adUserDataGrantedByDefault?: boolean\n measurementId?: string | null\n googleAdsConversionId?: string | null\n googleAdsConversionLabel?: string | null\n gtag?: GtagTransport\n}\n\n/**\n * Public analytics values that a client bundle must bind through direct,\n * statically analysable `process.env.NEXT_PUBLIC_*` property access. Ads keys\n * are bound only when the contract declares Google Ads (and website calls).\n */\nexport interface BrandlabBrowserAnalyticsPublicValues {\n /** `process.env.NEXT_PUBLIC_VERCEL_ENV`, inlined by defineBrandlabNextConfig. */\n deploymentEnvironment: string | null | undefined\n measurementId: string | null | undefined\n googleAdsConversionId?: string | null | undefined\n googleAdsConversionLabel?: string | null | undefined\n googleAdsWebsiteCallConversionLabel?: string | null | undefined\n}\n\nexport interface SafeContactAnalyticsParams {\n page_template?: string\n service_slug?: string\n location_slug?: string\n}\n\nexport const PHONE_CALL_PLACEMENTS = [\n 'header',\n 'footer',\n 'hero',\n 'cta',\n 'fab',\n 'contact',\n 'location',\n 'page',\n] as const\n\nexport type PhoneCallPlacement = (typeof PHONE_CALL_PLACEMENTS)[number]\n\nexport function trackPhoneLinkClick(\n input: { pagePath: string; placement: PhoneCallPlacement },\n config: Pick<BrandlabBrowserAnalyticsConfig, 'enabled' | 'measurementId' | 'gtag'>,\n) {\n if (!PHONE_CALL_PLACEMENTS.includes(input.placement)) return false\n return sendBrandlabAnalyticsEvent({\n ...config,\n gtag: config.gtag ?? getBrowserGtag(),\n eventName: 'phone_link_click',\n params: {\n event_category: 'contact_interaction',\n page_path: safePagePath(input.pagePath),\n placement: input.placement,\n },\n })\n}\n\nexport const LEAD_FORM_ERROR_STAGES = [\n 'validation',\n 'submission',\n 'delivery',\n 'unknown',\n] as const\nexport type LeadFormErrorStage = (typeof LEAD_FORM_ERROR_STAGES)[number]\n\n/**\n * Why a contact attempt failed, sent as lead_form_error's error_reason:\n * - `field`: the server rejected one or more fields (error_field names the first);\n * the visitor sees the field message and can correct it.\n * - `request`: the server refused the request itself with no field to correct\n * (origin, form shape, or a submission time outside the accepted window).\n * - `attachment`: the photos could not be attached.\n * - `delivery`: the server could not deliver the enquiry.\n * - `network`: the browser could not complete the request or read the answer\n * (offline, blocked, timed out, or a platform error page instead of JSON).\n */\nexport const LEAD_FORM_ERROR_REASONS = [\n 'field',\n 'request',\n 'attachment',\n 'delivery',\n 'network',\n] as const\nexport type LeadFormErrorReason = (typeof LEAD_FORM_ERROR_REASONS)[number]\n\nexport interface LeadFormErrorInput {\n pagePath: string\n formId: string\n stage: LeadFormErrorStage\n errorCount: number\n /** Optional failure reason; an unknown value drops the event. */\n reason?: LeadFormErrorReason\n /** The first contract field key the server rejected; sent only with reason `field`. Never a value. */\n field?: string\n}\n\nexport const LEAD_CTA_PLACEMENTS = [\n 'header',\n 'footer',\n 'hero',\n 'section',\n 'navigation',\n 'floating',\n 'page',\n] as const\nexport type LeadCtaPlacement = (typeof LEAD_CTA_PLACEMENTS)[number]\n\ntype LeadDiagnosticConfig = Pick<\n BrandlabBrowserAnalyticsConfig,\n 'enabled' | 'leadDiagnosticsEnabled' | 'measurementId' | 'gtag'\n>\n\nexport function trackLeadFormStart(\n input: { pagePath: string; formId: string; pageTemplate?: string },\n config: LeadDiagnosticConfig,\n) {\n const formId = safeSlug(input.formId)\n if (!formId) return false\n return sendLeadDiagnosticEvent('lead_form_start', {\n form_id: formId,\n page_path: safePagePath(input.pagePath),\n ...(safeSlug(input.pageTemplate) ? { page_template: safeSlug(input.pageTemplate) } : {}),\n }, config)\n}\n\nexport function trackLeadFormError(\n input: LeadFormErrorInput,\n config: LeadDiagnosticConfig,\n) {\n const formId = safeSlug(input.formId)\n if (\n !formId ||\n !LEAD_FORM_ERROR_STAGES.includes(input.stage) ||\n !Number.isInteger(input.errorCount) ||\n input.errorCount < 1 ||\n input.errorCount > 50 ||\n (input.reason !== undefined && !LEAD_FORM_ERROR_REASONS.includes(input.reason))\n ) {\n return false\n }\n const field = input.reason === 'field' ? safeSlug(input.field) : undefined\n return sendLeadDiagnosticEvent('lead_form_error', {\n form_id: formId,\n page_path: safePagePath(input.pagePath),\n error_stage: input.stage,\n error_count: input.errorCount,\n ...(input.reason ? { error_reason: input.reason } : {}),\n ...(field ? { error_field: field } : {}),\n }, config)\n}\n\nexport function trackLeadCtaClick(\n input: {\n pagePath: string\n ctaId: string\n placement: LeadCtaPlacement\n },\n config: LeadDiagnosticConfig,\n) {\n const ctaId = safeSlug(input.ctaId)\n if (!ctaId || !LEAD_CTA_PLACEMENTS.includes(input.placement)) return false\n return sendLeadDiagnosticEvent('lead_cta_click', {\n cta_id: ctaId,\n page_path: safePagePath(input.pagePath),\n placement: input.placement,\n }, config)\n}\n\nfunction sendLeadDiagnosticEvent(\n eventName: 'lead_form_start' | 'lead_form_error' | 'lead_cta_click',\n params: Record<string, unknown>,\n config: LeadDiagnosticConfig,\n) {\n return sendBrandlabAnalyticsEvent({\n enabled: config.enabled && config.leadDiagnosticsEnabled === true,\n measurementId: config.measurementId,\n gtag: config.gtag ?? getBrowserGtag(),\n eventName,\n params,\n })\n}\n\nfunction safeAdditionalEventValue(\n value: unknown,\n policy: NonNullable<BrandlabAnalyticsContract['additionalEvents']>[number]['parameters'][number],\n) {\n if (policy.type === 'boolean') return typeof value === 'boolean' ? value : undefined\n if (policy.type === 'count') {\n const minimum = policy.minimum ?? 0\n const maximum = policy.maximum ?? 100_000\n return typeof value === 'number' && Number.isInteger(value) && value >= minimum && value <= maximum\n ? value\n : undefined\n }\n if (typeof value !== 'string') return undefined\n const normalized = value.trim()\n if (policy.type === 'enum') return policy.values.includes(normalized) ? normalized : undefined\n if (!/^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$/.test(normalized) || normalized.length > 80) {\n return undefined\n }\n const digits = normalized.replace(/\\D/g, '')\n return digits.length >= 4 || /^[a-f0-9]{24,}$/.test(normalized)\n ? undefined\n : normalized\n}\n\nexport function trackBrandlabBrowserEvent(\n eventName: string,\n params: Record<string, unknown>,\n config: Pick<BrandlabBrowserAnalyticsConfig, 'enabled' | 'measurementId' | 'gtag'> & {\n contract: BrandlabAnalyticsContract\n },\n) {\n defineAnalyticsContract(config.contract)\n const event = config.contract.additionalEvents?.find(\n (candidate) => candidate.name === eventName,\n )\n if (!event) return false\n const safeParams = Object.fromEntries(\n Object.entries(params).flatMap(([key, value]) => {\n const policy = event.parameters.find((candidate) => candidate.key === key)\n if (!policy) return []\n const safeValue = safeAdditionalEventValue(value, policy)\n return safeValue === undefined ? [] : [[key, safeValue]]\n }),\n )\n return sendBrandlabAnalyticsEvent({\n ...config,\n gtag: config.gtag ?? getBrowserGtag(),\n eventName,\n params: safeParams,\n })\n}\n\n// ---------------------------------------------------------------------------\n// Project binding: src/config/analytics.ts\n// ---------------------------------------------------------------------------\n\ntype PageDimension = 'service_slug' | 'location_slug'\n\nconst PAGE_DIMENSIONS: readonly PageDimension[] = ['service_slug', 'location_slug']\nconst MAX_PAGE_TEMPLATE_ROWS = 5_000\n\n/**\n * One row of the page-template table. Rows are tried in order against the\n * page path's segments (query, hash, empty and trailing segments ignored) and\n * the first match wins; no match reports no page dimensions.\n *\n * `path` segments: a literal matches itself; `:name` matches any one segment,\n * and `:service_slug` / `:location_slug` also report it (lowercased, or\n * omitted when it is not a safe slug, without failing the row); `**` matches\n * zero or more segments and may appear once, anywhere.\n */\nexport interface BrandlabAnalyticsPageTemplate {\n path: string\n template: string\n /** Constant dimensions for every page the row matches. */\n params?: Readonly<Partial<Record<PageDimension, string>>>\n /**\n * Constraints on `:name` segments, tested against the segment exactly as it\n * appears in the path: a list of allowed values or a regular expression. A\n * segment that fails its constraint skips the row.\n */\n values?: Readonly<Record<string, readonly string[] | RegExp>>\n}\n\ntype PageTemplateSegment =\n | { kind: 'literal'; value: string }\n | {\n kind: 'parameter'\n name: string\n dimension?: PageDimension\n allowed?: ReadonlySet<string> | RegExp\n }\n\ninterface CompiledPageTemplate {\n head: readonly PageTemplateSegment[]\n /** Segments after `**`; undefined when the row has no `**`. */\n tail?: readonly PageTemplateSegment[]\n template: string\n params: SafeContactAnalyticsParams\n}\n\nfunction compilePageTemplate(row: BrandlabAnalyticsPageTemplate, index: number): CompiledPageTemplate {\n const field = `pageTemplates[${index}]`\n if (!row || typeof row !== 'object') throw new Error(`${field} must be a page-template row.`)\n const unsupported = Object.keys(row).filter((key) => !['path', 'template', 'params', 'values'].includes(key))\n if (unsupported.length > 0) {\n throw new Error(`${field} contains unsupported keys: ${unsupported.join(', ')}.`)\n }\n if (typeof row.path !== 'string' || !row.path.startsWith('/') || row.path.includes('//')) {\n throw new Error(`${field}.path must be a root-relative pattern.`)\n }\n const template = safeSlug(row.template)\n if (!template || template !== row.template) {\n throw new Error(`${field}.template must be a lowercase slug.`)\n }\n const parts = row.path === '/' ? [] : row.path.replace(/\\/$/, '').slice(1).split('/')\n if (parts.filter((part) => part === '**').length > 1) {\n throw new Error(`${field}.path may use ** only once.`)\n }\n const names = new Set<string>()\n const segments = parts.map((part): PageTemplateSegment | '**' => {\n if (part === '**') return '**'\n if (part.startsWith(':')) {\n const name = part.slice(1)\n if (!/^[a-z_][a-z0-9_]*$/.test(name)) throw new Error(`${field}.path has an invalid parameter \"${part}\".`)\n if (names.has(name)) throw new Error(`${field}.path repeats ${part}.`)\n names.add(name)\n return PAGE_DIMENSIONS.includes(name as PageDimension)\n ? { kind: 'parameter', name, dimension: name as PageDimension }\n : { kind: 'parameter', name }\n }\n if (!/^[A-Za-z0-9._~-]+$/.test(part)) throw new Error(`${field}.path has an invalid segment \"${part}\".`)\n return { kind: 'literal', value: part }\n })\n const params: SafeContactAnalyticsParams = {}\n for (const [key, value] of Object.entries(row.params ?? {})) {\n const dimension = key as PageDimension\n if (!PAGE_DIMENSIONS.includes(dimension) || names.has(dimension)) {\n throw new Error(`${field}.params.${key} is not an uncaptured page dimension.`)\n }\n if (typeof value !== 'string' || safeSlug(value) !== value) {\n throw new Error(`${field}.params.${key} must be a lowercase slug.`)\n }\n params[dimension] = value\n }\n for (const [name, allowed] of Object.entries(row.values ?? {})) {\n if (!names.has(name)) throw new Error(`${field}.values.${name} needs :${name} in the path.`)\n let constraint: ReadonlySet<string> | RegExp\n if (allowed instanceof RegExp) {\n if (allowed.global || allowed.sticky) {\n throw new Error(`${field}.values.${name} must be a regular expression without the g or y flag.`)\n }\n constraint = allowed\n } else if (Array.isArray(allowed) && allowed.every((value) => typeof value === 'string' && value.length > 0)) {\n constraint = new Set(allowed)\n } else {\n throw new Error(`${field}.values.${name} must list segment values or be a regular expression.`)\n }\n for (const segment of segments) {\n if (segment !== '**' && segment.kind === 'parameter' && segment.name === name) segment.allowed = constraint\n }\n }\n const rest = segments.indexOf('**')\n const compiled = segments.filter((segment): segment is PageTemplateSegment => segment !== '**')\n return {\n head: rest === -1 ? compiled : compiled.slice(0, rest),\n ...(rest === -1 ? {} : { tail: compiled.slice(rest) }),\n template,\n params,\n }\n}\n\nfunction pathSegments(pagePath: string) {\n return String(pagePath).split(/[?#]/, 1)[0]!.split('/').filter(Boolean)\n}\n\nfunction matchSegment(\n segment: PageTemplateSegment,\n part: string,\n params: SafeContactAnalyticsParams,\n) {\n if (segment.kind === 'literal') return segment.value === part\n const { allowed } = segment\n if (allowed && !(allowed instanceof RegExp ? allowed.test(part) : allowed.has(part))) return false\n if (segment.dimension) {\n const value = safeSlug(part)\n if (value) params[segment.dimension] = value\n }\n return true\n}\n\nfunction matchPageTemplate(row: CompiledPageTemplate, parts: readonly string[]) {\n const fixed = row.head.length + (row.tail?.length ?? 0)\n if (row.tail ? parts.length < fixed : parts.length !== fixed) return undefined\n const params: SafeContactAnalyticsParams = { page_template: row.template, ...row.params }\n for (let index = 0; index < row.head.length; index += 1) {\n if (!matchSegment(row.head[index]!, parts[index]!, params)) return undefined\n }\n const offset = parts.length - (row.tail?.length ?? 0)\n for (let index = 0; index < (row.tail?.length ?? 0); index += 1) {\n if (!matchSegment(row.tail![index]!, parts[offset + index]!, params)) return undefined\n }\n return params\n}\n\n/** What a declared lead value sees for one accepted enquiry. */\nexport interface BrandlabLeadContext {\n /** The form's page path, as useBrandlabContactForm received it. */\n readonly pagePath: string\n /** The page's contact_form dimensions from the page-template table. */\n readonly pageParams: SafeContactAnalyticsParams\n /** The accepted enquiry's fields; they are never sent to analytics. */\n readonly formData: FormData\n}\n\nexport interface BrandlabGoogleAdsLeadConversion {\n /**\n * Conversion value: a number, or a function evaluated for each accepted\n * enquiry. A function that throws or returns anything but a finite,\n * non-negative number sends the conversion without a value.\n */\n value?: number | ((lead: BrandlabLeadContext) => number | undefined)\n /** ISO 4217 currency code, for example `AUD`. */\n currency?: string\n}\n\n/**\n * How the site reports an accepted enquiry, declared once. Every accepted\n * enquiry sends one GA4 `contact_form` (page dimensions from the page-template\n * table) and, when the contract declares Google Ads, one form conversion with\n * `send_to` and `transaction_id` plus the value and currency declared here.\n */\nexport interface BrandlabLeadReporting {\n googleAdsConversion?: BrandlabGoogleAdsLeadConversion\n}\n\nfunction compileLeadReporting(\n contract: BrandlabAnalyticsContract,\n leads: BrandlabLeadReporting | undefined,\n): (lead: BrandlabLeadContext) => LeadAdsParams {\n if (leads === undefined) return () => ({})\n if (!leads || typeof leads !== 'object' || Array.isArray(leads)) {\n throw new Error('leads must be a lead-reporting declaration.')\n }\n const unsupported = Object.keys(leads).filter((key) => key !== 'googleAdsConversion')\n if (unsupported.length > 0) throw new Error(`leads contains unsupported keys: ${unsupported.join(', ')}.`)\n const conversion = leads.googleAdsConversion\n if (conversion === undefined) return () => ({})\n if (!conversion || typeof conversion !== 'object' || Array.isArray(conversion)) {\n throw new Error('leads.googleAdsConversion must declare value and/or currency.')\n }\n if (!contract.googleAds) {\n throw new Error('leads.googleAdsConversion needs googleAds in the analytics contract.')\n }\n const unsupportedConversion = Object.keys(conversion).filter((key) => key !== 'value' && key !== 'currency')\n if (unsupportedConversion.length > 0) {\n throw new Error(`leads.googleAdsConversion contains unsupported keys: ${unsupportedConversion.join(', ')}.`)\n }\n const { value, currency } = conversion\n if (currency !== undefined && (typeof currency !== 'string' || !/^[A-Z]{3}$/.test(currency))) {\n throw new Error('leads.googleAdsConversion.currency must be an ISO 4217 code such as AUD.')\n }\n if (\n value !== undefined &&\n typeof value !== 'function' &&\n (typeof value !== 'number' || !Number.isFinite(value) || value < 0)\n ) {\n throw new Error('leads.googleAdsConversion.value must be a non-negative number or a function.')\n }\n return (lead) => {\n let resolved: unknown = value\n if (typeof value === 'function') {\n try {\n resolved = value(lead)\n } catch {\n resolved = undefined\n }\n }\n return {\n ...(currency ? { currency } : {}),\n ...(typeof resolved === 'number' ? { value: resolved } : {}),\n }\n }\n}\n\nexport interface DefineBrandlabAnalyticsOptions {\n contract: BrandlabAnalyticsContract\n publicValues: BrandlabBrowserAnalyticsPublicValues\n /** Classifies page paths for contact_form and lead_form_start dimensions. */\n pageTemplates: readonly BrandlabAnalyticsPageTemplate[]\n /** The Google Ads form-conversion value and currency, if the site sends them. */\n leads?: BrandlabLeadReporting\n}\n\nexport interface BrandlabAnalytics {\n readonly contract: BrandlabAnalyticsContract\n /** True only on the Vercel production deployment. */\n readonly enabled: boolean\n /** Props for the single `<BrandlabGoogleTag {...analytics.googleTag} />` in the root layout. */\n readonly googleTag: BrandlabGoogleTagConfig\n /** Tracking configuration; the page's gtag function is resolved when an event is sent. */\n readonly config: BrandlabBrowserAnalyticsConfig\n /** Privacy-safe page_template, service_slug and location_slug for a path. */\n pageParams(pagePath: string): SafeContactAnalyticsParams\n trackPhoneLinkClick(input: { pagePath: string; placement: PhoneCallPlacement }): boolean\n trackLeadFormStart(input: { pagePath: string; formId: string }): boolean\n trackLeadFormError(input: LeadFormErrorInput): boolean\n trackLeadCtaClick(input: { pagePath: string; ctaId: string; placement: LeadCtaPlacement }): boolean\n /** Sends an event declared in the contract's additionalEvents, filtered by its parameter policy. */\n trackEvent(eventName: string, params: Record<string, unknown>): boolean\n}\n\nconst PUBLIC_VALUE_KEYS = [\n 'deploymentEnvironment',\n 'measurementId',\n 'googleAdsConversionId',\n 'googleAdsConversionLabel',\n 'googleAdsWebsiteCallConversionLabel',\n] as const\n\nfunction assertPublicValues(\n contract: BrandlabAnalyticsContract,\n publicValues: BrandlabBrowserAnalyticsPublicValues,\n) {\n if (!publicValues || typeof publicValues !== 'object' || Array.isArray(publicValues)) {\n throw new Error('publicValues must bind the NEXT_PUBLIC_* analytics values.')\n }\n const required = new Set<string>(['deploymentEnvironment', 'measurementId'])\n if (contract.googleAds) {\n required.add('googleAdsConversionId')\n required.add('googleAdsConversionLabel')\n if (contract.googleAds.websiteCalls) required.add('googleAdsWebsiteCallConversionLabel')\n }\n for (const key of Object.keys(publicValues)) {\n if (!(PUBLIC_VALUE_KEYS as readonly string[]).includes(key)) {\n throw new Error(`publicValues contains unsupported key \"${key}\".`)\n }\n if (!required.has(key)) {\n throw new Error(`publicValues.${key} is bound but the analytics contract does not declare it.`)\n }\n }\n for (const key of required) {\n if (!Object.hasOwn(publicValues, key)) {\n throw new Error(`publicValues.${key} must be bound with literal process.env property access.`)\n }\n const value = (publicValues as unknown as Record<string, unknown>)[key]\n if (value !== undefined && value !== null && typeof value !== 'string') {\n throw new Error(`publicValues.${key} must be a string or undefined.`)\n }\n }\n const browserEnvKeys = [\n contract.measurementIdEnv,\n contract.googleAds?.conversionIdEnv,\n contract.googleAds?.contactConversionLabelEnv,\n contract.googleAds?.websiteCalls?.conversionLabelEnv,\n ].filter((key): key is string => Boolean(key))\n const privateKey = browserEnvKeys.find((key) => !key.startsWith('NEXT_PUBLIC_'))\n if (privateKey) {\n throw new Error(`${privateKey} is read in the browser and must be a NEXT_PUBLIC_ variable.`)\n }\n}\n\nfunction publicValue(value: string | null | undefined) {\n const trimmed = value?.trim()\n return trimmed || undefined\n}\n\n/**\n * The project's single analytics binding (`src/config/analytics.ts`). It is\n * safe in server and browser code: the root layout renders\n * `analytics.googleTag`, `src/instrumentation-client.ts` installs the phone\n * listener, and `useBrandlabContactForm` reports each accepted enquiry exactly\n * as `pageTemplates` and `leads` declare, plus the lead diagnostics. Analytics\n * runs only on the Vercel production deployment.\n */\nexport function defineBrandlabAnalytics(options: DefineBrandlabAnalyticsOptions): BrandlabAnalytics {\n if (!options || typeof options !== 'object') {\n throw new TypeError('defineBrandlabAnalytics requires contract, publicValues and pageTemplates.')\n }\n const contract = defineAnalyticsContract(options.contract)\n assertPublicValues(contract, options.publicValues)\n if (!Array.isArray(options.pageTemplates) || options.pageTemplates.length > MAX_PAGE_TEMPLATE_ROWS) {\n throw new Error(`pageTemplates must list at most ${MAX_PAGE_TEMPLATE_ROWS} rows.`)\n }\n const pageTemplates = options.pageTemplates.map(compilePageTemplate)\n const leadAdsParams = compileLeadReporting(contract, options.leads)\n const values = options.publicValues\n const enabled =\n resolveBrandlabDeployment({ NEXT_PUBLIC_VERCEL_ENV: publicValue(values.deploymentEnvironment) })\n .environment === 'production'\n const measurementId = publicValue(values.measurementId)\n const googleAdsConversionId = contract.googleAds ? publicValue(values.googleAdsConversionId) : undefined\n const enhancedConversionsMode = contract.googleAds?.enhancedConversions?.mode\n const adUserDataGrantedByDefault = enhancedConversionsMode === 'disclosed'\n const config: BrandlabBrowserAnalyticsConfig = Object.freeze({\n enabled,\n // Privacy-safe lead_form_start / lead_form_error diagnostics are on for every site.\n leadDiagnosticsEnabled: true,\n enhancedConversionsEnabled:\n enhancedConversionsMode === 'consent-required' || enhancedConversionsMode === 'disclosed',\n adUserDataGrantedByDefault,\n measurementId,\n googleAdsConversionId,\n googleAdsConversionLabel: contract.googleAds\n ? publicValue(values.googleAdsConversionLabel)\n : undefined,\n })\n const googleTag: BrandlabGoogleTagConfig = Object.freeze({\n enabled,\n measurementId,\n googleAdsConversionId,\n googleAdsWebsiteCallConversionLabel: contract.googleAds?.websiteCalls\n ? publicValue(values.googleAdsWebsiteCallConversionLabel)\n : undefined,\n adUserData: adUserDataGrantedByDefault ? 'granted' : 'denied',\n })\n const pageParams = (pagePath: string): SafeContactAnalyticsParams => {\n const parts = pathSegments(pagePath)\n for (const row of pageTemplates) {\n const params = matchPageTemplate(row, parts)\n if (params) return params\n }\n return {}\n }\n\n const binding: BrandlabAnalytics = Object.freeze({\n contract,\n enabled,\n googleTag,\n config,\n pageParams,\n trackPhoneLinkClick: (input: { pagePath: string; placement: PhoneCallPlacement }) =>\n trackPhoneLinkClick(input, config),\n trackLeadFormStart: (input: { pagePath: string; formId: string }) =>\n trackLeadFormStart({ ...input, pageTemplate: pageParams(input.pagePath).page_template }, config),\n trackLeadFormError: (input: LeadFormErrorInput) => trackLeadFormError(input, config),\n trackLeadCtaClick: (input: { pagePath: string; ctaId: string; placement: LeadCtaPlacement }) =>\n trackLeadCtaClick(input, config),\n trackEvent: (eventName: string, params: Record<string, unknown>) =>\n trackBrandlabBrowserEvent(eventName, params, { ...config, contract }),\n })\n registerLeadBinding(binding, { config, pageParams, adsParams: leadAdsParams })\n return binding\n}\n"]}
1
+ {"version":3,"file":"analytics.js","sourceRoot":"","sources":["../src/analytics.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n defineAnalyticsContract,\n sendBrandlabAnalyticsEvent,\n type BrandlabAnalyticsContract,\n type GtagTransport,\n} from '@brandlab-digital/site-core/analytics'\nimport { resolveBrandlabDeployment } from '@brandlab-digital/site-core/site'\nimport type { BrandlabGoogleTagConfig } from './google-tag-plan.js'\nimport {\n getBrowserGtag,\n registerLeadBinding,\n safePagePath,\n safeSlug,\n type LeadAdsParams,\n} from './lead-registry.js'\n\nexport { getBrowserGtag }\n\nexport interface BrandlabBrowserAnalyticsConfig {\n enabled: boolean\n leadDiagnosticsEnabled?: boolean\n enhancedConversionsEnabled?: boolean\n /** Disclosed Enhanced Conversions: ad_user_data is granted by default. */\n adUserDataGrantedByDefault?: boolean\n measurementId?: string | null\n googleAdsConversionId?: string | null\n googleAdsConversionLabel?: string | null\n gtag?: GtagTransport\n}\n\n/**\n * Public analytics values that a client bundle must bind through direct,\n * statically analysable `process.env.NEXT_PUBLIC_*` property access. Ads keys\n * are bound only when the contract declares Google Ads (and website calls).\n */\nexport interface BrandlabBrowserAnalyticsPublicValues {\n /** `process.env.NEXT_PUBLIC_VERCEL_ENV`, inlined by defineBrandlabNextConfig. */\n deploymentEnvironment: string | null | undefined\n measurementId: string | null | undefined\n googleAdsConversionId?: string | null | undefined\n googleAdsConversionLabel?: string | null | undefined\n googleAdsWebsiteCallConversionLabel?: string | null | undefined\n}\n\nexport interface SafeContactAnalyticsParams {\n page_template?: string\n service_slug?: string\n location_slug?: string\n}\n\nexport const PHONE_CALL_PLACEMENTS = [\n 'header',\n 'footer',\n 'hero',\n 'cta',\n 'fab',\n 'contact',\n 'location',\n 'page',\n /** The after-hours `EmergencyBand` (3.15.0). */\n 'emergency',\n] as const\n\nexport type PhoneCallPlacement = (typeof PHONE_CALL_PLACEMENTS)[number]\n\nexport function trackPhoneLinkClick(\n input: { pagePath: string; placement: PhoneCallPlacement },\n config: Pick<BrandlabBrowserAnalyticsConfig, 'enabled' | 'measurementId' | 'gtag'>,\n) {\n if (!PHONE_CALL_PLACEMENTS.includes(input.placement)) return false\n return sendBrandlabAnalyticsEvent({\n ...config,\n gtag: config.gtag ?? getBrowserGtag(),\n eventName: 'phone_link_click',\n params: {\n event_category: 'contact_interaction',\n page_path: safePagePath(input.pagePath),\n placement: input.placement,\n },\n })\n}\n\nexport const LEAD_FORM_ERROR_STAGES = [\n 'validation',\n 'submission',\n 'delivery',\n 'unknown',\n] as const\nexport type LeadFormErrorStage = (typeof LEAD_FORM_ERROR_STAGES)[number]\n\n/**\n * Why a contact attempt failed, sent as lead_form_error's error_reason:\n * - `field`: the server rejected one or more fields (error_field names the first);\n * the visitor sees the field message and can correct it.\n * - `request`: the server refused the request itself with no field to correct\n * (origin, form shape, or a submission time outside the accepted window).\n * - `attachment`: the photos could not be attached.\n * - `delivery`: the server could not deliver the enquiry.\n * - `network`: the browser could not complete the request or read the answer\n * (offline, blocked, timed out, or a platform error page instead of JSON).\n */\nexport const LEAD_FORM_ERROR_REASONS = [\n 'field',\n 'request',\n 'attachment',\n 'delivery',\n 'network',\n] as const\nexport type LeadFormErrorReason = (typeof LEAD_FORM_ERROR_REASONS)[number]\n\nexport interface LeadFormErrorInput {\n pagePath: string\n formId: string\n stage: LeadFormErrorStage\n errorCount: number\n /** Optional failure reason; an unknown value drops the event. */\n reason?: LeadFormErrorReason\n /** The first contract field key the server rejected; sent only with reason `field`. Never a value. */\n field?: string\n}\n\nexport const LEAD_CTA_PLACEMENTS = [\n 'header',\n 'footer',\n 'hero',\n 'section',\n 'navigation',\n 'floating',\n 'page',\n] as const\nexport type LeadCtaPlacement = (typeof LEAD_CTA_PLACEMENTS)[number]\n\ntype LeadDiagnosticConfig = Pick<\n BrandlabBrowserAnalyticsConfig,\n 'enabled' | 'leadDiagnosticsEnabled' | 'measurementId' | 'gtag'\n>\n\nexport function trackLeadFormStart(\n input: { pagePath: string; formId: string; pageTemplate?: string },\n config: LeadDiagnosticConfig,\n) {\n const formId = safeSlug(input.formId)\n if (!formId) return false\n return sendLeadDiagnosticEvent('lead_form_start', {\n form_id: formId,\n page_path: safePagePath(input.pagePath),\n ...(safeSlug(input.pageTemplate) ? { page_template: safeSlug(input.pageTemplate) } : {}),\n }, config)\n}\n\nexport function trackLeadFormError(\n input: LeadFormErrorInput,\n config: LeadDiagnosticConfig,\n) {\n const formId = safeSlug(input.formId)\n if (\n !formId ||\n !LEAD_FORM_ERROR_STAGES.includes(input.stage) ||\n !Number.isInteger(input.errorCount) ||\n input.errorCount < 1 ||\n input.errorCount > 50 ||\n (input.reason !== undefined && !LEAD_FORM_ERROR_REASONS.includes(input.reason))\n ) {\n return false\n }\n const field = input.reason === 'field' ? safeSlug(input.field) : undefined\n return sendLeadDiagnosticEvent('lead_form_error', {\n form_id: formId,\n page_path: safePagePath(input.pagePath),\n error_stage: input.stage,\n error_count: input.errorCount,\n ...(input.reason ? { error_reason: input.reason } : {}),\n ...(field ? { error_field: field } : {}),\n }, config)\n}\n\nexport function trackLeadCtaClick(\n input: {\n pagePath: string\n ctaId: string\n placement: LeadCtaPlacement\n },\n config: LeadDiagnosticConfig,\n) {\n const ctaId = safeSlug(input.ctaId)\n if (!ctaId || !LEAD_CTA_PLACEMENTS.includes(input.placement)) return false\n return sendLeadDiagnosticEvent('lead_cta_click', {\n cta_id: ctaId,\n page_path: safePagePath(input.pagePath),\n placement: input.placement,\n }, config)\n}\n\nfunction sendLeadDiagnosticEvent(\n eventName: 'lead_form_start' | 'lead_form_error' | 'lead_cta_click',\n params: Record<string, unknown>,\n config: LeadDiagnosticConfig,\n) {\n return sendBrandlabAnalyticsEvent({\n enabled: config.enabled && config.leadDiagnosticsEnabled === true,\n measurementId: config.measurementId,\n gtag: config.gtag ?? getBrowserGtag(),\n eventName,\n params,\n })\n}\n\nfunction safeAdditionalEventValue(\n value: unknown,\n policy: NonNullable<BrandlabAnalyticsContract['additionalEvents']>[number]['parameters'][number],\n) {\n if (policy.type === 'boolean') return typeof value === 'boolean' ? value : undefined\n if (policy.type === 'count') {\n const minimum = policy.minimum ?? 0\n const maximum = policy.maximum ?? 100_000\n return typeof value === 'number' && Number.isInteger(value) && value >= minimum && value <= maximum\n ? value\n : undefined\n }\n if (typeof value !== 'string') return undefined\n const normalized = value.trim()\n if (policy.type === 'enum') return policy.values.includes(normalized) ? normalized : undefined\n if (!/^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$/.test(normalized) || normalized.length > 80) {\n return undefined\n }\n const digits = normalized.replace(/\\D/g, '')\n return digits.length >= 4 || /^[a-f0-9]{24,}$/.test(normalized)\n ? undefined\n : normalized\n}\n\nexport function trackBrandlabBrowserEvent(\n eventName: string,\n params: Record<string, unknown>,\n config: Pick<BrandlabBrowserAnalyticsConfig, 'enabled' | 'measurementId' | 'gtag'> & {\n contract: BrandlabAnalyticsContract\n },\n) {\n defineAnalyticsContract(config.contract)\n const event = config.contract.additionalEvents?.find(\n (candidate) => candidate.name === eventName,\n )\n if (!event) return false\n const safeParams = Object.fromEntries(\n Object.entries(params).flatMap(([key, value]) => {\n const policy = event.parameters.find((candidate) => candidate.key === key)\n if (!policy) return []\n const safeValue = safeAdditionalEventValue(value, policy)\n return safeValue === undefined ? [] : [[key, safeValue]]\n }),\n )\n return sendBrandlabAnalyticsEvent({\n ...config,\n gtag: config.gtag ?? getBrowserGtag(),\n eventName,\n params: safeParams,\n })\n}\n\n// ---------------------------------------------------------------------------\n// Project binding: src/config/analytics.ts\n// ---------------------------------------------------------------------------\n\ntype PageDimension = 'service_slug' | 'location_slug'\n\nconst PAGE_DIMENSIONS: readonly PageDimension[] = ['service_slug', 'location_slug']\nconst MAX_PAGE_TEMPLATE_ROWS = 5_000\n\n/**\n * One row of the page-template table. Rows are tried in order against the\n * page path's segments (query, hash, empty and trailing segments ignored) and\n * the first match wins; no match reports no page dimensions.\n *\n * `path` segments: a literal matches itself; `:name` matches any one segment,\n * and `:service_slug` / `:location_slug` also report it (lowercased, or\n * omitted when it is not a safe slug, without failing the row); `**` matches\n * zero or more segments and may appear once, anywhere.\n */\nexport interface BrandlabAnalyticsPageTemplate {\n path: string\n template: string\n /** Constant dimensions for every page the row matches. */\n params?: Readonly<Partial<Record<PageDimension, string>>>\n /**\n * Constraints on `:name` segments, tested against the segment exactly as it\n * appears in the path: a list of allowed values or a regular expression. A\n * segment that fails its constraint skips the row.\n */\n values?: Readonly<Record<string, readonly string[] | RegExp>>\n}\n\ntype PageTemplateSegment =\n | { kind: 'literal'; value: string }\n | {\n kind: 'parameter'\n name: string\n dimension?: PageDimension\n allowed?: ReadonlySet<string> | RegExp\n }\n\ninterface CompiledPageTemplate {\n head: readonly PageTemplateSegment[]\n /** Segments after `**`; undefined when the row has no `**`. */\n tail?: readonly PageTemplateSegment[]\n template: string\n params: SafeContactAnalyticsParams\n}\n\nfunction compilePageTemplate(row: BrandlabAnalyticsPageTemplate, index: number): CompiledPageTemplate {\n const field = `pageTemplates[${index}]`\n if (!row || typeof row !== 'object') throw new Error(`${field} must be a page-template row.`)\n const unsupported = Object.keys(row).filter((key) => !['path', 'template', 'params', 'values'].includes(key))\n if (unsupported.length > 0) {\n throw new Error(`${field} contains unsupported keys: ${unsupported.join(', ')}.`)\n }\n if (typeof row.path !== 'string' || !row.path.startsWith('/') || row.path.includes('//')) {\n throw new Error(`${field}.path must be a root-relative pattern.`)\n }\n const template = safeSlug(row.template)\n if (!template || template !== row.template) {\n throw new Error(`${field}.template must be a lowercase slug.`)\n }\n const parts = row.path === '/' ? [] : row.path.replace(/\\/$/, '').slice(1).split('/')\n if (parts.filter((part) => part === '**').length > 1) {\n throw new Error(`${field}.path may use ** only once.`)\n }\n const names = new Set<string>()\n const segments = parts.map((part): PageTemplateSegment | '**' => {\n if (part === '**') return '**'\n if (part.startsWith(':')) {\n const name = part.slice(1)\n if (!/^[a-z_][a-z0-9_]*$/.test(name)) throw new Error(`${field}.path has an invalid parameter \"${part}\".`)\n if (names.has(name)) throw new Error(`${field}.path repeats ${part}.`)\n names.add(name)\n return PAGE_DIMENSIONS.includes(name as PageDimension)\n ? { kind: 'parameter', name, dimension: name as PageDimension }\n : { kind: 'parameter', name }\n }\n if (!/^[A-Za-z0-9._~-]+$/.test(part)) throw new Error(`${field}.path has an invalid segment \"${part}\".`)\n return { kind: 'literal', value: part }\n })\n const params: SafeContactAnalyticsParams = {}\n for (const [key, value] of Object.entries(row.params ?? {})) {\n const dimension = key as PageDimension\n if (!PAGE_DIMENSIONS.includes(dimension) || names.has(dimension)) {\n throw new Error(`${field}.params.${key} is not an uncaptured page dimension.`)\n }\n if (typeof value !== 'string' || safeSlug(value) !== value) {\n throw new Error(`${field}.params.${key} must be a lowercase slug.`)\n }\n params[dimension] = value\n }\n for (const [name, allowed] of Object.entries(row.values ?? {})) {\n if (!names.has(name)) throw new Error(`${field}.values.${name} needs :${name} in the path.`)\n let constraint: ReadonlySet<string> | RegExp\n if (allowed instanceof RegExp) {\n if (allowed.global || allowed.sticky) {\n throw new Error(`${field}.values.${name} must be a regular expression without the g or y flag.`)\n }\n constraint = allowed\n } else if (Array.isArray(allowed) && allowed.every((value) => typeof value === 'string' && value.length > 0)) {\n constraint = new Set(allowed)\n } else {\n throw new Error(`${field}.values.${name} must list segment values or be a regular expression.`)\n }\n for (const segment of segments) {\n if (segment !== '**' && segment.kind === 'parameter' && segment.name === name) segment.allowed = constraint\n }\n }\n const rest = segments.indexOf('**')\n const compiled = segments.filter((segment): segment is PageTemplateSegment => segment !== '**')\n return {\n head: rest === -1 ? compiled : compiled.slice(0, rest),\n ...(rest === -1 ? {} : { tail: compiled.slice(rest) }),\n template,\n params,\n }\n}\n\nfunction pathSegments(pagePath: string) {\n return String(pagePath).split(/[?#]/, 1)[0]!.split('/').filter(Boolean)\n}\n\nfunction matchSegment(\n segment: PageTemplateSegment,\n part: string,\n params: SafeContactAnalyticsParams,\n) {\n if (segment.kind === 'literal') return segment.value === part\n const { allowed } = segment\n if (allowed && !(allowed instanceof RegExp ? allowed.test(part) : allowed.has(part))) return false\n if (segment.dimension) {\n const value = safeSlug(part)\n if (value) params[segment.dimension] = value\n }\n return true\n}\n\nfunction matchPageTemplate(row: CompiledPageTemplate, parts: readonly string[]) {\n const fixed = row.head.length + (row.tail?.length ?? 0)\n if (row.tail ? parts.length < fixed : parts.length !== fixed) return undefined\n const params: SafeContactAnalyticsParams = { page_template: row.template, ...row.params }\n for (let index = 0; index < row.head.length; index += 1) {\n if (!matchSegment(row.head[index]!, parts[index]!, params)) return undefined\n }\n const offset = parts.length - (row.tail?.length ?? 0)\n for (let index = 0; index < (row.tail?.length ?? 0); index += 1) {\n if (!matchSegment(row.tail![index]!, parts[offset + index]!, params)) return undefined\n }\n return params\n}\n\n/** What a declared lead value sees for one accepted enquiry. */\nexport interface BrandlabLeadContext {\n /** The form's page path, as useBrandlabContactForm received it. */\n readonly pagePath: string\n /** The page's contact_form dimensions from the page-template table. */\n readonly pageParams: SafeContactAnalyticsParams\n /** The accepted enquiry's fields; they are never sent to analytics. */\n readonly formData: FormData\n}\n\nexport interface BrandlabGoogleAdsLeadConversion {\n /**\n * Conversion value: a number, or a function evaluated for each accepted\n * enquiry. A function that throws or returns anything but a finite,\n * non-negative number sends the conversion without a value.\n */\n value?: number | ((lead: BrandlabLeadContext) => number | undefined)\n /** ISO 4217 currency code, for example `AUD`. */\n currency?: string\n}\n\n/**\n * How the site reports an accepted enquiry, declared once. Every accepted\n * enquiry sends one GA4 `contact_form` (page dimensions from the page-template\n * table) and, when the contract declares Google Ads, one form conversion with\n * `send_to` and `transaction_id` plus the value and currency declared here.\n */\nexport interface BrandlabLeadReporting {\n googleAdsConversion?: BrandlabGoogleAdsLeadConversion\n}\n\nfunction compileLeadReporting(\n contract: BrandlabAnalyticsContract,\n leads: BrandlabLeadReporting | undefined,\n): (lead: BrandlabLeadContext) => LeadAdsParams {\n if (leads === undefined) return () => ({})\n if (!leads || typeof leads !== 'object' || Array.isArray(leads)) {\n throw new Error('leads must be a lead-reporting declaration.')\n }\n const unsupported = Object.keys(leads).filter((key) => key !== 'googleAdsConversion')\n if (unsupported.length > 0) throw new Error(`leads contains unsupported keys: ${unsupported.join(', ')}.`)\n const conversion = leads.googleAdsConversion\n if (conversion === undefined) return () => ({})\n if (!conversion || typeof conversion !== 'object' || Array.isArray(conversion)) {\n throw new Error('leads.googleAdsConversion must declare value and/or currency.')\n }\n if (!contract.googleAds) {\n throw new Error('leads.googleAdsConversion needs googleAds in the analytics contract.')\n }\n const unsupportedConversion = Object.keys(conversion).filter((key) => key !== 'value' && key !== 'currency')\n if (unsupportedConversion.length > 0) {\n throw new Error(`leads.googleAdsConversion contains unsupported keys: ${unsupportedConversion.join(', ')}.`)\n }\n const { value, currency } = conversion\n if (currency !== undefined && (typeof currency !== 'string' || !/^[A-Z]{3}$/.test(currency))) {\n throw new Error('leads.googleAdsConversion.currency must be an ISO 4217 code such as AUD.')\n }\n if (\n value !== undefined &&\n typeof value !== 'function' &&\n (typeof value !== 'number' || !Number.isFinite(value) || value < 0)\n ) {\n throw new Error('leads.googleAdsConversion.value must be a non-negative number or a function.')\n }\n return (lead) => {\n let resolved: unknown = value\n if (typeof value === 'function') {\n try {\n resolved = value(lead)\n } catch {\n resolved = undefined\n }\n }\n return {\n ...(currency ? { currency } : {}),\n ...(typeof resolved === 'number' ? { value: resolved } : {}),\n }\n }\n}\n\nexport interface DefineBrandlabAnalyticsOptions {\n contract: BrandlabAnalyticsContract\n publicValues: BrandlabBrowserAnalyticsPublicValues\n /** Classifies page paths for contact_form and lead_form_start dimensions. */\n pageTemplates: readonly BrandlabAnalyticsPageTemplate[]\n /** The Google Ads form-conversion value and currency, if the site sends them. */\n leads?: BrandlabLeadReporting\n}\n\nexport interface BrandlabAnalytics {\n readonly contract: BrandlabAnalyticsContract\n /** True only on the Vercel production deployment. */\n readonly enabled: boolean\n /** Props for the single `<BrandlabGoogleTag {...analytics.googleTag} />` in the root layout. */\n readonly googleTag: BrandlabGoogleTagConfig\n /** Tracking configuration; the page's gtag function is resolved when an event is sent. */\n readonly config: BrandlabBrowserAnalyticsConfig\n /** Privacy-safe page_template, service_slug and location_slug for a path. */\n pageParams(pagePath: string): SafeContactAnalyticsParams\n trackPhoneLinkClick(input: { pagePath: string; placement: PhoneCallPlacement }): boolean\n trackLeadFormStart(input: { pagePath: string; formId: string }): boolean\n trackLeadFormError(input: LeadFormErrorInput): boolean\n trackLeadCtaClick(input: { pagePath: string; ctaId: string; placement: LeadCtaPlacement }): boolean\n /** Sends an event declared in the contract's additionalEvents, filtered by its parameter policy. */\n trackEvent(eventName: string, params: Record<string, unknown>): boolean\n}\n\nconst PUBLIC_VALUE_KEYS = [\n 'deploymentEnvironment',\n 'measurementId',\n 'googleAdsConversionId',\n 'googleAdsConversionLabel',\n 'googleAdsWebsiteCallConversionLabel',\n] as const\n\nfunction assertPublicValues(\n contract: BrandlabAnalyticsContract,\n publicValues: BrandlabBrowserAnalyticsPublicValues,\n) {\n if (!publicValues || typeof publicValues !== 'object' || Array.isArray(publicValues)) {\n throw new Error('publicValues must bind the NEXT_PUBLIC_* analytics values.')\n }\n const required = new Set<string>(['deploymentEnvironment', 'measurementId'])\n if (contract.googleAds) {\n required.add('googleAdsConversionId')\n required.add('googleAdsConversionLabel')\n if (contract.googleAds.websiteCalls) required.add('googleAdsWebsiteCallConversionLabel')\n }\n for (const key of Object.keys(publicValues)) {\n if (!(PUBLIC_VALUE_KEYS as readonly string[]).includes(key)) {\n throw new Error(`publicValues contains unsupported key \"${key}\".`)\n }\n if (!required.has(key)) {\n throw new Error(`publicValues.${key} is bound but the analytics contract does not declare it.`)\n }\n }\n for (const key of required) {\n if (!Object.hasOwn(publicValues, key)) {\n throw new Error(`publicValues.${key} must be bound with literal process.env property access.`)\n }\n const value = (publicValues as unknown as Record<string, unknown>)[key]\n if (value !== undefined && value !== null && typeof value !== 'string') {\n throw new Error(`publicValues.${key} must be a string or undefined.`)\n }\n }\n const browserEnvKeys = [\n contract.measurementIdEnv,\n contract.googleAds?.conversionIdEnv,\n contract.googleAds?.contactConversionLabelEnv,\n contract.googleAds?.websiteCalls?.conversionLabelEnv,\n ].filter((key): key is string => Boolean(key))\n const privateKey = browserEnvKeys.find((key) => !key.startsWith('NEXT_PUBLIC_'))\n if (privateKey) {\n throw new Error(`${privateKey} is read in the browser and must be a NEXT_PUBLIC_ variable.`)\n }\n}\n\nfunction publicValue(value: string | null | undefined) {\n const trimmed = value?.trim()\n return trimmed || undefined\n}\n\n/**\n * The project's single analytics binding (`src/config/analytics.ts`). It is\n * safe in server and browser code: the root layout renders\n * `analytics.googleTag`, `src/instrumentation-client.ts` installs the phone\n * listener, and `useBrandlabContactForm` reports each accepted enquiry exactly\n * as `pageTemplates` and `leads` declare, plus the lead diagnostics. Analytics\n * runs only on the Vercel production deployment.\n */\nexport function defineBrandlabAnalytics(options: DefineBrandlabAnalyticsOptions): BrandlabAnalytics {\n if (!options || typeof options !== 'object') {\n throw new TypeError('defineBrandlabAnalytics requires contract, publicValues and pageTemplates.')\n }\n const contract = defineAnalyticsContract(options.contract)\n assertPublicValues(contract, options.publicValues)\n if (!Array.isArray(options.pageTemplates) || options.pageTemplates.length > MAX_PAGE_TEMPLATE_ROWS) {\n throw new Error(`pageTemplates must list at most ${MAX_PAGE_TEMPLATE_ROWS} rows.`)\n }\n const pageTemplates = options.pageTemplates.map(compilePageTemplate)\n const leadAdsParams = compileLeadReporting(contract, options.leads)\n const values = options.publicValues\n const enabled =\n resolveBrandlabDeployment({ NEXT_PUBLIC_VERCEL_ENV: publicValue(values.deploymentEnvironment) })\n .environment === 'production'\n const measurementId = publicValue(values.measurementId)\n const googleAdsConversionId = contract.googleAds ? publicValue(values.googleAdsConversionId) : undefined\n const enhancedConversionsMode = contract.googleAds?.enhancedConversions?.mode\n const adUserDataGrantedByDefault = enhancedConversionsMode === 'disclosed'\n const config: BrandlabBrowserAnalyticsConfig = Object.freeze({\n enabled,\n // Privacy-safe lead_form_start / lead_form_error diagnostics are on for every site.\n leadDiagnosticsEnabled: true,\n enhancedConversionsEnabled:\n enhancedConversionsMode === 'consent-required' || enhancedConversionsMode === 'disclosed',\n adUserDataGrantedByDefault,\n measurementId,\n googleAdsConversionId,\n googleAdsConversionLabel: contract.googleAds\n ? publicValue(values.googleAdsConversionLabel)\n : undefined,\n })\n const googleTag: BrandlabGoogleTagConfig = Object.freeze({\n enabled,\n measurementId,\n googleAdsConversionId,\n googleAdsWebsiteCallConversionLabel: contract.googleAds?.websiteCalls\n ? publicValue(values.googleAdsWebsiteCallConversionLabel)\n : undefined,\n adUserData: adUserDataGrantedByDefault ? 'granted' : 'denied',\n })\n const pageParams = (pagePath: string): SafeContactAnalyticsParams => {\n const parts = pathSegments(pagePath)\n for (const row of pageTemplates) {\n const params = matchPageTemplate(row, parts)\n if (params) return params\n }\n return {}\n }\n\n const binding: BrandlabAnalytics = Object.freeze({\n contract,\n enabled,\n googleTag,\n config,\n pageParams,\n trackPhoneLinkClick: (input: { pagePath: string; placement: PhoneCallPlacement }) =>\n trackPhoneLinkClick(input, config),\n trackLeadFormStart: (input: { pagePath: string; formId: string }) =>\n trackLeadFormStart({ ...input, pageTemplate: pageParams(input.pagePath).page_template }, config),\n trackLeadFormError: (input: LeadFormErrorInput) => trackLeadFormError(input, config),\n trackLeadCtaClick: (input: { pagePath: string; ctaId: string; placement: LeadCtaPlacement }) =>\n trackLeadCtaClick(input, config),\n trackEvent: (eventName: string, params: Record<string, unknown>) =>\n trackBrandlabBrowserEvent(eventName, params, { ...config, contract }),\n })\n registerLeadBinding(binding, { config, pageParams, adsParams: leadAdsParams })\n return binding\n}\n"]}
@@ -0,0 +1,81 @@
1
+ import 'server-only';
2
+ /**
3
+ * Delivery truth for contact enquiries (3.14.0).
4
+ *
5
+ * An enquiry Resend accepts is queued, not delivered: Resend can still bounce it, drop it because the inbox is on the
6
+ * account's suppression list, or have it reported as spam. `contact_form` keeps its analytics-v4 meaning (the provider
7
+ * accepted the enquiry) and is unchanged. Delivery is tracked beside it:
8
+ * - before sending, the business inboxes are checked against Resend's suppression list (`createResendSuppressionCheck`);
9
+ * - after sending, Resend's signed webhook reports delivered, delayed, bounced, complained, suppressed and failed
10
+ * copies (`createBrandlabResendWebhookRoute`).
11
+ * Every failure writes one `brandlab_contact_delivery_failed` line for the daily delivery monitor and calls the
12
+ * optional alert hook the same moment. No line carries an address, a name, a form value or a key.
13
+ */
14
+ export declare const BRANDLAB_CONTACT_DELIVERY_FAILED_EVENT: "brandlab_contact_delivery_failed";
15
+ export declare const BRANDLAB_CONTACT_DELIVERY_STATUS_EVENT: "brandlab_contact_delivery_status";
16
+ export declare const BRANDLAB_CONTACT_SUPPRESSION_UNAVAILABLE_EVENT: "brandlab_contact_suppression_check_unavailable";
17
+ export declare const BRANDLAB_CONTACT_DELIVERY_ALERT_FAILED_EVENT: "brandlab_contact_delivery_alert_failed";
18
+ /** How long one suppression answer (suppressed, clear or unavailable) is reused for an inbox. */
19
+ export declare const BRANDLAB_CONTACT_SUPPRESSION_CACHE_MS: number;
20
+ /** The longest an enquiry waits for the suppression lookup before it is sent anyway. */
21
+ export declare const BRANDLAB_CONTACT_SUPPRESSION_TIMEOUT_MS = 2500;
22
+ /**
23
+ * An optional full-access Resend key used only for the suppression and domain lookups, so the sending key
24
+ * (`RESEND_API_KEY`) can stay sending-only. Without it the lookups use `RESEND_API_KEY`.
25
+ */
26
+ export declare const BRANDLAB_CONTACT_LOOKUP_API_KEY_ENV: "RESEND_LOOKUP_API_KEY";
27
+ /** `suppressed`: Resend will not deliver to the inbox. `unknown`: the lookup could not tell, so the enquiry is sent. */
28
+ export type BrandlabContactSuppressionStatus = 'suppressed' | 'clear' | 'unknown';
29
+ export interface BrandlabContactSuppressionCheck {
30
+ /** Never throws; `unknown` whenever Resend could not be asked or did not answer clearly (fail open). */
31
+ check(address: string, context: {
32
+ apiKey: string;
33
+ }): Promise<BrandlabContactSuppressionStatus>;
34
+ }
35
+ export type BrandlabContactDeliveryFailureCode = 'recipient_suppressed' | 'recipient_bounced' | 'recipient_complained' | 'provider_failed';
36
+ /** One enquiry copy that did not, or will not, reach a business inbox. Never carries an address or form value. */
37
+ export interface BrandlabContactDeliveryFailure {
38
+ event: typeof BRANDLAB_CONTACT_DELIVERY_FAILED_EVENT;
39
+ provider: 'resend';
40
+ code: BrandlabContactDeliveryFailureCode;
41
+ /** `pre-send`: the suppression check stopped it before sending. `webhook`: Resend reported it after sending. */
42
+ source: 'pre-send' | 'webhook';
43
+ /** Pre-send only: whether every business inbox was suppressed (nothing sent) or only some (sent to the rest). */
44
+ recipients?: 'all' | 'some';
45
+ /** The first 16 hex characters of the enquiry's confirmation id (the `submission_hash` tag), when known. */
46
+ submission?: string;
47
+ /** Resend's email id, from the webhook. */
48
+ emailId?: string;
49
+ /** The webhook's `svix-id`, the same on every retry of one event, for de-duplication. */
50
+ webhookId?: string;
51
+ /** Resend's bounce type and subtype, suppression type or failure reason, when it gave a plain one. */
52
+ detail?: string;
53
+ /** When the library saw it (ISO 8601). */
54
+ at: string;
55
+ }
56
+ /**
57
+ * A same-day alert hook for delivery failures (email, Slack, a durable store): called once per failure after the log
58
+ * line is written. A hook that throws is logged; the webhook route then answers 500 so Resend retries the event.
59
+ */
60
+ export type BrandlabContactDeliveryAlert = (failure: BrandlabContactDeliveryFailure) => void | Promise<void>;
61
+ /** Writes the failure line (the daily monitor's input), then calls the alert hook. False when the hook failed. */
62
+ export declare function reportContactDeliveryFailure(failure: BrandlabContactDeliveryFailure, alert?: BrandlabContactDeliveryAlert): Promise<boolean>;
63
+ export interface ResendSuppressionCheckOptions {
64
+ /** Defaults to the global fetch. */
65
+ fetch?: typeof globalThis.fetch;
66
+ /** Defaults to `Date.now`. */
67
+ now?: () => number;
68
+ /** Defaults to BRANDLAB_CONTACT_SUPPRESSION_CACHE_MS. */
69
+ cacheMs?: number;
70
+ /** Defaults to BRANDLAB_CONTACT_SUPPRESSION_TIMEOUT_MS. */
71
+ timeoutMs?: number;
72
+ }
73
+ /**
74
+ * Asks Resend's suppression list about a business inbox before an enquiry is sent (`GET /suppressions/{email}`).
75
+ * Answers are reused for the cache window per inbox, so a busy form asks at most once per inbox in that window. Any
76
+ * error, timeout or refusal answers `unknown` and the enquiry is sent: the check can stop a send it knows is lost,
77
+ * never one it is unsure about. A refusal is logged once per cache window (a sending-only API key answers 401
78
+ * `restricted_api_key` and the line names the missing permission: set a full-access key in RESEND_LOOKUP_API_KEY).
79
+ */
80
+ export declare function createResendSuppressionCheck(options?: ResendSuppressionCheckOptions): BrandlabContactSuppressionCheck;
81
+ //# sourceMappingURL=delivery.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"delivery.d.ts","sourceRoot":"","sources":["../../src/contact/delivery.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAA;AAIpB;;;;;;;;;;;GAWG;AAEH,eAAO,MAAM,sCAAsC,EAAG,kCAA2C,CAAA;AACjG,eAAO,MAAM,sCAAsC,EAAG,kCAA2C,CAAA;AACjG,eAAO,MAAM,8CAA8C,EAAG,gDAAyD,CAAA;AACvH,eAAO,MAAM,4CAA4C,EAAG,wCAAiD,CAAA;AAE7G,iGAAiG;AACjG,eAAO,MAAM,qCAAqC,QAAiB,CAAA;AACnE,wFAAwF;AACxF,eAAO,MAAM,uCAAuC,OAAQ,CAAA;AAI5D;;;GAGG;AACH,eAAO,MAAM,mCAAmC,EAAG,uBAAgC,CAAA;AAGnF,wHAAwH;AACxH,MAAM,MAAM,gCAAgC,GAAG,YAAY,GAAG,OAAO,GAAG,SAAS,CAAA;AAEjF,MAAM,WAAW,+BAA+B;IAC9C,wGAAwG;IACxG,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,gCAAgC,CAAC,CAAA;CAC/F;AAED,MAAM,MAAM,kCAAkC,GAC1C,sBAAsB,GACtB,mBAAmB,GACnB,sBAAsB,GACtB,iBAAiB,CAAA;AAErB,kHAAkH;AAClH,MAAM,WAAW,8BAA8B;IAC7C,KAAK,EAAE,OAAO,sCAAsC,CAAA;IACpD,QAAQ,EAAE,QAAQ,CAAA;IAClB,IAAI,EAAE,kCAAkC,CAAA;IACxC,gHAAgH;IAChH,MAAM,EAAE,UAAU,GAAG,SAAS,CAAA;IAC9B,iHAAiH;IACjH,UAAU,CAAC,EAAE,KAAK,GAAG,MAAM,CAAA;IAC3B,4GAA4G;IAC5G,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,2CAA2C;IAC3C,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,yFAAyF;IACzF,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,sGAAsG;IACtG,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,0CAA0C;IAC1C,EAAE,EAAE,MAAM,CAAA;CACX;AAED;;;GAGG;AACH,MAAM,MAAM,4BAA4B,GAAG,CAAC,OAAO,EAAE,8BAA8B,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;AAE5G,kHAAkH;AAClH,wBAAsB,4BAA4B,CAChD,OAAO,EAAE,8BAA8B,EACvC,KAAK,CAAC,EAAE,4BAA4B,GACnC,OAAO,CAAC,OAAO,CAAC,CA6BlB;AAED,MAAM,WAAW,6BAA6B;IAC5C,oCAAoC;IACpC,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAA;IAC/B,8BAA8B;IAC9B,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;IAClB,yDAAyD;IACzD,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,2DAA2D;IAC3D,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AA6CD;;;;;;GAMG;AACH,wBAAgB,4BAA4B,CAC1C,OAAO,GAAE,6BAAkC,GAC1C,+BAA+B,CAoCjC"}
@@ -0,0 +1,138 @@
1
+ import 'server-only';
2
+ import { RESEND_ERROR_CODES } from './resend.js';
3
+ /**
4
+ * Delivery truth for contact enquiries (3.14.0).
5
+ *
6
+ * An enquiry Resend accepts is queued, not delivered: Resend can still bounce it, drop it because the inbox is on the
7
+ * account's suppression list, or have it reported as spam. `contact_form` keeps its analytics-v4 meaning (the provider
8
+ * accepted the enquiry) and is unchanged. Delivery is tracked beside it:
9
+ * - before sending, the business inboxes are checked against Resend's suppression list (`createResendSuppressionCheck`);
10
+ * - after sending, Resend's signed webhook reports delivered, delayed, bounced, complained, suppressed and failed
11
+ * copies (`createBrandlabResendWebhookRoute`).
12
+ * Every failure writes one `brandlab_contact_delivery_failed` line for the daily delivery monitor and calls the
13
+ * optional alert hook the same moment. No line carries an address, a name, a form value or a key.
14
+ */
15
+ export const BRANDLAB_CONTACT_DELIVERY_FAILED_EVENT = 'brandlab_contact_delivery_failed';
16
+ export const BRANDLAB_CONTACT_DELIVERY_STATUS_EVENT = 'brandlab_contact_delivery_status';
17
+ export const BRANDLAB_CONTACT_SUPPRESSION_UNAVAILABLE_EVENT = 'brandlab_contact_suppression_check_unavailable';
18
+ export const BRANDLAB_CONTACT_DELIVERY_ALERT_FAILED_EVENT = 'brandlab_contact_delivery_alert_failed';
19
+ /** How long one suppression answer (suppressed, clear or unavailable) is reused for an inbox. */
20
+ export const BRANDLAB_CONTACT_SUPPRESSION_CACHE_MS = 5 * 60 * 1_000;
21
+ /** The longest an enquiry waits for the suppression lookup before it is sent anyway. */
22
+ export const BRANDLAB_CONTACT_SUPPRESSION_TIMEOUT_MS = 2_500;
23
+ const RESEND_SUPPRESSIONS_URL = 'https://api.resend.com/suppressions/';
24
+ /**
25
+ * An optional full-access Resend key used only for the suppression and domain lookups, so the sending key
26
+ * (`RESEND_API_KEY`) can stay sending-only. Without it the lookups use `RESEND_API_KEY`.
27
+ */
28
+ export const BRANDLAB_CONTACT_LOOKUP_API_KEY_ENV = 'RESEND_LOOKUP_API_KEY';
29
+ const MAILBOX_PATTERN = /^[^\s<>,@]+@[^\s<>,@]+\.[^\s<>,@]+$/;
30
+ /** Writes the failure line (the daily monitor's input), then calls the alert hook. False when the hook failed. */
31
+ export async function reportContactDeliveryFailure(failure, alert) {
32
+ console.error(JSON.stringify({
33
+ event: failure.event,
34
+ provider: failure.provider,
35
+ status: null,
36
+ code: failure.code,
37
+ source: failure.source,
38
+ ...(failure.recipients ? { recipients: failure.recipients } : {}),
39
+ ...(failure.submission ? { submission: failure.submission } : {}),
40
+ ...(failure.emailId ? { emailId: failure.emailId } : {}),
41
+ ...(failure.webhookId ? { webhookId: failure.webhookId } : {}),
42
+ ...(failure.detail ? { detail: failure.detail } : {}),
43
+ }));
44
+ if (!alert)
45
+ return true;
46
+ try {
47
+ await alert(failure);
48
+ return true;
49
+ }
50
+ catch (error) {
51
+ console.error(JSON.stringify({
52
+ event: BRANDLAB_CONTACT_DELIVERY_ALERT_FAILED_EVENT,
53
+ code: failure.code,
54
+ reason: error instanceof Error ? error.name : 'error',
55
+ }));
56
+ return false;
57
+ }
58
+ }
59
+ async function lookupSuppression(fetcher, address, apiKey, timeoutMs) {
60
+ let response;
61
+ try {
62
+ response = await fetcher(`${RESEND_SUPPRESSIONS_URL}${encodeURIComponent(address)}`, {
63
+ method: 'GET',
64
+ headers: { authorization: `Bearer ${apiKey}`, accept: 'application/json' },
65
+ signal: AbortSignal.timeout(timeoutMs),
66
+ });
67
+ }
68
+ catch {
69
+ return { status: 'unknown', code: 'lookup_failed' };
70
+ }
71
+ let body;
72
+ try {
73
+ body = await response.json();
74
+ }
75
+ catch {
76
+ body = undefined;
77
+ }
78
+ const record = body && typeof body === 'object' && !Array.isArray(body) ? body : {};
79
+ const name = typeof record.name === 'string' ? record.name.trim().toLowerCase() : '';
80
+ if (response.ok) {
81
+ return typeof record.email === 'string' && record.email.trim().toLowerCase() === address
82
+ ? { status: 'suppressed' }
83
+ : { status: 'unknown', httpStatus: response.status, code: 'unexpected_answer' };
84
+ }
85
+ if (response.status === 404)
86
+ return { status: 'clear' };
87
+ return {
88
+ status: 'unknown',
89
+ httpStatus: response.status,
90
+ code: RESEND_ERROR_CODES.has(name) ? name : 'provider_rejected',
91
+ };
92
+ }
93
+ /**
94
+ * Asks Resend's suppression list about a business inbox before an enquiry is sent (`GET /suppressions/{email}`).
95
+ * Answers are reused for the cache window per inbox, so a busy form asks at most once per inbox in that window. Any
96
+ * error, timeout or refusal answers `unknown` and the enquiry is sent: the check can stop a send it knows is lost,
97
+ * never one it is unsure about. A refusal is logged once per cache window (a sending-only API key answers 401
98
+ * `restricted_api_key` and the line names the missing permission: set a full-access key in RESEND_LOOKUP_API_KEY).
99
+ */
100
+ export function createResendSuppressionCheck(options = {}) {
101
+ const fetcher = options.fetch ?? ((...args) => globalThis.fetch(...args));
102
+ const now = options.now ?? Date.now;
103
+ const cacheMs = options.cacheMs ?? BRANDLAB_CONTACT_SUPPRESSION_CACHE_MS;
104
+ const timeoutMs = options.timeoutMs ?? BRANDLAB_CONTACT_SUPPRESSION_TIMEOUT_MS;
105
+ const cache = new Map();
106
+ return Object.freeze({
107
+ async check(address, context) {
108
+ const normalized = typeof address === 'string' ? address.trim().toLowerCase() : '';
109
+ if (!MAILBOX_PATTERN.test(normalized) || normalized.length > 320 || !context?.apiKey)
110
+ return 'unknown';
111
+ const at = now();
112
+ const cached = cache.get(normalized);
113
+ if (cached && cached.until > at)
114
+ return cached.status;
115
+ let lookup;
116
+ try {
117
+ lookup = await lookupSuppression(fetcher, normalized, context.apiKey, timeoutMs);
118
+ }
119
+ catch {
120
+ lookup = { status: 'unknown', code: 'lookup_failed' };
121
+ }
122
+ if (lookup.status === 'unknown') {
123
+ console.warn(JSON.stringify({
124
+ event: BRANDLAB_CONTACT_SUPPRESSION_UNAVAILABLE_EVENT,
125
+ provider: 'resend',
126
+ status: lookup.httpStatus ?? null,
127
+ code: lookup.code ?? 'unknown',
128
+ ...(lookup.code === 'restricted_api_key' ? { needs: `full-access key (${BRANDLAB_CONTACT_LOOKUP_API_KEY_ENV})` } : {}),
129
+ }));
130
+ }
131
+ if (cache.size > 64)
132
+ cache.clear();
133
+ cache.set(normalized, { status: lookup.status, until: at + cacheMs });
134
+ return lookup.status;
135
+ },
136
+ });
137
+ }
138
+ //# sourceMappingURL=delivery.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"delivery.js","sourceRoot":"","sources":["../../src/contact/delivery.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAA;AAEpB,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AAEhD;;;;;;;;;;;GAWG;AAEH,MAAM,CAAC,MAAM,sCAAsC,GAAG,kCAA2C,CAAA;AACjG,MAAM,CAAC,MAAM,sCAAsC,GAAG,kCAA2C,CAAA;AACjG,MAAM,CAAC,MAAM,8CAA8C,GAAG,gDAAyD,CAAA;AACvH,MAAM,CAAC,MAAM,4CAA4C,GAAG,wCAAiD,CAAA;AAE7G,iGAAiG;AACjG,MAAM,CAAC,MAAM,qCAAqC,GAAG,CAAC,GAAG,EAAE,GAAG,KAAK,CAAA;AACnE,wFAAwF;AACxF,MAAM,CAAC,MAAM,uCAAuC,GAAG,KAAK,CAAA;AAE5D,MAAM,uBAAuB,GAAG,sCAAsC,CAAA;AAEtE;;;GAGG;AACH,MAAM,CAAC,MAAM,mCAAmC,GAAG,uBAAgC,CAAA;AACnF,MAAM,eAAe,GAAG,qCAAqC,CAAA;AA2C7D,kHAAkH;AAClH,MAAM,CAAC,KAAK,UAAU,4BAA4B,CAChD,OAAuC,EACvC,KAAoC;IAEpC,OAAO,CAAC,KAAK,CACX,IAAI,CAAC,SAAS,CAAC;QACb,KAAK,EAAE,OAAO,CAAC,KAAK;QACpB,QAAQ,EAAE,OAAO,CAAC,QAAQ;QAC1B,MAAM,EAAE,IAAI;QACZ,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,GAAG,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACjE,GAAG,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACjE,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACxD,GAAG,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC9D,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACtD,CAAC,CACH,CAAA;IACD,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAA;IACvB,IAAI,CAAC;QACH,MAAM,KAAK,CAAC,OAAO,CAAC,CAAA;QACpB,OAAO,IAAI,CAAA;IACb,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,CACX,IAAI,CAAC,SAAS,CAAC;YACb,KAAK,EAAE,4CAA4C;YACnD,IAAI,EAAE,OAAO,CAAC,IAAI;YAClB,MAAM,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO;SACtD,CAAC,CACH,CAAA;QACD,OAAO,KAAK,CAAA;IACd,CAAC;AACH,CAAC;AAmBD,KAAK,UAAU,iBAAiB,CAC9B,OAAgC,EAChC,OAAe,EACf,MAAc,EACd,SAAiB;IAEjB,IAAI,QAAkB,CAAA;IACtB,IAAI,CAAC;QACH,QAAQ,GAAG,MAAM,OAAO,CAAC,GAAG,uBAAuB,GAAG,kBAAkB,CAAC,OAAO,CAAC,EAAE,EAAE;YACnF,MAAM,EAAE,KAAK;YACb,OAAO,EAAE,EAAE,aAAa,EAAE,UAAU,MAAM,EAAE,EAAE,MAAM,EAAE,kBAAkB,EAAE;YAC1E,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,SAAS,CAAC;SACvC,CAAC,CAAA;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,eAAe,EAAE,CAAA;IACrD,CAAC;IACD,IAAI,IAAa,CAAA;IACjB,IAAI,CAAC;QACH,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAA;IAC9B,CAAC;IAAC,MAAM,CAAC;QACP,IAAI,GAAG,SAAS,CAAA;IAClB,CAAC;IACD,MAAM,MAAM,GAAG,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAE,IAAgC,CAAC,CAAC,CAAC,EAAE,CAAA;IAChH,MAAM,IAAI,GAAG,OAAO,MAAM,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAA;IACpF,IAAI,QAAQ,CAAC,EAAE,EAAE,CAAC;QAChB,OAAO,OAAO,MAAM,CAAC,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,KAAK,OAAO;YACtF,CAAC,CAAC,EAAE,MAAM,EAAE,YAAY,EAAE;YAC1B,CAAC,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,UAAU,EAAE,QAAQ,CAAC,MAAM,EAAE,IAAI,EAAE,mBAAmB,EAAE,CAAA;IACnF,CAAC;IACD,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG;QAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,CAAA;IACvD,OAAO;QACL,MAAM,EAAE,SAAS;QACjB,UAAU,EAAE,QAAQ,CAAC,MAAM;QAC3B,IAAI,EAAE,kBAAkB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,mBAAmB;KAChE,CAAA;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,4BAA4B,CAC1C,UAAyC,EAAE;IAE3C,MAAM,OAAO,GAAG,OAAO,CAAC,KAAK,IAAI,CAAC,CAAC,GAAG,IAAyC,EAAE,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,CAAC,CAAA;IAC9G,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,CAAA;IACnC,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,qCAAqC,CAAA;IACxE,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,uCAAuC,CAAA;IAC9E,MAAM,KAAK,GAAG,IAAI,GAAG,EAAuE,CAAA;IAE5F,OAAO,MAAM,CAAC,MAAM,CAAC;QACnB,KAAK,CAAC,KAAK,CAAC,OAAe,EAAE,OAA2B;YACtD,MAAM,UAAU,GAAG,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAA;YAClF,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,UAAU,CAAC,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,EAAE,MAAM;gBAAE,OAAO,SAAS,CAAA;YACtG,MAAM,EAAE,GAAG,GAAG,EAAE,CAAA;YAChB,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,UAAU,CAAC,CAAA;YACpC,IAAI,MAAM,IAAI,MAAM,CAAC,KAAK,GAAG,EAAE;gBAAE,OAAO,MAAM,CAAC,MAAM,CAAA;YACrD,IAAI,MAAyB,CAAA;YAC7B,IAAI,CAAC;gBACH,MAAM,GAAG,MAAM,iBAAiB,CAAC,OAAO,EAAE,UAAU,EAAE,OAAO,CAAC,MAAM,EAAE,SAAS,CAAC,CAAA;YAClF,CAAC;YAAC,MAAM,CAAC;gBACP,MAAM,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,eAAe,EAAE,CAAA;YACvD,CAAC;YACD,IAAI,MAAM,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;gBAChC,OAAO,CAAC,IAAI,CACV,IAAI,CAAC,SAAS,CAAC;oBACb,KAAK,EAAE,8CAA8C;oBACrD,QAAQ,EAAE,QAAQ;oBAClB,MAAM,EAAE,MAAM,CAAC,UAAU,IAAI,IAAI;oBACjC,IAAI,EAAE,MAAM,CAAC,IAAI,IAAI,SAAS;oBAC9B,GAAG,CAAC,MAAM,CAAC,IAAI,KAAK,oBAAoB,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,oBAAoB,mCAAmC,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;iBACvH,CAAC,CACH,CAAA;YACH,CAAC;YACD,IAAI,KAAK,CAAC,IAAI,GAAG,EAAE;gBAAE,KAAK,CAAC,KAAK,EAAE,CAAA;YAClC,KAAK,CAAC,GAAG,CAAC,UAAU,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,GAAG,OAAO,EAAE,CAAC,CAAA;YACrE,OAAO,MAAM,CAAC,MAAM,CAAA;QACtB,CAAC;KACF,CAAC,CAAA;AACJ,CAAC","sourcesContent":["import 'server-only'\n\nimport { RESEND_ERROR_CODES } from './resend.js'\n\n/**\n * Delivery truth for contact enquiries (3.14.0).\n *\n * An enquiry Resend accepts is queued, not delivered: Resend can still bounce it, drop it because the inbox is on the\n * account's suppression list, or have it reported as spam. `contact_form` keeps its analytics-v4 meaning (the provider\n * accepted the enquiry) and is unchanged. Delivery is tracked beside it:\n * - before sending, the business inboxes are checked against Resend's suppression list (`createResendSuppressionCheck`);\n * - after sending, Resend's signed webhook reports delivered, delayed, bounced, complained, suppressed and failed\n * copies (`createBrandlabResendWebhookRoute`).\n * Every failure writes one `brandlab_contact_delivery_failed` line for the daily delivery monitor and calls the\n * optional alert hook the same moment. No line carries an address, a name, a form value or a key.\n */\n\nexport const BRANDLAB_CONTACT_DELIVERY_FAILED_EVENT = 'brandlab_contact_delivery_failed' as const\nexport const BRANDLAB_CONTACT_DELIVERY_STATUS_EVENT = 'brandlab_contact_delivery_status' as const\nexport const BRANDLAB_CONTACT_SUPPRESSION_UNAVAILABLE_EVENT = 'brandlab_contact_suppression_check_unavailable' as const\nexport const BRANDLAB_CONTACT_DELIVERY_ALERT_FAILED_EVENT = 'brandlab_contact_delivery_alert_failed' as const\n\n/** How long one suppression answer (suppressed, clear or unavailable) is reused for an inbox. */\nexport const BRANDLAB_CONTACT_SUPPRESSION_CACHE_MS = 5 * 60 * 1_000\n/** The longest an enquiry waits for the suppression lookup before it is sent anyway. */\nexport const BRANDLAB_CONTACT_SUPPRESSION_TIMEOUT_MS = 2_500\n\nconst RESEND_SUPPRESSIONS_URL = 'https://api.resend.com/suppressions/'\n\n/**\n * An optional full-access Resend key used only for the suppression and domain lookups, so the sending key\n * (`RESEND_API_KEY`) can stay sending-only. Without it the lookups use `RESEND_API_KEY`.\n */\nexport const BRANDLAB_CONTACT_LOOKUP_API_KEY_ENV = 'RESEND_LOOKUP_API_KEY' as const\nconst MAILBOX_PATTERN = /^[^\\s<>,@]+@[^\\s<>,@]+\\.[^\\s<>,@]+$/\n\n/** `suppressed`: Resend will not deliver to the inbox. `unknown`: the lookup could not tell, so the enquiry is sent. */\nexport type BrandlabContactSuppressionStatus = 'suppressed' | 'clear' | 'unknown'\n\nexport interface BrandlabContactSuppressionCheck {\n /** Never throws; `unknown` whenever Resend could not be asked or did not answer clearly (fail open). */\n check(address: string, context: { apiKey: string }): Promise<BrandlabContactSuppressionStatus>\n}\n\nexport type BrandlabContactDeliveryFailureCode =\n | 'recipient_suppressed'\n | 'recipient_bounced'\n | 'recipient_complained'\n | 'provider_failed'\n\n/** One enquiry copy that did not, or will not, reach a business inbox. Never carries an address or form value. */\nexport interface BrandlabContactDeliveryFailure {\n event: typeof BRANDLAB_CONTACT_DELIVERY_FAILED_EVENT\n provider: 'resend'\n code: BrandlabContactDeliveryFailureCode\n /** `pre-send`: the suppression check stopped it before sending. `webhook`: Resend reported it after sending. */\n source: 'pre-send' | 'webhook'\n /** Pre-send only: whether every business inbox was suppressed (nothing sent) or only some (sent to the rest). */\n recipients?: 'all' | 'some'\n /** The first 16 hex characters of the enquiry's confirmation id (the `submission_hash` tag), when known. */\n submission?: string\n /** Resend's email id, from the webhook. */\n emailId?: string\n /** The webhook's `svix-id`, the same on every retry of one event, for de-duplication. */\n webhookId?: string\n /** Resend's bounce type and subtype, suppression type or failure reason, when it gave a plain one. */\n detail?: string\n /** When the library saw it (ISO 8601). */\n at: string\n}\n\n/**\n * A same-day alert hook for delivery failures (email, Slack, a durable store): called once per failure after the log\n * line is written. A hook that throws is logged; the webhook route then answers 500 so Resend retries the event.\n */\nexport type BrandlabContactDeliveryAlert = (failure: BrandlabContactDeliveryFailure) => void | Promise<void>\n\n/** Writes the failure line (the daily monitor's input), then calls the alert hook. False when the hook failed. */\nexport async function reportContactDeliveryFailure(\n failure: BrandlabContactDeliveryFailure,\n alert?: BrandlabContactDeliveryAlert,\n): Promise<boolean> {\n console.error(\n JSON.stringify({\n event: failure.event,\n provider: failure.provider,\n status: null,\n code: failure.code,\n source: failure.source,\n ...(failure.recipients ? { recipients: failure.recipients } : {}),\n ...(failure.submission ? { submission: failure.submission } : {}),\n ...(failure.emailId ? { emailId: failure.emailId } : {}),\n ...(failure.webhookId ? { webhookId: failure.webhookId } : {}),\n ...(failure.detail ? { detail: failure.detail } : {}),\n }),\n )\n if (!alert) return true\n try {\n await alert(failure)\n return true\n } catch (error) {\n console.error(\n JSON.stringify({\n event: BRANDLAB_CONTACT_DELIVERY_ALERT_FAILED_EVENT,\n code: failure.code,\n reason: error instanceof Error ? error.name : 'error',\n }),\n )\n return false\n }\n}\n\nexport interface ResendSuppressionCheckOptions {\n /** Defaults to the global fetch. */\n fetch?: typeof globalThis.fetch\n /** Defaults to `Date.now`. */\n now?: () => number\n /** Defaults to BRANDLAB_CONTACT_SUPPRESSION_CACHE_MS. */\n cacheMs?: number\n /** Defaults to BRANDLAB_CONTACT_SUPPRESSION_TIMEOUT_MS. */\n timeoutMs?: number\n}\n\ninterface SuppressionLookup {\n status: BrandlabContactSuppressionStatus\n httpStatus?: number\n code?: string\n}\n\nasync function lookupSuppression(\n fetcher: typeof globalThis.fetch,\n address: string,\n apiKey: string,\n timeoutMs: number,\n): Promise<SuppressionLookup> {\n let response: Response\n try {\n response = await fetcher(`${RESEND_SUPPRESSIONS_URL}${encodeURIComponent(address)}`, {\n method: 'GET',\n headers: { authorization: `Bearer ${apiKey}`, accept: 'application/json' },\n signal: AbortSignal.timeout(timeoutMs),\n })\n } catch {\n return { status: 'unknown', code: 'lookup_failed' }\n }\n let body: unknown\n try {\n body = await response.json()\n } catch {\n body = undefined\n }\n const record = body && typeof body === 'object' && !Array.isArray(body) ? (body as Record<string, unknown>) : {}\n const name = typeof record.name === 'string' ? record.name.trim().toLowerCase() : ''\n if (response.ok) {\n return typeof record.email === 'string' && record.email.trim().toLowerCase() === address\n ? { status: 'suppressed' }\n : { status: 'unknown', httpStatus: response.status, code: 'unexpected_answer' }\n }\n if (response.status === 404) return { status: 'clear' }\n return {\n status: 'unknown',\n httpStatus: response.status,\n code: RESEND_ERROR_CODES.has(name) ? name : 'provider_rejected',\n }\n}\n\n/**\n * Asks Resend's suppression list about a business inbox before an enquiry is sent (`GET /suppressions/{email}`).\n * Answers are reused for the cache window per inbox, so a busy form asks at most once per inbox in that window. Any\n * error, timeout or refusal answers `unknown` and the enquiry is sent: the check can stop a send it knows is lost,\n * never one it is unsure about. A refusal is logged once per cache window (a sending-only API key answers 401\n * `restricted_api_key` and the line names the missing permission: set a full-access key in RESEND_LOOKUP_API_KEY).\n */\nexport function createResendSuppressionCheck(\n options: ResendSuppressionCheckOptions = {},\n): BrandlabContactSuppressionCheck {\n const fetcher = options.fetch ?? ((...args: Parameters<typeof globalThis.fetch>) => globalThis.fetch(...args))\n const now = options.now ?? Date.now\n const cacheMs = options.cacheMs ?? BRANDLAB_CONTACT_SUPPRESSION_CACHE_MS\n const timeoutMs = options.timeoutMs ?? BRANDLAB_CONTACT_SUPPRESSION_TIMEOUT_MS\n const cache = new Map<string, { status: BrandlabContactSuppressionStatus; until: number }>()\n\n return Object.freeze({\n async check(address: string, context: { apiKey: string }): Promise<BrandlabContactSuppressionStatus> {\n const normalized = typeof address === 'string' ? address.trim().toLowerCase() : ''\n if (!MAILBOX_PATTERN.test(normalized) || normalized.length > 320 || !context?.apiKey) return 'unknown'\n const at = now()\n const cached = cache.get(normalized)\n if (cached && cached.until > at) return cached.status\n let lookup: SuppressionLookup\n try {\n lookup = await lookupSuppression(fetcher, normalized, context.apiKey, timeoutMs)\n } catch {\n lookup = { status: 'unknown', code: 'lookup_failed' }\n }\n if (lookup.status === 'unknown') {\n console.warn(\n JSON.stringify({\n event: BRANDLAB_CONTACT_SUPPRESSION_UNAVAILABLE_EVENT,\n provider: 'resend',\n status: lookup.httpStatus ?? null,\n code: lookup.code ?? 'unknown',\n ...(lookup.code === 'restricted_api_key' ? { needs: `full-access key (${BRANDLAB_CONTACT_LOOKUP_API_KEY_ENV})` } : {}),\n }),\n )\n }\n if (cache.size > 64) cache.clear()\n cache.set(normalized, { status: lookup.status, until: at + cacheMs })\n return lookup.status\n },\n })\n}\n"]}