@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
@@ -0,0 +1,72 @@
1
+ import 'server-only';
2
+ import { type BrandlabContactDeliveryAlert } from './delivery.js';
3
+ import { type BrandlabContactRateLimitRule } from './rate-limit.js';
4
+ /**
5
+ * The signed Resend webhook route (3.14.0): it learns, the same day, when the business's copy of an enquiry bounced,
6
+ * was suppressed, was reported as spam or failed, and records each as a delivery failure. Serve it at
7
+ * `src/app/api/brandlab/resend-webhook/route.ts`:
8
+ *
9
+ * ```ts
10
+ * import { createBrandlabResendWebhookRoute } from '@brandlab-digital/site-next/contact/server'
11
+ * import { contactContract } from '@/config/contact'
12
+ *
13
+ * export const { POST } = createBrandlabResendWebhookRoute({ projectId: contactContract.projectId })
14
+ * ```
15
+ *
16
+ * Resend signs every event with Svix (`svix-id`, `svix-timestamp`, `svix-signature`): HMAC-SHA256 over
17
+ * `<svix-id>.<svix-timestamp>.<raw body>` keyed with the base64 part of the endpoint's `whsec_` signing secret, held in
18
+ * `RESEND_WEBHOOK_SECRET` and never logged. Webhooks are account-wide, so the route records only the emails this site's
19
+ * contact route sent (their `project_id` tag) and answers 200 to everything else so Resend stops retrying it.
20
+ *
21
+ * Answers: 200 `{ ok: true, recorded }` (`duplicate: true` for a retry this instance already recorded); 400 for an
22
+ * unreadable body; 401 for a missing or wrong signature or a timestamp more than 5 minutes off; 413 for a body over
23
+ * 256 KB; 429 with `Retry-After` once one client IP (`x-real-ip`) has failed those checks past the rate limit; 503 when
24
+ * the secret is not set; 500 when the alert hook failed (Resend retries).
25
+ *
26
+ * Retries are de-duplicated by `svix-id` in a small in-memory list per server instance: best effort only, so the daily
27
+ * monitor still de-duplicates on `webhookId`. Only failed attempts count towards the rate limit, so Resend's correctly
28
+ * signed requests are never refused with 429, and without `x-real-ip` nothing is rate-limited.
29
+ */
30
+ export declare const BRANDLAB_RESEND_WEBHOOK_SECRET_ENV: "RESEND_WEBHOOK_SECRET";
31
+ /** Failed attempts (bad signature, stale timestamp, oversized body) per client IP before the route answers 429. */
32
+ export declare const BRANDLAB_RESEND_WEBHOOK_RATE_LIMIT: Readonly<{
33
+ limit: 10;
34
+ windowMs: number;
35
+ }>;
36
+ export interface BrandlabResendWebhookRouteOptions {
37
+ /** The contact contract's canonical project id; only emails tagged with it are recorded. */
38
+ projectId: string;
39
+ /** Defaults to `process.env`, read on each request: `RESEND_WEBHOOK_SECRET` (the endpoint's `whsec_` secret). */
40
+ environment?: Readonly<Record<string, string | undefined>>;
41
+ /** The same-day alert for a bounced, suppressed, complained or failed business copy. */
42
+ onDeliveryFailure?: BrandlabContactDeliveryAlert;
43
+ /**
44
+ * Failed attempts per client IP (`x-real-ip`) before the route answers 429, held in memory per server instance
45
+ * (BRANDLAB_RESEND_WEBHOOK_RATE_LIMIT by default); `false` turns it off. Correctly signed requests never count.
46
+ */
47
+ rateLimit?: BrandlabContactRateLimitRule | false;
48
+ /** Defaults to `Date.now`. */
49
+ now?: () => number;
50
+ }
51
+ export interface BrandlabResendWebhookRoute {
52
+ POST(request: Request): Promise<Response>;
53
+ }
54
+ /**
55
+ * Verifies a Resend (Svix) webhook signature: one `v1,<base64>` entry in the space-separated `svix-signature` list must
56
+ * equal HMAC-SHA256 of `<id>.<timestamp>.<body>`, and the timestamp must be within 5 minutes of `now` (milliseconds).
57
+ */
58
+ export declare function verifyResendWebhookSignature(input: {
59
+ secret: string;
60
+ id: string;
61
+ timestamp: string;
62
+ signature: string;
63
+ body: string;
64
+ now: number;
65
+ }): Promise<boolean>;
66
+ /**
67
+ * Creates the signed Resend webhook route for one site. Create it once at module level. In Resend, add a webhook to
68
+ * `https://<site>/api/brandlab/resend-webhook` with the email events (at least delivered, delivery delayed, bounced,
69
+ * complained, suppressed and failed) and copy its signing secret into `RESEND_WEBHOOK_SECRET`.
70
+ */
71
+ export declare function createBrandlabResendWebhookRoute(options: BrandlabResendWebhookRouteOptions): BrandlabResendWebhookRoute;
72
+ //# sourceMappingURL=resend-webhook.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resend-webhook.d.ts","sourceRoot":"","sources":["../../src/contact/resend-webhook.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAA;AAGpB,OAAO,EAIL,KAAK,4BAA4B,EAElC,MAAM,eAAe,CAAA;AACtB,OAAO,EAAoC,KAAK,4BAA4B,EAAE,MAAM,iBAAiB,CAAA;AAGrG;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,eAAO,MAAM,kCAAkC,EAAG,uBAAgC,CAAA;AAWlF,mHAAmH;AACnH,eAAO,MAAM,kCAAkC;;;EAA0D,CAAA;AAazG,MAAM,WAAW,iCAAiC;IAChD,4FAA4F;IAC5F,SAAS,EAAE,MAAM,CAAA;IACjB,iHAAiH;IACjH,WAAW,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAA;IAC1D,wFAAwF;IACxF,iBAAiB,CAAC,EAAE,4BAA4B,CAAA;IAChD;;;OAGG;IACH,SAAS,CAAC,EAAE,4BAA4B,GAAG,KAAK,CAAA;IAChD,8BAA8B;IAC9B,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,0BAA0B;IACzC,IAAI,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;CAC1C;AAgDD;;;GAGG;AACH,wBAAsB,4BAA4B,CAAC,KAAK,EAAE;IACxD,MAAM,EAAE,MAAM,CAAA;IACd,EAAE,EAAE,MAAM,CAAA;IACV,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,MAAM,CAAA;CACZ,GAAG,OAAO,CAAC,OAAO,CAAC,CAmBnB;AA4DD;;;;GAIG;AACH,wBAAgB,gCAAgC,CAC9C,OAAO,EAAE,iCAAiC,GACzC,0BAA0B,CAqH5B"}
@@ -0,0 +1,300 @@
1
+ import 'server-only';
2
+ import { isBrandlabCanonicalEntityId } from '@brandlab-digital/site-core/entity-id';
3
+ import { BRANDLAB_CONTACT_DELIVERY_FAILED_EVENT, BRANDLAB_CONTACT_DELIVERY_STATUS_EVENT, reportContactDeliveryFailure, } from './delivery.js';
4
+ import { createBrandlabContactRateLimiter } from './rate-limit.js';
5
+ import { encodeBrandlabEntityIdForResendTag } from './resend.js';
6
+ /**
7
+ * The signed Resend webhook route (3.14.0): it learns, the same day, when the business's copy of an enquiry bounced,
8
+ * was suppressed, was reported as spam or failed, and records each as a delivery failure. Serve it at
9
+ * `src/app/api/brandlab/resend-webhook/route.ts`:
10
+ *
11
+ * ```ts
12
+ * import { createBrandlabResendWebhookRoute } from '@brandlab-digital/site-next/contact/server'
13
+ * import { contactContract } from '@/config/contact'
14
+ *
15
+ * export const { POST } = createBrandlabResendWebhookRoute({ projectId: contactContract.projectId })
16
+ * ```
17
+ *
18
+ * Resend signs every event with Svix (`svix-id`, `svix-timestamp`, `svix-signature`): HMAC-SHA256 over
19
+ * `<svix-id>.<svix-timestamp>.<raw body>` keyed with the base64 part of the endpoint's `whsec_` signing secret, held in
20
+ * `RESEND_WEBHOOK_SECRET` and never logged. Webhooks are account-wide, so the route records only the emails this site's
21
+ * contact route sent (their `project_id` tag) and answers 200 to everything else so Resend stops retrying it.
22
+ *
23
+ * Answers: 200 `{ ok: true, recorded }` (`duplicate: true` for a retry this instance already recorded); 400 for an
24
+ * unreadable body; 401 for a missing or wrong signature or a timestamp more than 5 minutes off; 413 for a body over
25
+ * 256 KB; 429 with `Retry-After` once one client IP (`x-real-ip`) has failed those checks past the rate limit; 503 when
26
+ * the secret is not set; 500 when the alert hook failed (Resend retries).
27
+ *
28
+ * Retries are de-duplicated by `svix-id` in a small in-memory list per server instance: best effort only, so the daily
29
+ * monitor still de-duplicates on `webhookId`. Only failed attempts count towards the rate limit, so Resend's correctly
30
+ * signed requests are never refused with 429, and without `x-real-ip` nothing is rate-limited.
31
+ */
32
+ export const BRANDLAB_RESEND_WEBHOOK_SECRET_ENV = 'RESEND_WEBHOOK_SECRET';
33
+ const MAX_BODY_BYTES = 256 * 1_024;
34
+ const MAX_CLOCK_SKEW_SECONDS = 300;
35
+ const LOG_EVENT = 'brandlab_resend_webhook';
36
+ const WEBHOOK_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
37
+ const EMAIL_ID_PATTERN = /^[A-Za-z0-9-]{1,128}$/;
38
+ const SUBMISSION_PATTERN = /^[0-9a-f]{64}$/;
39
+ const DETAIL_PART_PATTERN = /^[A-Za-z0-9_]{1,64}$/;
40
+ const SEEN_IDS_LIMIT = 512;
41
+ /** Failed attempts (bad signature, stale timestamp, oversized body) per client IP before the route answers 429. */
42
+ export const BRANDLAB_RESEND_WEBHOOK_RATE_LIMIT = Object.freeze({ limit: 10, windowMs: 10 * 60 * 1_000 });
43
+ const FAILURES = {
44
+ 'email.bounced': 'recipient_bounced',
45
+ 'email.complained': 'recipient_complained',
46
+ 'email.suppressed': 'recipient_suppressed',
47
+ 'email.failed': 'provider_failed',
48
+ };
49
+ const STATES = {
50
+ 'email.delivered': { state: 'delivered', level: 'info' },
51
+ 'email.delivery_delayed': { state: 'delayed', level: 'warn' },
52
+ };
53
+ function json(status, body) {
54
+ return Response.json(body, { status, headers: { 'cache-control': 'no-store' } });
55
+ }
56
+ function refusedLog(reason) {
57
+ console.error(JSON.stringify({ event: LOG_EVENT, result: 'refused', reason }));
58
+ }
59
+ /** The platform-set client IP (Vercel's `x-real-ip`); `x-forwarded-for` can be written by the client, so it is not read. */
60
+ function clientIp(request) {
61
+ const candidate = request.headers.get('x-real-ip')?.trim();
62
+ return candidate && candidate.length <= 64 && /^[0-9A-Fa-f:.]+$/.test(candidate) ? candidate : undefined;
63
+ }
64
+ function base64ToBytes(value) {
65
+ if (!/^[A-Za-z0-9+/]+={0,2}$/.test(value))
66
+ return undefined;
67
+ try {
68
+ const binary = atob(value);
69
+ return Uint8Array.from(binary, (character) => character.charCodeAt(0));
70
+ }
71
+ catch {
72
+ return undefined;
73
+ }
74
+ }
75
+ function bytesToBase64(bytes) {
76
+ let binary = '';
77
+ for (const byte of bytes)
78
+ binary += String.fromCharCode(byte);
79
+ return btoa(binary);
80
+ }
81
+ /** Equal texts compared in time that does not depend on where they differ. */
82
+ function sameText(a, b) {
83
+ let difference = a.length ^ b.length;
84
+ const reference = a.length === b.length ? b : a;
85
+ for (let index = 0; index < a.length; index += 1)
86
+ difference |= a.charCodeAt(index) ^ reference.charCodeAt(index);
87
+ return difference === 0;
88
+ }
89
+ /** The HMAC key inside a `whsec_` signing secret, or undefined when the value is not one. */
90
+ function signingKey(secret) {
91
+ const trimmed = typeof secret === 'string' ? secret.trim() : '';
92
+ if (!trimmed.startsWith('whsec_') || trimmed.length > 512)
93
+ return undefined;
94
+ const key = base64ToBytes(trimmed.slice('whsec_'.length));
95
+ return key && key.byteLength >= 16 ? key : undefined;
96
+ }
97
+ /**
98
+ * Verifies a Resend (Svix) webhook signature: one `v1,<base64>` entry in the space-separated `svix-signature` list must
99
+ * equal HMAC-SHA256 of `<id>.<timestamp>.<body>`, and the timestamp must be within 5 minutes of `now` (milliseconds).
100
+ */
101
+ export async function verifyResendWebhookSignature(input) {
102
+ const key = signingKey(input.secret);
103
+ if (!key || !WEBHOOK_ID_PATTERN.test(input.id) || !/^\d{1,12}$/.test(input.timestamp))
104
+ return false;
105
+ if (Math.abs(input.now / 1_000 - Number(input.timestamp)) > MAX_CLOCK_SKEW_SECONDS)
106
+ return false;
107
+ const candidates = input.signature
108
+ .split(' ')
109
+ .filter((entry) => entry.startsWith('v1,'))
110
+ .map((entry) => entry.slice(3));
111
+ if (candidates.length === 0)
112
+ return false;
113
+ const cryptoKey = await crypto.subtle.importKey('raw', key, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
114
+ const digest = await crypto.subtle.sign('HMAC', cryptoKey, new TextEncoder().encode(`${input.id}.${input.timestamp}.${input.body}`));
115
+ const expected = bytesToBase64(new Uint8Array(digest));
116
+ let matched = false;
117
+ for (const candidate of candidates)
118
+ if (sameText(candidate, expected))
119
+ matched = true;
120
+ return matched;
121
+ }
122
+ async function readCappedText(request, limit) {
123
+ if (!request.body)
124
+ return '';
125
+ const reader = request.body.getReader();
126
+ const chunks = [];
127
+ let size = 0;
128
+ for (;;) {
129
+ const { done, value } = await reader.read();
130
+ if (done)
131
+ break;
132
+ size += value.byteLength;
133
+ if (size > limit) {
134
+ await reader.cancel().catch(() => undefined);
135
+ return null;
136
+ }
137
+ chunks.push(value);
138
+ }
139
+ const bytes = new Uint8Array(size);
140
+ let offset = 0;
141
+ for (const chunk of chunks) {
142
+ bytes.set(chunk, offset);
143
+ offset += chunk.byteLength;
144
+ }
145
+ return new TextDecoder().decode(bytes);
146
+ }
147
+ function asRecord(value) {
148
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
149
+ }
150
+ /** Resend's tags as a name-to-value map, from either the object or the name/value list form. */
151
+ function readTags(value) {
152
+ const tags = new Map();
153
+ if (Array.isArray(value)) {
154
+ for (const entry of value) {
155
+ const tag = asRecord(entry);
156
+ if (typeof tag?.name === 'string' && typeof tag.value === 'string')
157
+ tags.set(tag.name, tag.value);
158
+ }
159
+ }
160
+ else {
161
+ for (const [name, tagValue] of Object.entries(asRecord(value) ?? {})) {
162
+ if (typeof tagValue === 'string')
163
+ tags.set(name, tagValue);
164
+ }
165
+ }
166
+ return tags;
167
+ }
168
+ /** Resend's own plain type words (bounce type/subtype, suppression type, failure reason); free text is never kept. */
169
+ function failureDetail(type, data) {
170
+ const parts = type === 'email.bounced'
171
+ ? [asRecord(data.bounce)?.type, asRecord(data.bounce)?.subType]
172
+ : type === 'email.suppressed'
173
+ ? [asRecord(data.suppressed)?.type]
174
+ : type === 'email.failed'
175
+ ? [asRecord(data.failed)?.reason]
176
+ : [];
177
+ const plain = parts.filter((part) => typeof part === 'string' && DETAIL_PART_PATTERN.test(part));
178
+ return plain.length > 0 ? plain.join('/') : undefined;
179
+ }
180
+ /**
181
+ * Creates the signed Resend webhook route for one site. Create it once at module level. In Resend, add a webhook to
182
+ * `https://<site>/api/brandlab/resend-webhook` with the email events (at least delivered, delivery delayed, bounced,
183
+ * complained, suppressed and failed) and copy its signing secret into `RESEND_WEBHOOK_SECRET`.
184
+ */
185
+ export function createBrandlabResendWebhookRoute(options) {
186
+ if (!options || typeof options !== 'object')
187
+ throw new TypeError('Resend webhook route options must be an object.');
188
+ if (!isBrandlabCanonicalEntityId(options.projectId)) {
189
+ throw new Error('The Resend webhook route needs the contact contract\'s canonical project id.');
190
+ }
191
+ if (options.onDeliveryFailure !== undefined && typeof options.onDeliveryFailure !== 'function') {
192
+ throw new TypeError('onDeliveryFailure must be a function.');
193
+ }
194
+ const projectTag = encodeBrandlabEntityIdForResendTag(options.projectId);
195
+ const now = options.now ?? Date.now;
196
+ const rule = options.rateLimit === undefined ? BRANDLAB_RESEND_WEBHOOK_RATE_LIMIT : options.rateLimit;
197
+ const limiter = rule === false ? undefined : createBrandlabContactRateLimiter({ perIp: rule, perAddress: false });
198
+ // svix-ids this instance recorded, oldest first (a Map keeps insertion order): a best-effort retry filter.
199
+ const seen = new Map();
200
+ const remember = (id) => {
201
+ seen.set(id, true);
202
+ if (seen.size > SEEN_IDS_LIMIT)
203
+ seen.delete(seen.keys().next().value);
204
+ };
205
+ return Object.freeze({
206
+ async POST(request) {
207
+ const at = now();
208
+ // Only failed attempts count towards the limit, so Resend's signed requests are never locked out.
209
+ const refuse = (status, error, reason) => {
210
+ const ip = limiter ? clientIp(request) : undefined;
211
+ if (limiter && ip) {
212
+ const decision = limiter.check({ ipKey: ip, submissionId: crypto.randomUUID(), now: at });
213
+ if (!decision.ok) {
214
+ refusedLog('rate-limited');
215
+ return Response.json({ ok: false, error: 'rate-limited' }, { status: 429, headers: { 'cache-control': 'no-store', 'retry-after': String(decision.retryAfterSeconds) } });
216
+ }
217
+ }
218
+ refusedLog(reason);
219
+ return json(status, { ok: false, error });
220
+ };
221
+ const environment = options.environment ?? process.env;
222
+ const secret = environment[BRANDLAB_RESEND_WEBHOOK_SECRET_ENV] ?? '';
223
+ if (!signingKey(secret)) {
224
+ refusedLog('not-configured');
225
+ return json(503, { ok: false, error: 'not-configured' });
226
+ }
227
+ const declaredLength = Number(request.headers.get('content-length') ?? '0');
228
+ if (Number.isFinite(declaredLength) && declaredLength > MAX_BODY_BYTES)
229
+ return refuse(413, 'too-large', 'too-large');
230
+ const body = await readCappedText(request, MAX_BODY_BYTES);
231
+ if (body === null)
232
+ return refuse(413, 'too-large', 'too-large');
233
+ const id = request.headers.get('svix-id')?.trim() ?? '';
234
+ const verified = await verifyResendWebhookSignature({
235
+ secret,
236
+ id,
237
+ timestamp: request.headers.get('svix-timestamp')?.trim() ?? '',
238
+ signature: request.headers.get('svix-signature')?.trim() ?? '',
239
+ body,
240
+ now: at,
241
+ });
242
+ if (!verified)
243
+ return refuse(401, 'unauthorised', 'signature');
244
+ if (seen.has(id))
245
+ return json(200, { ok: true, recorded: false, duplicate: true });
246
+ let parsed;
247
+ try {
248
+ parsed = JSON.parse(body);
249
+ }
250
+ catch {
251
+ refusedLog('invalid-body');
252
+ return json(400, { ok: false, error: 'invalid-body' });
253
+ }
254
+ const event = asRecord(parsed);
255
+ const data = asRecord(event?.data);
256
+ const type = typeof event?.type === 'string' ? event.type : '';
257
+ if (!event || !data) {
258
+ refusedLog('invalid-body');
259
+ return json(400, { ok: false, error: 'invalid-body' });
260
+ }
261
+ const code = FAILURES[type];
262
+ const state = STATES[type];
263
+ const tags = readTags(data.tags);
264
+ if ((!code && !state) || tags.get('project_id') !== projectTag)
265
+ return json(200, { ok: true, recorded: false });
266
+ const submissionTag = tags.get('submission_hash') ?? '';
267
+ const submission = SUBMISSION_PATTERN.test(submissionTag) ? submissionTag.slice(0, 16) : undefined;
268
+ const emailId = typeof data.email_id === 'string' && EMAIL_ID_PATTERN.test(data.email_id) ? data.email_id : undefined;
269
+ const shared = {
270
+ ...(submission ? { submission } : {}),
271
+ ...(emailId ? { emailId } : {}),
272
+ webhookId: id,
273
+ };
274
+ if (state) {
275
+ const line = JSON.stringify({ event: BRANDLAB_CONTACT_DELIVERY_STATUS_EVENT, provider: 'resend', state: state.state, ...shared });
276
+ if (state.level === 'info')
277
+ console.info(line);
278
+ else
279
+ console.warn(line);
280
+ remember(id);
281
+ return json(200, { ok: true, recorded: true });
282
+ }
283
+ const detail = failureDetail(type, data);
284
+ const alerted = await reportContactDeliveryFailure({
285
+ event: BRANDLAB_CONTACT_DELIVERY_FAILED_EVENT,
286
+ provider: 'resend',
287
+ code: code,
288
+ source: 'webhook',
289
+ ...shared,
290
+ ...(detail ? { detail } : {}),
291
+ at: new Date(at).toISOString(),
292
+ }, options.onDeliveryFailure);
293
+ if (!alerted)
294
+ return json(500, { ok: false, error: 'alert-failed' });
295
+ remember(id);
296
+ return json(200, { ok: true, recorded: true });
297
+ },
298
+ });
299
+ }
300
+ //# sourceMappingURL=resend-webhook.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resend-webhook.js","sourceRoot":"","sources":["../../src/contact/resend-webhook.ts"],"names":[],"mappings":"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","sourcesContent":["import 'server-only'\n\nimport { isBrandlabCanonicalEntityId } from '@brandlab-digital/site-core/entity-id'\nimport {\n BRANDLAB_CONTACT_DELIVERY_FAILED_EVENT,\n BRANDLAB_CONTACT_DELIVERY_STATUS_EVENT,\n reportContactDeliveryFailure,\n type BrandlabContactDeliveryAlert,\n type BrandlabContactDeliveryFailureCode,\n} from './delivery.js'\nimport { createBrandlabContactRateLimiter, type BrandlabContactRateLimitRule } from './rate-limit.js'\nimport { encodeBrandlabEntityIdForResendTag } from './resend.js'\n\n/**\n * The signed Resend webhook route (3.14.0): it learns, the same day, when the business's copy of an enquiry bounced,\n * was suppressed, was reported as spam or failed, and records each as a delivery failure. Serve it at\n * `src/app/api/brandlab/resend-webhook/route.ts`:\n *\n * ```ts\n * import { createBrandlabResendWebhookRoute } from '@brandlab-digital/site-next/contact/server'\n * import { contactContract } from '@/config/contact'\n *\n * export const { POST } = createBrandlabResendWebhookRoute({ projectId: contactContract.projectId })\n * ```\n *\n * Resend signs every event with Svix (`svix-id`, `svix-timestamp`, `svix-signature`): HMAC-SHA256 over\n * `<svix-id>.<svix-timestamp>.<raw body>` keyed with the base64 part of the endpoint's `whsec_` signing secret, held in\n * `RESEND_WEBHOOK_SECRET` and never logged. Webhooks are account-wide, so the route records only the emails this site's\n * contact route sent (their `project_id` tag) and answers 200 to everything else so Resend stops retrying it.\n *\n * Answers: 200 `{ ok: true, recorded }` (`duplicate: true` for a retry this instance already recorded); 400 for an\n * unreadable body; 401 for a missing or wrong signature or a timestamp more than 5 minutes off; 413 for a body over\n * 256 KB; 429 with `Retry-After` once one client IP (`x-real-ip`) has failed those checks past the rate limit; 503 when\n * the secret is not set; 500 when the alert hook failed (Resend retries).\n *\n * Retries are de-duplicated by `svix-id` in a small in-memory list per server instance: best effort only, so the daily\n * monitor still de-duplicates on `webhookId`. Only failed attempts count towards the rate limit, so Resend's correctly\n * signed requests are never refused with 429, and without `x-real-ip` nothing is rate-limited.\n */\n\nexport const BRANDLAB_RESEND_WEBHOOK_SECRET_ENV = 'RESEND_WEBHOOK_SECRET' as const\n\nconst MAX_BODY_BYTES = 256 * 1_024\nconst MAX_CLOCK_SKEW_SECONDS = 300\nconst LOG_EVENT = 'brandlab_resend_webhook'\nconst WEBHOOK_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/\nconst EMAIL_ID_PATTERN = /^[A-Za-z0-9-]{1,128}$/\nconst SUBMISSION_PATTERN = /^[0-9a-f]{64}$/\nconst DETAIL_PART_PATTERN = /^[A-Za-z0-9_]{1,64}$/\nconst SEEN_IDS_LIMIT = 512\n\n/** Failed attempts (bad signature, stale timestamp, oversized body) per client IP before the route answers 429. */\nexport const BRANDLAB_RESEND_WEBHOOK_RATE_LIMIT = Object.freeze({ limit: 10, windowMs: 10 * 60 * 1_000 })\n\nconst FAILURES: Readonly<Record<string, BrandlabContactDeliveryFailureCode>> = {\n 'email.bounced': 'recipient_bounced',\n 'email.complained': 'recipient_complained',\n 'email.suppressed': 'recipient_suppressed',\n 'email.failed': 'provider_failed',\n}\nconst STATES: Readonly<Record<string, { state: 'delivered' | 'delayed'; level: 'info' | 'warn' }>> = {\n 'email.delivered': { state: 'delivered', level: 'info' },\n 'email.delivery_delayed': { state: 'delayed', level: 'warn' },\n}\n\nexport interface BrandlabResendWebhookRouteOptions {\n /** The contact contract's canonical project id; only emails tagged with it are recorded. */\n projectId: string\n /** Defaults to `process.env`, read on each request: `RESEND_WEBHOOK_SECRET` (the endpoint's `whsec_` secret). */\n environment?: Readonly<Record<string, string | undefined>>\n /** The same-day alert for a bounced, suppressed, complained or failed business copy. */\n onDeliveryFailure?: BrandlabContactDeliveryAlert\n /**\n * Failed attempts per client IP (`x-real-ip`) before the route answers 429, held in memory per server instance\n * (BRANDLAB_RESEND_WEBHOOK_RATE_LIMIT by default); `false` turns it off. Correctly signed requests never count.\n */\n rateLimit?: BrandlabContactRateLimitRule | false\n /** Defaults to `Date.now`. */\n now?: () => number\n}\n\nexport interface BrandlabResendWebhookRoute {\n POST(request: Request): Promise<Response>\n}\n\nfunction json(status: number, body: Record<string, unknown>) {\n return Response.json(body, { status, headers: { 'cache-control': 'no-store' } })\n}\n\nfunction refusedLog(reason: string) {\n console.error(JSON.stringify({ event: LOG_EVENT, result: 'refused', reason }))\n}\n\n/** The platform-set client IP (Vercel's `x-real-ip`); `x-forwarded-for` can be written by the client, so it is not read. */\nfunction clientIp(request: Request) {\n const candidate = request.headers.get('x-real-ip')?.trim()\n return candidate && candidate.length <= 64 && /^[0-9A-Fa-f:.]+$/.test(candidate) ? candidate : undefined\n}\n\nfunction base64ToBytes(value: string): Uint8Array<ArrayBuffer> | undefined {\n if (!/^[A-Za-z0-9+/]+={0,2}$/.test(value)) return undefined\n try {\n const binary = atob(value)\n return Uint8Array.from(binary, (character) => character.charCodeAt(0))\n } catch {\n return undefined\n }\n}\n\nfunction bytesToBase64(bytes: Uint8Array) {\n let binary = ''\n for (const byte of bytes) binary += String.fromCharCode(byte)\n return btoa(binary)\n}\n\n/** Equal texts compared in time that does not depend on where they differ. */\nfunction sameText(a: string, b: string) {\n let difference = a.length ^ b.length\n const reference = a.length === b.length ? b : a\n for (let index = 0; index < a.length; index += 1) difference |= a.charCodeAt(index) ^ reference.charCodeAt(index)\n return difference === 0\n}\n\n/** The HMAC key inside a `whsec_` signing secret, or undefined when the value is not one. */\nfunction signingKey(secret: string): Uint8Array<ArrayBuffer> | undefined {\n const trimmed = typeof secret === 'string' ? secret.trim() : ''\n if (!trimmed.startsWith('whsec_') || trimmed.length > 512) return undefined\n const key = base64ToBytes(trimmed.slice('whsec_'.length))\n return key && key.byteLength >= 16 ? key : undefined\n}\n\n/**\n * Verifies a Resend (Svix) webhook signature: one `v1,<base64>` entry in the space-separated `svix-signature` list must\n * equal HMAC-SHA256 of `<id>.<timestamp>.<body>`, and the timestamp must be within 5 minutes of `now` (milliseconds).\n */\nexport async function verifyResendWebhookSignature(input: {\n secret: string\n id: string\n timestamp: string\n signature: string\n body: string\n now: number\n}): Promise<boolean> {\n const key = signingKey(input.secret)\n if (!key || !WEBHOOK_ID_PATTERN.test(input.id) || !/^\\d{1,12}$/.test(input.timestamp)) return false\n if (Math.abs(input.now / 1_000 - Number(input.timestamp)) > MAX_CLOCK_SKEW_SECONDS) return false\n const candidates = input.signature\n .split(' ')\n .filter((entry) => entry.startsWith('v1,'))\n .map((entry) => entry.slice(3))\n if (candidates.length === 0) return false\n const cryptoKey = await crypto.subtle.importKey('raw', key, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'])\n const digest = await crypto.subtle.sign(\n 'HMAC',\n cryptoKey,\n new TextEncoder().encode(`${input.id}.${input.timestamp}.${input.body}`),\n )\n const expected = bytesToBase64(new Uint8Array(digest))\n let matched = false\n for (const candidate of candidates) if (sameText(candidate, expected)) matched = true\n return matched\n}\n\nasync function readCappedText(request: Request, limit: number): Promise<string | null> {\n if (!request.body) return ''\n const reader = request.body.getReader()\n const chunks: Uint8Array[] = []\n let size = 0\n for (;;) {\n const { done, value } = await reader.read()\n if (done) break\n size += value.byteLength\n if (size > limit) {\n await reader.cancel().catch(() => undefined)\n return null\n }\n chunks.push(value)\n }\n const bytes = new Uint8Array(size)\n let offset = 0\n for (const chunk of chunks) {\n bytes.set(chunk, offset)\n offset += chunk.byteLength\n }\n return new TextDecoder().decode(bytes)\n}\n\nfunction asRecord(value: unknown): Record<string, unknown> | undefined {\n return value && typeof value === 'object' && !Array.isArray(value) ? (value as Record<string, unknown>) : undefined\n}\n\n/** Resend's tags as a name-to-value map, from either the object or the name/value list form. */\nfunction readTags(value: unknown): Map<string, string> {\n const tags = new Map<string, string>()\n if (Array.isArray(value)) {\n for (const entry of value) {\n const tag = asRecord(entry)\n if (typeof tag?.name === 'string' && typeof tag.value === 'string') tags.set(tag.name, tag.value)\n }\n } else {\n for (const [name, tagValue] of Object.entries(asRecord(value) ?? {})) {\n if (typeof tagValue === 'string') tags.set(name, tagValue)\n }\n }\n return tags\n}\n\n/** Resend's own plain type words (bounce type/subtype, suppression type, failure reason); free text is never kept. */\nfunction failureDetail(type: string, data: Record<string, unknown>): string | undefined {\n const parts =\n type === 'email.bounced'\n ? [asRecord(data.bounce)?.type, asRecord(data.bounce)?.subType]\n : type === 'email.suppressed'\n ? [asRecord(data.suppressed)?.type]\n : type === 'email.failed'\n ? [asRecord(data.failed)?.reason]\n : []\n const plain = parts.filter((part): part is string => typeof part === 'string' && DETAIL_PART_PATTERN.test(part))\n return plain.length > 0 ? plain.join('/') : undefined\n}\n\n/**\n * Creates the signed Resend webhook route for one site. Create it once at module level. In Resend, add a webhook to\n * `https://<site>/api/brandlab/resend-webhook` with the email events (at least delivered, delivery delayed, bounced,\n * complained, suppressed and failed) and copy its signing secret into `RESEND_WEBHOOK_SECRET`.\n */\nexport function createBrandlabResendWebhookRoute(\n options: BrandlabResendWebhookRouteOptions,\n): BrandlabResendWebhookRoute {\n if (!options || typeof options !== 'object') throw new TypeError('Resend webhook route options must be an object.')\n if (!isBrandlabCanonicalEntityId(options.projectId)) {\n throw new Error('The Resend webhook route needs the contact contract\\'s canonical project id.')\n }\n if (options.onDeliveryFailure !== undefined && typeof options.onDeliveryFailure !== 'function') {\n throw new TypeError('onDeliveryFailure must be a function.')\n }\n const projectTag = encodeBrandlabEntityIdForResendTag(options.projectId)\n const now = options.now ?? Date.now\n const rule = options.rateLimit === undefined ? BRANDLAB_RESEND_WEBHOOK_RATE_LIMIT : options.rateLimit\n const limiter = rule === false ? undefined : createBrandlabContactRateLimiter({ perIp: rule, perAddress: false })\n // svix-ids this instance recorded, oldest first (a Map keeps insertion order): a best-effort retry filter.\n const seen = new Map<string, true>()\n const remember = (id: string) => {\n seen.set(id, true)\n if (seen.size > SEEN_IDS_LIMIT) seen.delete(seen.keys().next().value!)\n }\n\n return Object.freeze({\n async POST(request: Request): Promise<Response> {\n const at = now()\n // Only failed attempts count towards the limit, so Resend's signed requests are never locked out.\n const refuse = (status: number, error: string, reason: string) => {\n const ip = limiter ? clientIp(request) : undefined\n if (limiter && ip) {\n const decision = limiter.check({ ipKey: ip, submissionId: crypto.randomUUID(), now: at })\n if (!decision.ok) {\n refusedLog('rate-limited')\n return Response.json(\n { ok: false, error: 'rate-limited' },\n { status: 429, headers: { 'cache-control': 'no-store', 'retry-after': String(decision.retryAfterSeconds) } },\n )\n }\n }\n refusedLog(reason)\n return json(status, { ok: false, error })\n }\n const environment = options.environment ?? process.env\n const secret = environment[BRANDLAB_RESEND_WEBHOOK_SECRET_ENV] ?? ''\n if (!signingKey(secret)) {\n refusedLog('not-configured')\n return json(503, { ok: false, error: 'not-configured' })\n }\n\n const declaredLength = Number(request.headers.get('content-length') ?? '0')\n if (Number.isFinite(declaredLength) && declaredLength > MAX_BODY_BYTES) return refuse(413, 'too-large', 'too-large')\n const body = await readCappedText(request, MAX_BODY_BYTES)\n if (body === null) return refuse(413, 'too-large', 'too-large')\n\n const id = request.headers.get('svix-id')?.trim() ?? ''\n const verified = await verifyResendWebhookSignature({\n secret,\n id,\n timestamp: request.headers.get('svix-timestamp')?.trim() ?? '',\n signature: request.headers.get('svix-signature')?.trim() ?? '',\n body,\n now: at,\n })\n if (!verified) return refuse(401, 'unauthorised', 'signature')\n if (seen.has(id)) return json(200, { ok: true, recorded: false, duplicate: true })\n\n let parsed: unknown\n try {\n parsed = JSON.parse(body)\n } catch {\n refusedLog('invalid-body')\n return json(400, { ok: false, error: 'invalid-body' })\n }\n const event = asRecord(parsed)\n const data = asRecord(event?.data)\n const type = typeof event?.type === 'string' ? event.type : ''\n if (!event || !data) {\n refusedLog('invalid-body')\n return json(400, { ok: false, error: 'invalid-body' })\n }\n\n const code = FAILURES[type]\n const state = STATES[type]\n const tags = readTags(data.tags)\n if ((!code && !state) || tags.get('project_id') !== projectTag) return json(200, { ok: true, recorded: false })\n\n const submissionTag = tags.get('submission_hash') ?? ''\n const submission = SUBMISSION_PATTERN.test(submissionTag) ? submissionTag.slice(0, 16) : undefined\n const emailId = typeof data.email_id === 'string' && EMAIL_ID_PATTERN.test(data.email_id) ? data.email_id : undefined\n const shared = {\n ...(submission ? { submission } : {}),\n ...(emailId ? { emailId } : {}),\n webhookId: id,\n }\n\n if (state) {\n const line = JSON.stringify({ event: BRANDLAB_CONTACT_DELIVERY_STATUS_EVENT, provider: 'resend', state: state.state, ...shared })\n if (state.level === 'info') console.info(line)\n else console.warn(line)\n remember(id)\n return json(200, { ok: true, recorded: true })\n }\n\n const detail = failureDetail(type, data)\n const alerted = await reportContactDeliveryFailure(\n {\n event: BRANDLAB_CONTACT_DELIVERY_FAILED_EVENT,\n provider: 'resend',\n code: code!,\n source: 'webhook',\n ...shared,\n ...(detail ? { detail } : {}),\n at: new Date(at).toISOString(),\n },\n options.onDeliveryFailure,\n )\n if (!alerted) return json(500, { ok: false, error: 'alert-failed' })\n remember(id)\n return json(200, { ok: true, recorded: true })\n },\n })\n}\n"]}
@@ -2,6 +2,7 @@ import 'server-only';
2
2
  import type { BrandlabContactContract } from '@brandlab-digital/site-core/contact';
3
3
  import type { BrandlabEnvironment } from '@brandlab-digital/site-core/site';
4
4
  import { type BrandlabContactRateLimitOptions } from './rate-limit.js';
5
+ import { type BrandlabContactDeliveryAlert, type BrandlabContactSuppressionCheck } from './delivery.js';
5
6
  import { type ResendContactSend } from './resend.js';
6
7
  import { type BrandlabContactRuntimeDefinitionOptions } from './server.js';
7
8
  import type { ContactBotGuard } from './shared.js';
@@ -20,6 +21,21 @@ export interface BrandlabContactRouteHandlerOptions {
20
21
  * by default; `false` turns them off). They apply only where enquiries are delivered (production and Preview).
21
22
  */
22
23
  rateLimit?: BrandlabContactRateLimitOptions | false;
24
+ /**
25
+ * The pre-send check of each business inbox against Resend's suppression list (3.14.0), held for the life of the
26
+ * route module so its answers are cached (5 minutes per inbox). On by default with Resend's suppressions API and a
27
+ * full-access key: `RESEND_LOOKUP_API_KEY` when set, otherwise `RESEND_API_KEY`. A sending-only key is refused, logged
28
+ * once per 5 minutes naming the missing permission, and the enquiry is sent as before. Off by default when
29
+ * `createResendSend` replaces the provider, and `false` turns it off.
30
+ */
31
+ suppressionCheck?: BrandlabContactSuppressionCheck | false;
32
+ /**
33
+ * Opt-in only (default `false`): when every business inbox is suppressed, refuse and ask the visitor to call. By
34
+ * default the suppression check only records and alerts, and the enquiry is sent to the client's own inbox.
35
+ */
36
+ refuseWhenAllSuppressed?: boolean;
37
+ /** The same-day alert for a delivery failure the route finds before sending (3.14.0). */
38
+ onDeliveryFailure?: BrandlabContactDeliveryAlert;
23
39
  }
24
40
  /**
25
41
  * A Resend send function that loads the optional `resend` peer only when an
@@ -1 +1 @@
1
- {"version":3,"file":"route-handler.d.ts","sourceRoot":"","sources":["../../src/contact/route-handler.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAA;AAEpB,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,qCAAqC,CAAA;AAClF,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAA;AAC3E,OAAO,EAEL,KAAK,+BAA+B,EACrC,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAA8B,KAAK,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAChF,OAAO,EAGL,KAAK,uCAAuC,EAC7C,MAAM,aAAa,CAAA;AACpB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAElD,MAAM,WAAW,kCAAkC;IACjD,QAAQ,EAAE,uBAAuB,CAAA;IACjC,mFAAmF;IACnF,OAAO,EAAE,uCAAuC,CAAA;IAChD,QAAQ,CAAC,EAAE,eAAe,CAAA;IAC1B,sDAAsD;IACtD,WAAW,CAAC,EAAE,mBAAmB,CAAA;IACjC,GAAG,CAAC,EAAE,MAAM,IAAI,CAAA;IAChB,4EAA4E;IAC5E,gBAAgB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,iBAAiB,CAAA;IACxD;;;OAGG;IACH,SAAS,CAAC,EAAE,+BAA+B,GAAG,KAAK,CAAA;CACpD;AAED;;;;GAIG;AACH,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,MAAM,GAAG,iBAAiB,CAQ7E;AAED;;;;;;GAMG;AACH,wBAAgB,iCAAiC,CAC/C,OAAO,EAAE,kCAAkC,GAC1C,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CA2BzC"}
1
+ {"version":3,"file":"route-handler.d.ts","sourceRoot":"","sources":["../../src/contact/route-handler.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAA;AAEpB,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,qCAAqC,CAAA;AAClF,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAA;AAC3E,OAAO,EAEL,KAAK,+BAA+B,EACrC,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAEL,KAAK,4BAA4B,EACjC,KAAK,+BAA+B,EACrC,MAAM,eAAe,CAAA;AACtB,OAAO,EAA8B,KAAK,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAChF,OAAO,EAGL,KAAK,uCAAuC,EAC7C,MAAM,aAAa,CAAA;AACpB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAElD,MAAM,WAAW,kCAAkC;IACjD,QAAQ,EAAE,uBAAuB,CAAA;IACjC,mFAAmF;IACnF,OAAO,EAAE,uCAAuC,CAAA;IAChD,QAAQ,CAAC,EAAE,eAAe,CAAA;IAC1B,sDAAsD;IACtD,WAAW,CAAC,EAAE,mBAAmB,CAAA;IACjC,GAAG,CAAC,EAAE,MAAM,IAAI,CAAA;IAChB,4EAA4E;IAC5E,gBAAgB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,iBAAiB,CAAA;IACxD;;;OAGG;IACH,SAAS,CAAC,EAAE,+BAA+B,GAAG,KAAK,CAAA;IACnD;;;;;;OAMG;IACH,gBAAgB,CAAC,EAAE,+BAA+B,GAAG,KAAK,CAAA;IAC1D;;;OAGG;IACH,uBAAuB,CAAC,EAAE,OAAO,CAAA;IACjC,yFAAyF;IACzF,iBAAiB,CAAC,EAAE,4BAA4B,CAAA;CACjD;AAED;;;;GAIG;AACH,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,MAAM,GAAG,iBAAiB,CAQ7E;AAED;;;;;;GAMG;AACH,wBAAgB,iCAAiC,CAC/C,OAAO,EAAE,kCAAkC,GAC1C,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAmCzC"}
@@ -1,5 +1,6 @@
1
1
  import 'server-only';
2
2
  import { createBrandlabContactRateLimiter, } from './rate-limit.js';
3
+ import { createResendSuppressionCheck, } from './delivery.js';
3
4
  import { createResendSdkContactSend } from './resend.js';
4
5
  import { createBrandlabContactRuntime, detectBrandlabContactDeploymentMode, } from './server.js';
5
6
  /**
@@ -29,6 +30,10 @@ export function createBrandlabContactRouteHandler(options) {
29
30
  throw new TypeError('createResendSend must be a function.');
30
31
  }
31
32
  const createResendSend = options.createResendSend ?? createLazyResendContactSend;
33
+ // One suppression cache for the module. A synthetic test that replaces the provider never asks the real one.
34
+ const suppressionCheck = options.suppressionCheck === false
35
+ ? undefined
36
+ : options.suppressionCheck ?? (options.createResendSend ? undefined : createResendSuppressionCheck());
32
37
  // One limiter for the module, shared by every per-request runtime, so its windows outlive each request.
33
38
  const rateLimit = options.rateLimit === false ? false : createBrandlabContactRateLimiter(options.rateLimit);
34
39
  const runtimeFor = (environment) => createBrandlabContactRuntime({
@@ -40,6 +45,9 @@ export function createBrandlabContactRouteHandler(options) {
40
45
  ...(options.botGuard ? { botGuard: options.botGuard } : {}),
41
46
  ...(options.now ? { now: options.now } : {}),
42
47
  rateLimit,
48
+ ...(suppressionCheck ? { suppressionCheck } : {}),
49
+ ...(options.onDeliveryFailure ? { onDeliveryFailure: options.onDeliveryFailure } : {}),
50
+ ...(options.refuseWhenAllSuppressed === true ? { refuseWhenAllSuppressed: true } : {}),
43
51
  });
44
52
  // Fail at module load, not on a customer's first enquiry.
45
53
  runtimeFor({});
@@ -1 +1 @@
1
- {"version":3,"file":"route-handler.js","sourceRoot":"","sources":["../../src/contact/route-handler.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAA;AAIpB,OAAO,EACL,gCAAgC,GAEjC,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAE,0BAA0B,EAA0B,MAAM,aAAa,CAAA;AAChF,OAAO,EACL,4BAA4B,EAC5B,mCAAmC,GAEpC,MAAM,aAAa,CAAA;AAoBpB;;;;GAIG;AACH,MAAM,UAAU,2BAA2B,CAAC,MAAc;IACxD,IAAI,IAA4C,CAAA;IAChD,OAAO,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE;QAChC,IAAI,KAAK,MAAM,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,CAC5C,0BAA0B,CAAC,IAAI,MAAM,CAAC,MAAM,CAAC,CAAC,CAC/C,CAAA;QACD,OAAO,CAAC,MAAM,IAAI,CAAC,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;IACvC,CAAC,CAAA;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,iCAAiC,CAC/C,OAA2C;IAE3C,IAAI,CAAC,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE,CAAC;QAC5C,MAAM,IAAI,SAAS,CAAC,sEAAsE,CAAC,CAAA;IAC7F,CAAC;IACD,IAAI,OAAO,CAAC,gBAAgB,KAAK,SAAS,IAAI,OAAO,OAAO,CAAC,gBAAgB,KAAK,UAAU,EAAE,CAAC;QAC7F,MAAM,IAAI,SAAS,CAAC,sCAAsC,CAAC,CAAA;IAC7D,CAAC;IACD,MAAM,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,IAAI,2BAA2B,CAAA;IAChF,wGAAwG;IACxG,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,KAAK,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,gCAAgC,CAAC,OAAO,CAAC,SAAS,CAAC,CAAA;IAC3G,MAAM,UAAU,GAAG,CAAC,WAAgC,EAAE,EAAE,CACtD,4BAA4B,CAAC;QAC3B,QAAQ,EAAE,OAAO,CAAC,QAAQ;QAC1B,OAAO,EAAE,OAAO,CAAC,OAAO;QACxB,WAAW;QACX,cAAc,EAAE,mCAAmC,CAAC,WAAW,CAAC;QAChE,gBAAgB;QAChB,GAAG,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC3D,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC5C,SAAS;KACV,CAAC,CAAA;IAEJ,0DAA0D;IAC1D,UAAU,CAAC,EAAE,CAAC,CAAA;IAEd,OAAO,CAAC,OAAgB,EAAE,EAAE,CAC1B,UAAU,CAAC,OAAO,CAAC,WAAW,IAAI,OAAO,CAAC,GAAG,CAAC,CAAC,mBAAmB,CAAC,OAAO,CAAC,CAAA;AAC/E,CAAC","sourcesContent":["import 'server-only'\n\nimport type { BrandlabContactContract } from '@brandlab-digital/site-core/contact'\nimport type { BrandlabEnvironment } from '@brandlab-digital/site-core/site'\nimport {\n createBrandlabContactRateLimiter,\n type BrandlabContactRateLimitOptions,\n} from './rate-limit.js'\nimport { createResendSdkContactSend, type ResendContactSend } from './resend.js'\nimport {\n createBrandlabContactRuntime,\n detectBrandlabContactDeploymentMode,\n type BrandlabContactRuntimeDefinitionOptions,\n} from './server.js'\nimport type { ContactBotGuard } from './shared.js'\n\nexport interface BrandlabContactRouteHandlerOptions {\n contract: BrandlabContactContract\n /** Labels, brand name, canonical site URL, email style and attachment settings. */\n runtime: BrandlabContactRuntimeDefinitionOptions\n botGuard?: ContactBotGuard\n /** Defaults to process.env, read on every request. */\n environment?: BrandlabEnvironment\n now?: () => Date\n /** Replaces the Resend SDK in synthetic tests; production code omits it. */\n createResendSend?: (apiKey: string) => ResendContactSend\n /**\n * Abuse limits per client IP and per email address, held for the life of the route module (BRANDLAB_CONTACT_RATE_LIMITS\n * by default; `false` turns them off). They apply only where enquiries are delivered (production and Preview).\n */\n rateLimit?: BrandlabContactRateLimitOptions | false\n}\n\n/**\n * A Resend send function that loads the optional `resend` peer only when an\n * enquiry is actually delivered, so validation, bot and local requests never\n * load the provider SDK.\n */\nexport function createLazyResendContactSend(apiKey: string): ResendContactSend {\n let send: Promise<ResendContactSend> | undefined\n return async (payload, options) => {\n send ??= import('resend').then(({ Resend }) =>\n createResendSdkContactSend(new Resend(apiKey)),\n )\n return (await send)(payload, options)\n }\n}\n\n/**\n * The whole of `src/app/api/contact/route.ts`:\n * `export const POST = createBrandlabContactRouteHandler({ contract, runtime })`.\n * The contract and runtime are validated when the route module loads; each\n * request then resolves the deployment from its environment and delegates to\n * createBrandlabContactRuntime exactly as a per-request runtime would.\n */\nexport function createBrandlabContactRouteHandler(\n options: BrandlabContactRouteHandlerOptions,\n): (request: Request) => Promise<Response> {\n if (!options || typeof options !== 'object') {\n throw new TypeError('createBrandlabContactRouteHandler requires the contract and runtime.')\n }\n if (options.createResendSend !== undefined && typeof options.createResendSend !== 'function') {\n throw new TypeError('createResendSend must be a function.')\n }\n const createResendSend = options.createResendSend ?? createLazyResendContactSend\n // One limiter for the module, shared by every per-request runtime, so its windows outlive each request.\n const rateLimit = options.rateLimit === false ? false : createBrandlabContactRateLimiter(options.rateLimit)\n const runtimeFor = (environment: BrandlabEnvironment) =>\n createBrandlabContactRuntime({\n contract: options.contract,\n runtime: options.runtime,\n environment,\n deploymentMode: detectBrandlabContactDeploymentMode(environment),\n createResendSend,\n ...(options.botGuard ? { botGuard: options.botGuard } : {}),\n ...(options.now ? { now: options.now } : {}),\n rateLimit,\n })\n\n // Fail at module load, not on a customer's first enquiry.\n runtimeFor({})\n\n return (request: Request) =>\n runtimeFor(options.environment ?? process.env).contactRouteHandler(request)\n}\n"]}
1
+ {"version":3,"file":"route-handler.js","sourceRoot":"","sources":["../../src/contact/route-handler.ts"],"names":[],"mappings":"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","sourcesContent":["import 'server-only'\n\nimport type { BrandlabContactContract } from '@brandlab-digital/site-core/contact'\nimport type { BrandlabEnvironment } from '@brandlab-digital/site-core/site'\nimport {\n createBrandlabContactRateLimiter,\n type BrandlabContactRateLimitOptions,\n} from './rate-limit.js'\nimport {\n createResendSuppressionCheck,\n type BrandlabContactDeliveryAlert,\n type BrandlabContactSuppressionCheck,\n} from './delivery.js'\nimport { createResendSdkContactSend, type ResendContactSend } from './resend.js'\nimport {\n createBrandlabContactRuntime,\n detectBrandlabContactDeploymentMode,\n type BrandlabContactRuntimeDefinitionOptions,\n} from './server.js'\nimport type { ContactBotGuard } from './shared.js'\n\nexport interface BrandlabContactRouteHandlerOptions {\n contract: BrandlabContactContract\n /** Labels, brand name, canonical site URL, email style and attachment settings. */\n runtime: BrandlabContactRuntimeDefinitionOptions\n botGuard?: ContactBotGuard\n /** Defaults to process.env, read on every request. */\n environment?: BrandlabEnvironment\n now?: () => Date\n /** Replaces the Resend SDK in synthetic tests; production code omits it. */\n createResendSend?: (apiKey: string) => ResendContactSend\n /**\n * Abuse limits per client IP and per email address, held for the life of the route module (BRANDLAB_CONTACT_RATE_LIMITS\n * by default; `false` turns them off). They apply only where enquiries are delivered (production and Preview).\n */\n rateLimit?: BrandlabContactRateLimitOptions | false\n /**\n * The pre-send check of each business inbox against Resend's suppression list (3.14.0), held for the life of the\n * route module so its answers are cached (5 minutes per inbox). On by default with Resend's suppressions API and a\n * full-access key: `RESEND_LOOKUP_API_KEY` when set, otherwise `RESEND_API_KEY`. A sending-only key is refused, logged\n * once per 5 minutes naming the missing permission, and the enquiry is sent as before. Off by default when\n * `createResendSend` replaces the provider, and `false` turns it off.\n */\n suppressionCheck?: BrandlabContactSuppressionCheck | false\n /**\n * Opt-in only (default `false`): when every business inbox is suppressed, refuse and ask the visitor to call. By\n * default the suppression check only records and alerts, and the enquiry is sent to the client's own inbox.\n */\n refuseWhenAllSuppressed?: boolean\n /** The same-day alert for a delivery failure the route finds before sending (3.14.0). */\n onDeliveryFailure?: BrandlabContactDeliveryAlert\n}\n\n/**\n * A Resend send function that loads the optional `resend` peer only when an\n * enquiry is actually delivered, so validation, bot and local requests never\n * load the provider SDK.\n */\nexport function createLazyResendContactSend(apiKey: string): ResendContactSend {\n let send: Promise<ResendContactSend> | undefined\n return async (payload, options) => {\n send ??= import('resend').then(({ Resend }) =>\n createResendSdkContactSend(new Resend(apiKey)),\n )\n return (await send)(payload, options)\n }\n}\n\n/**\n * The whole of `src/app/api/contact/route.ts`:\n * `export const POST = createBrandlabContactRouteHandler({ contract, runtime })`.\n * The contract and runtime are validated when the route module loads; each\n * request then resolves the deployment from its environment and delegates to\n * createBrandlabContactRuntime exactly as a per-request runtime would.\n */\nexport function createBrandlabContactRouteHandler(\n options: BrandlabContactRouteHandlerOptions,\n): (request: Request) => Promise<Response> {\n if (!options || typeof options !== 'object') {\n throw new TypeError('createBrandlabContactRouteHandler requires the contract and runtime.')\n }\n if (options.createResendSend !== undefined && typeof options.createResendSend !== 'function') {\n throw new TypeError('createResendSend must be a function.')\n }\n const createResendSend = options.createResendSend ?? createLazyResendContactSend\n // One suppression cache for the module. A synthetic test that replaces the provider never asks the real one.\n const suppressionCheck =\n options.suppressionCheck === false\n ? undefined\n : options.suppressionCheck ?? (options.createResendSend ? undefined : createResendSuppressionCheck())\n // One limiter for the module, shared by every per-request runtime, so its windows outlive each request.\n const rateLimit = options.rateLimit === false ? false : createBrandlabContactRateLimiter(options.rateLimit)\n const runtimeFor = (environment: BrandlabEnvironment) =>\n createBrandlabContactRuntime({\n contract: options.contract,\n runtime: options.runtime,\n environment,\n deploymentMode: detectBrandlabContactDeploymentMode(environment),\n createResendSend,\n ...(options.botGuard ? { botGuard: options.botGuard } : {}),\n ...(options.now ? { now: options.now } : {}),\n rateLimit,\n ...(suppressionCheck ? { suppressionCheck } : {}),\n ...(options.onDeliveryFailure ? { onDeliveryFailure: options.onDeliveryFailure } : {}),\n ...(options.refuseWhenAllSuppressed === true ? { refuseWhenAllSuppressed: true } : {}),\n })\n\n // Fail at module load, not on a customer's first enquiry.\n runtimeFor({})\n\n return (request: Request) =>\n runtimeFor(options.environment ?? process.env).contactRouteHandler(request)\n}\n"]}
@@ -1,4 +1,6 @@
1
1
  import 'server-only';
2
2
  export { BRANDLAB_CONTACT_API_KEY_ENV, BRANDLAB_CONTACT_MAX_REQUEST_BYTES, BRANDLAB_CONTACT_RATE_LIMITS, BRANDLAB_CONTACT_RUNTIME, createBrandlabContactRateLimiter, createBrandlabContactRuntime, createResendSdkContactSend, detectBrandlabContactDeploymentMode, type BrandlabContactRateLimitOptions, type BrandlabContactRateLimitRule, type BrandlabContactRateLimiter, type BrandlabContactRuntime, type BrandlabContactRuntimeDefinitionOptions, type BrandlabContactRuntimeOptions, type ResendContactSend, type ResendSdkContactClient, } from './server.js';
3
3
  export { createBrandlabContactRouteHandler, createLazyResendContactSend, type BrandlabContactRouteHandlerOptions, } from './route-handler.js';
4
+ export { BRANDLAB_CONTACT_DELIVERY_ALERT_FAILED_EVENT, BRANDLAB_CONTACT_DELIVERY_FAILED_EVENT, BRANDLAB_CONTACT_DELIVERY_STATUS_EVENT, BRANDLAB_CONTACT_LOOKUP_API_KEY_ENV, BRANDLAB_CONTACT_SUPPRESSION_CACHE_MS, BRANDLAB_CONTACT_SUPPRESSION_UNAVAILABLE_EVENT, createResendSuppressionCheck, type BrandlabContactDeliveryAlert, type BrandlabContactDeliveryFailure, type BrandlabContactDeliveryFailureCode, type BrandlabContactSuppressionCheck, type BrandlabContactSuppressionStatus, type ResendSuppressionCheckOptions, } from './delivery.js';
5
+ export { BRANDLAB_RESEND_WEBHOOK_RATE_LIMIT, BRANDLAB_RESEND_WEBHOOK_SECRET_ENV, createBrandlabResendWebhookRoute, verifyResendWebhookSignature, type BrandlabResendWebhookRoute, type BrandlabResendWebhookRouteOptions, } from './resend-webhook.js';
4
6
  //# sourceMappingURL=server-entry.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"server-entry.d.ts","sourceRoot":"","sources":["../../src/contact/server-entry.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAA;AAEpB,OAAO,EACL,4BAA4B,EAC5B,kCAAkC,EAClC,4BAA4B,EAC5B,wBAAwB,EACxB,gCAAgC,EAChC,4BAA4B,EAC5B,0BAA0B,EAC1B,mCAAmC,EACnC,KAAK,+BAA+B,EACpC,KAAK,4BAA4B,EACjC,KAAK,0BAA0B,EAC/B,KAAK,sBAAsB,EAC3B,KAAK,uCAAuC,EAC5C,KAAK,6BAA6B,EAClC,KAAK,iBAAiB,EACtB,KAAK,sBAAsB,GAC5B,MAAM,aAAa,CAAA;AACpB,OAAO,EACL,iCAAiC,EACjC,2BAA2B,EAC3B,KAAK,kCAAkC,GACxC,MAAM,oBAAoB,CAAA"}
1
+ {"version":3,"file":"server-entry.d.ts","sourceRoot":"","sources":["../../src/contact/server-entry.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAA;AAEpB,OAAO,EACL,4BAA4B,EAC5B,kCAAkC,EAClC,4BAA4B,EAC5B,wBAAwB,EACxB,gCAAgC,EAChC,4BAA4B,EAC5B,0BAA0B,EAC1B,mCAAmC,EACnC,KAAK,+BAA+B,EACpC,KAAK,4BAA4B,EACjC,KAAK,0BAA0B,EAC/B,KAAK,sBAAsB,EAC3B,KAAK,uCAAuC,EAC5C,KAAK,6BAA6B,EAClC,KAAK,iBAAiB,EACtB,KAAK,sBAAsB,GAC5B,MAAM,aAAa,CAAA;AACpB,OAAO,EACL,iCAAiC,EACjC,2BAA2B,EAC3B,KAAK,kCAAkC,GACxC,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EACL,4CAA4C,EAC5C,sCAAsC,EACtC,sCAAsC,EACtC,mCAAmC,EACnC,qCAAqC,EACrC,8CAA8C,EAC9C,4BAA4B,EAC5B,KAAK,4BAA4B,EACjC,KAAK,8BAA8B,EACnC,KAAK,kCAAkC,EACvC,KAAK,+BAA+B,EACpC,KAAK,gCAAgC,EACrC,KAAK,6BAA6B,GACnC,MAAM,eAAe,CAAA;AACtB,OAAO,EACL,kCAAkC,EAClC,kCAAkC,EAClC,gCAAgC,EAChC,4BAA4B,EAC5B,KAAK,0BAA0B,EAC/B,KAAK,iCAAiC,GACvC,MAAM,qBAAqB,CAAA"}
@@ -1,4 +1,6 @@
1
1
  import 'server-only';
2
2
  export { BRANDLAB_CONTACT_API_KEY_ENV, BRANDLAB_CONTACT_MAX_REQUEST_BYTES, BRANDLAB_CONTACT_RATE_LIMITS, BRANDLAB_CONTACT_RUNTIME, createBrandlabContactRateLimiter, createBrandlabContactRuntime, createResendSdkContactSend, detectBrandlabContactDeploymentMode, } from './server.js';
3
3
  export { createBrandlabContactRouteHandler, createLazyResendContactSend, } from './route-handler.js';
4
+ export { BRANDLAB_CONTACT_DELIVERY_ALERT_FAILED_EVENT, BRANDLAB_CONTACT_DELIVERY_FAILED_EVENT, BRANDLAB_CONTACT_DELIVERY_STATUS_EVENT, BRANDLAB_CONTACT_LOOKUP_API_KEY_ENV, BRANDLAB_CONTACT_SUPPRESSION_CACHE_MS, BRANDLAB_CONTACT_SUPPRESSION_UNAVAILABLE_EVENT, createResendSuppressionCheck, } from './delivery.js';
5
+ export { BRANDLAB_RESEND_WEBHOOK_RATE_LIMIT, BRANDLAB_RESEND_WEBHOOK_SECRET_ENV, createBrandlabResendWebhookRoute, verifyResendWebhookSignature, } from './resend-webhook.js';
4
6
  //# sourceMappingURL=server-entry.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"server-entry.js","sourceRoot":"","sources":["../../src/contact/server-entry.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAA;AAEpB,OAAO,EACL,4BAA4B,EAC5B,kCAAkC,EAClC,4BAA4B,EAC5B,wBAAwB,EACxB,gCAAgC,EAChC,4BAA4B,EAC5B,0BAA0B,EAC1B,mCAAmC,GASpC,MAAM,aAAa,CAAA;AACpB,OAAO,EACL,iCAAiC,EACjC,2BAA2B,GAE5B,MAAM,oBAAoB,CAAA","sourcesContent":["import 'server-only'\n\nexport {\n BRANDLAB_CONTACT_API_KEY_ENV,\n BRANDLAB_CONTACT_MAX_REQUEST_BYTES,\n BRANDLAB_CONTACT_RATE_LIMITS,\n BRANDLAB_CONTACT_RUNTIME,\n createBrandlabContactRateLimiter,\n createBrandlabContactRuntime,\n createResendSdkContactSend,\n detectBrandlabContactDeploymentMode,\n type BrandlabContactRateLimitOptions,\n type BrandlabContactRateLimitRule,\n type BrandlabContactRateLimiter,\n type BrandlabContactRuntime,\n type BrandlabContactRuntimeDefinitionOptions,\n type BrandlabContactRuntimeOptions,\n type ResendContactSend,\n type ResendSdkContactClient,\n} from './server.js'\nexport {\n createBrandlabContactRouteHandler,\n createLazyResendContactSend,\n type BrandlabContactRouteHandlerOptions,\n} from './route-handler.js'\n"]}
1
+ {"version":3,"file":"server-entry.js","sourceRoot":"","sources":["../../src/contact/server-entry.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAA;AAEpB,OAAO,EACL,4BAA4B,EAC5B,kCAAkC,EAClC,4BAA4B,EAC5B,wBAAwB,EACxB,gCAAgC,EAChC,4BAA4B,EAC5B,0BAA0B,EAC1B,mCAAmC,GASpC,MAAM,aAAa,CAAA;AACpB,OAAO,EACL,iCAAiC,EACjC,2BAA2B,GAE5B,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EACL,4CAA4C,EAC5C,sCAAsC,EACtC,sCAAsC,EACtC,mCAAmC,EACnC,qCAAqC,EACrC,8CAA8C,EAC9C,4BAA4B,GAO7B,MAAM,eAAe,CAAA;AACtB,OAAO,EACL,kCAAkC,EAClC,kCAAkC,EAClC,gCAAgC,EAChC,4BAA4B,GAG7B,MAAM,qBAAqB,CAAA","sourcesContent":["import 'server-only'\n\nexport {\n BRANDLAB_CONTACT_API_KEY_ENV,\n BRANDLAB_CONTACT_MAX_REQUEST_BYTES,\n BRANDLAB_CONTACT_RATE_LIMITS,\n BRANDLAB_CONTACT_RUNTIME,\n createBrandlabContactRateLimiter,\n createBrandlabContactRuntime,\n createResendSdkContactSend,\n detectBrandlabContactDeploymentMode,\n type BrandlabContactRateLimitOptions,\n type BrandlabContactRateLimitRule,\n type BrandlabContactRateLimiter,\n type BrandlabContactRuntime,\n type BrandlabContactRuntimeDefinitionOptions,\n type BrandlabContactRuntimeOptions,\n type ResendContactSend,\n type ResendSdkContactClient,\n} from './server.js'\nexport {\n createBrandlabContactRouteHandler,\n createLazyResendContactSend,\n type BrandlabContactRouteHandlerOptions,\n} from './route-handler.js'\nexport {\n BRANDLAB_CONTACT_DELIVERY_ALERT_FAILED_EVENT,\n BRANDLAB_CONTACT_DELIVERY_FAILED_EVENT,\n BRANDLAB_CONTACT_DELIVERY_STATUS_EVENT,\n BRANDLAB_CONTACT_LOOKUP_API_KEY_ENV,\n BRANDLAB_CONTACT_SUPPRESSION_CACHE_MS,\n BRANDLAB_CONTACT_SUPPRESSION_UNAVAILABLE_EVENT,\n createResendSuppressionCheck,\n type BrandlabContactDeliveryAlert,\n type BrandlabContactDeliveryFailure,\n type BrandlabContactDeliveryFailureCode,\n type BrandlabContactSuppressionCheck,\n type BrandlabContactSuppressionStatus,\n type ResendSuppressionCheckOptions,\n} from './delivery.js'\nexport {\n BRANDLAB_RESEND_WEBHOOK_RATE_LIMIT,\n BRANDLAB_RESEND_WEBHOOK_SECRET_ENV,\n createBrandlabResendWebhookRoute,\n verifyResendWebhookSignature,\n type BrandlabResendWebhookRoute,\n type BrandlabResendWebhookRouteOptions,\n} from './resend-webhook.js'\n"]}