@aglyn/plugins-commerce 1.0.0-beta.143

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 (417) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +10 -0
  3. package/package.json +57 -0
  4. package/src/index.d.ts +38 -0
  5. package/src/index.js +38 -0
  6. package/src/index.js.map +1 -0
  7. package/src/lib/components/account.d.ts +31 -0
  8. package/src/lib/components/account.js +645 -0
  9. package/src/lib/components/account.js.map +1 -0
  10. package/src/lib/components/cart.d.ts +38 -0
  11. package/src/lib/components/cart.js +851 -0
  12. package/src/lib/components/cart.js.map +1 -0
  13. package/src/lib/components/commerce-console-page.d.ts +32 -0
  14. package/src/lib/components/commerce-console-page.js +296 -0
  15. package/src/lib/components/commerce-console-page.js.map +1 -0
  16. package/src/lib/components/commerce-console-sections.d.ts +34 -0
  17. package/src/lib/components/commerce-console-sections.js +62 -0
  18. package/src/lib/components/commerce-console-sections.js.map +1 -0
  19. package/src/lib/components/console/catalog-organization-card.component.d.ts +14 -0
  20. package/src/lib/components/console/catalog-organization-card.component.js +1087 -0
  21. package/src/lib/components/console/catalog-organization-card.component.js.map +1 -0
  22. package/src/lib/components/console/commerce-analytics-card.component.d.ts +27 -0
  23. package/src/lib/components/console/commerce-analytics-card.component.js +310 -0
  24. package/src/lib/components/console/commerce-analytics-card.component.js.map +1 -0
  25. package/src/lib/components/console/commerce-glance-card.component.d.ts +20 -0
  26. package/src/lib/components/console/commerce-glance-card.component.js +340 -0
  27. package/src/lib/components/console/commerce-glance-card.component.js.map +1 -0
  28. package/src/lib/components/console/commerce-stat-tile.component.d.ts +37 -0
  29. package/src/lib/components/console/commerce-stat-tile.component.js +72 -0
  30. package/src/lib/components/console/commerce-stat-tile.component.js.map +1 -0
  31. package/src/lib/components/console/discounts-card.component.d.ts +14 -0
  32. package/src/lib/components/console/discounts-card.component.js +443 -0
  33. package/src/lib/components/console/discounts-card.component.js.map +1 -0
  34. package/src/lib/components/console/entitlement-gate.component.d.ts +76 -0
  35. package/src/lib/components/console/entitlement-gate.component.js +115 -0
  36. package/src/lib/components/console/entitlement-gate.component.js.map +1 -0
  37. package/src/lib/components/console/gift-cards-card.component.d.ts +29 -0
  38. package/src/lib/components/console/gift-cards-card.component.js +379 -0
  39. package/src/lib/components/console/gift-cards-card.component.js.map +1 -0
  40. package/src/lib/components/console/host-coupons-card.component.d.ts +14 -0
  41. package/src/lib/components/console/host-coupons-card.component.js +252 -0
  42. package/src/lib/components/console/host-coupons-card.component.js.map +1 -0
  43. package/src/lib/components/console/host-orders-card.component.d.ts +13 -0
  44. package/src/lib/components/console/host-orders-card.component.js +861 -0
  45. package/src/lib/components/console/host-orders-card.component.js.map +1 -0
  46. package/src/lib/components/console/locations-card.component.d.ts +14 -0
  47. package/src/lib/components/console/locations-card.component.js +256 -0
  48. package/src/lib/components/console/locations-card.component.js.map +1 -0
  49. package/src/lib/components/console/member-posts-card.component.d.ts +8 -0
  50. package/src/lib/components/console/member-posts-card.component.js +283 -0
  51. package/src/lib/components/console/member-posts-card.component.js.map +1 -0
  52. package/src/lib/components/console/newest-site-users-card.component.d.ts +36 -0
  53. package/src/lib/components/console/newest-site-users-card.component.js +126 -0
  54. package/src/lib/components/console/newest-site-users-card.component.js.map +1 -0
  55. package/src/lib/components/console/order-detail-dialog.component.d.ts +28 -0
  56. package/src/lib/components/console/order-detail-dialog.component.js +976 -0
  57. package/src/lib/components/console/order-detail-dialog.component.js.map +1 -0
  58. package/src/lib/components/console/paid-media.d.ts +127 -0
  59. package/src/lib/components/console/paid-media.js +397 -0
  60. package/src/lib/components/console/paid-media.js.map +1 -0
  61. package/src/lib/components/console/payments-settings-card.component.d.ts +13 -0
  62. package/src/lib/components/console/payments-settings-card.component.js +295 -0
  63. package/src/lib/components/console/payments-settings-card.component.js.map +1 -0
  64. package/src/lib/components/console/pos-page.component.d.ts +13 -0
  65. package/src/lib/components/console/pos-page.component.js +916 -0
  66. package/src/lib/components/console/pos-page.component.js.map +1 -0
  67. package/src/lib/components/console/product-editor-dialog.component.d.ts +32 -0
  68. package/src/lib/components/console/product-editor-dialog.component.js +1374 -0
  69. package/src/lib/components/console/product-editor-dialog.component.js.map +1 -0
  70. package/src/lib/components/console/products-hub-card.component.d.ts +16 -0
  71. package/src/lib/components/console/products-hub-card.component.js +1280 -0
  72. package/src/lib/components/console/products-hub-card.component.js.map +1 -0
  73. package/src/lib/components/console/products-hub-zone.component.d.ts +50 -0
  74. package/src/lib/components/console/products-hub-zone.component.js +200 -0
  75. package/src/lib/components/console/products-hub-zone.component.js.map +1 -0
  76. package/src/lib/components/console/recovery-queue-card.component.d.ts +39 -0
  77. package/src/lib/components/console/recovery-queue-card.component.js +262 -0
  78. package/src/lib/components/console/recovery-queue-card.component.js.map +1 -0
  79. package/src/lib/components/console/registers-card.component.d.ts +21 -0
  80. package/src/lib/components/console/registers-card.component.js +296 -0
  81. package/src/lib/components/console/registers-card.component.js.map +1 -0
  82. package/src/lib/components/console/reservations-card.component.d.ts +14 -0
  83. package/src/lib/components/console/reservations-card.component.js +650 -0
  84. package/src/lib/components/console/reservations-card.component.js.map +1 -0
  85. package/src/lib/components/console/reviews-moderation-card.component.d.ts +8 -0
  86. package/src/lib/components/console/reviews-moderation-card.component.js +235 -0
  87. package/src/lib/components/console/reviews-moderation-card.component.js.map +1 -0
  88. package/src/lib/components/console/shipping-settings-card.component.d.ts +38 -0
  89. package/src/lib/components/console/shipping-settings-card.component.js +560 -0
  90. package/src/lib/components/console/shipping-settings-card.component.js.map +1 -0
  91. package/src/lib/components/console/stock-movements-card.component.d.ts +31 -0
  92. package/src/lib/components/console/stock-movements-card.component.js +333 -0
  93. package/src/lib/components/console/stock-movements-card.component.js.map +1 -0
  94. package/src/lib/components/console/store-settings-card.component.d.ts +14 -0
  95. package/src/lib/components/console/store-settings-card.component.js +300 -0
  96. package/src/lib/components/console/store-settings-card.component.js.map +1 -0
  97. package/src/lib/components/console/storefront-tax-summary-card.component.d.ts +73 -0
  98. package/src/lib/components/console/storefront-tax-summary-card.component.js +408 -0
  99. package/src/lib/components/console/storefront-tax-summary-card.component.js.map +1 -0
  100. package/src/lib/components/console/suppliers-card.component.d.ts +8 -0
  101. package/src/lib/components/console/suppliers-card.component.js +328 -0
  102. package/src/lib/components/console/suppliers-card.component.js.map +1 -0
  103. package/src/lib/components/console/tax-settings-card.component.d.ts +14 -0
  104. package/src/lib/components/console/tax-settings-card.component.js +473 -0
  105. package/src/lib/components/console/tax-settings-card.component.js.map +1 -0
  106. package/src/lib/components/gate.d.ts +39 -0
  107. package/src/lib/components/gate.js +202 -0
  108. package/src/lib/components/gate.js.map +1 -0
  109. package/src/lib/components/gated-video.d.ts +40 -0
  110. package/src/lib/components/gated-video.js +366 -0
  111. package/src/lib/components/gated-video.js.map +1 -0
  112. package/src/lib/components/member-feed.d.ts +32 -0
  113. package/src/lib/components/member-feed.js +198 -0
  114. package/src/lib/components/member-feed.js.map +1 -0
  115. package/src/lib/components/member-recovery.d.ts +35 -0
  116. package/src/lib/components/member-recovery.js +313 -0
  117. package/src/lib/components/member-recovery.js.map +1 -0
  118. package/src/lib/components/member-signin.d.ts +35 -0
  119. package/src/lib/components/member-signin.js +257 -0
  120. package/src/lib/components/member-signin.js.map +1 -0
  121. package/src/lib/components/member-signup.d.ts +34 -0
  122. package/src/lib/components/member-signup.js +287 -0
  123. package/src/lib/components/member-signup.js.map +1 -0
  124. package/src/lib/components/newsletter-signup.d.ts +39 -0
  125. package/src/lib/components/newsletter-signup.js +229 -0
  126. package/src/lib/components/newsletter-signup.js.map +1 -0
  127. package/src/lib/components/product-detail.d.ts +38 -0
  128. package/src/lib/components/product-detail.js +1022 -0
  129. package/src/lib/components/product-detail.js.map +1 -0
  130. package/src/lib/components/product-grid.d.ts +67 -0
  131. package/src/lib/components/product-grid.js +909 -0
  132. package/src/lib/components/product-grid.js.map +1 -0
  133. package/src/lib/components/product-reviews.d.ts +32 -0
  134. package/src/lib/components/product-reviews.js +349 -0
  135. package/src/lib/components/product-reviews.js.map +1 -0
  136. package/src/lib/components/related-products.d.ts +33 -0
  137. package/src/lib/components/related-products.js +232 -0
  138. package/src/lib/components/related-products.js.map +1 -0
  139. package/src/lib/components/reservation-widget.d.ts +33 -0
  140. package/src/lib/components/reservation-widget.js +418 -0
  141. package/src/lib/components/reservation-widget.js.map +1 -0
  142. package/src/lib/components/storefront-payment-element-fallback.d.ts +18 -0
  143. package/src/lib/components/storefront-payment-element-fallback.js +51 -0
  144. package/src/lib/components/storefront-payment-element-fallback.js.map +1 -0
  145. package/src/lib/components/storefront-payment-element.d.ts +78 -0
  146. package/src/lib/components/storefront-payment-element.js +156 -0
  147. package/src/lib/components/storefront-payment-element.js.map +1 -0
  148. package/src/lib/components/wishlist.d.ts +46 -0
  149. package/src/lib/components/wishlist.js +304 -0
  150. package/src/lib/components/wishlist.js.map +1 -0
  151. package/src/lib/constants/bundle-common.d.ts +18 -0
  152. package/src/lib/constants/bundle-common.js +18 -0
  153. package/src/lib/constants/bundle-common.js.map +1 -0
  154. package/src/lib/constants/product-filters.d.ts +35 -0
  155. package/src/lib/constants/product-filters.js +109 -0
  156. package/src/lib/constants/product-filters.js.map +1 -0
  157. package/src/lib/model/commerce-cart.d.ts +52 -0
  158. package/src/lib/model/commerce-cart.js +102 -0
  159. package/src/lib/model/commerce-cart.js.map +1 -0
  160. package/src/lib/model/commerce-connect-transfer.d.ts +243 -0
  161. package/src/lib/model/commerce-connect-transfer.js +231 -0
  162. package/src/lib/model/commerce-connect-transfer.js.map +1 -0
  163. package/src/lib/model/commerce-discounts.d.ts +164 -0
  164. package/src/lib/model/commerce-discounts.js +231 -0
  165. package/src/lib/model/commerce-discounts.js.map +1 -0
  166. package/src/lib/model/commerce-dispute.d.ts +124 -0
  167. package/src/lib/model/commerce-dispute.js +225 -0
  168. package/src/lib/model/commerce-dispute.js.map +1 -0
  169. package/src/lib/model/commerce-gift-cards.d.ts +107 -0
  170. package/src/lib/model/commerce-gift-cards.js +121 -0
  171. package/src/lib/model/commerce-gift-cards.js.map +1 -0
  172. package/src/lib/model/commerce-io.d.ts +36 -0
  173. package/src/lib/model/commerce-io.js +245 -0
  174. package/src/lib/model/commerce-io.js.map +1 -0
  175. package/src/lib/model/commerce-orders.d.ts +1040 -0
  176. package/src/lib/model/commerce-orders.js +1022 -0
  177. package/src/lib/model/commerce-orders.js.map +1 -0
  178. package/src/lib/model/commerce-promotions.d.ts +188 -0
  179. package/src/lib/model/commerce-promotions.js +199 -0
  180. package/src/lib/model/commerce-promotions.js.map +1 -0
  181. package/src/lib/model/commerce-reservations.d.ts +175 -0
  182. package/src/lib/model/commerce-reservations.js +157 -0
  183. package/src/lib/model/commerce-reservations.js.map +1 -0
  184. package/src/lib/model/commerce-shipping.d.ts +316 -0
  185. package/src/lib/model/commerce-shipping.js +484 -0
  186. package/src/lib/model/commerce-shipping.js.map +1 -0
  187. package/src/lib/model/commerce-stock-holds.d.ts +199 -0
  188. package/src/lib/model/commerce-stock-holds.js +190 -0
  189. package/src/lib/model/commerce-stock-holds.js.map +1 -0
  190. package/src/lib/model/commerce-subscription-status.d.ts +67 -0
  191. package/src/lib/model/commerce-subscription-status.js +76 -0
  192. package/src/lib/model/commerce-subscription-status.js.map +1 -0
  193. package/src/lib/model/commerce-tax-decision.d.ts +222 -0
  194. package/src/lib/model/commerce-tax-decision.js +165 -0
  195. package/src/lib/model/commerce-tax-decision.js.map +1 -0
  196. package/src/lib/model/commerce-tax.d.ts +176 -0
  197. package/src/lib/model/commerce-tax.js +92 -0
  198. package/src/lib/model/commerce-tax.js.map +1 -0
  199. package/src/lib/model/commerce.d.ts +551 -0
  200. package/src/lib/model/commerce.js +739 -0
  201. package/src/lib/model/commerce.js.map +1 -0
  202. package/src/lib/model/index.d.ts +40 -0
  203. package/src/lib/model/index.js +40 -0
  204. package/src/lib/model/index.js.map +1 -0
  205. package/src/lib/model/order-figures.d.ts +48 -0
  206. package/src/lib/model/order-figures.js +74 -0
  207. package/src/lib/model/order-figures.js.map +1 -0
  208. package/src/lib/model/plugin-permissions.d.ts +23 -0
  209. package/src/lib/model/plugin-permissions.js +34 -0
  210. package/src/lib/model/plugin-permissions.js.map +1 -0
  211. package/src/lib/model/purchase-analytics.d.ts +186 -0
  212. package/src/lib/model/purchase-analytics.js +216 -0
  213. package/src/lib/model/purchase-analytics.js.map +1 -0
  214. package/src/lib/plugin-config.d.ts +62 -0
  215. package/src/lib/plugin-config.js +78 -0
  216. package/src/lib/plugin-config.js.map +1 -0
  217. package/src/lib/plugin.d.ts +28 -0
  218. package/src/lib/plugin.js +147 -0
  219. package/src/lib/plugin.js.map +1 -0
  220. package/src/lib/server/billing-webhook.d.ts +26 -0
  221. package/src/lib/server/billing-webhook.js +4011 -0
  222. package/src/lib/server/billing-webhook.js.map +1 -0
  223. package/src/lib/server/cancel-order.d.ts +87 -0
  224. package/src/lib/server/cancel-order.js +368 -0
  225. package/src/lib/server/cancel-order.js.map +1 -0
  226. package/src/lib/server/cart-checkout.d.ts +25 -0
  227. package/src/lib/server/cart-checkout.js +976 -0
  228. package/src/lib/server/cart-checkout.js.map +1 -0
  229. package/src/lib/server/cart-cookie.d.ts +32 -0
  230. package/src/lib/server/cart-cookie.js +77 -0
  231. package/src/lib/server/cart-cookie.js.map +1 -0
  232. package/src/lib/server/cart.d.ts +34 -0
  233. package/src/lib/server/cart.js +181 -0
  234. package/src/lib/server/cart.js.map +1 -0
  235. package/src/lib/server/catalog.d.ts +127 -0
  236. package/src/lib/server/catalog.js +277 -0
  237. package/src/lib/server/catalog.js.map +1 -0
  238. package/src/lib/server/checkout.d.ts +36 -0
  239. package/src/lib/server/checkout.js +915 -0
  240. package/src/lib/server/checkout.js.map +1 -0
  241. package/src/lib/server/connect.d.ts +26 -0
  242. package/src/lib/server/connect.js +238 -0
  243. package/src/lib/server/connect.js.map +1 -0
  244. package/src/lib/server/contact-refund.d.ts +117 -0
  245. package/src/lib/server/contact-refund.js +196 -0
  246. package/src/lib/server/contact-refund.js.map +1 -0
  247. package/src/lib/server/download.d.ts +39 -0
  248. package/src/lib/server/download.js +206 -0
  249. package/src/lib/server/download.js.map +1 -0
  250. package/src/lib/server/draft-order.d.ts +26 -0
  251. package/src/lib/server/draft-order.js +706 -0
  252. package/src/lib/server/draft-order.js.map +1 -0
  253. package/src/lib/server/feed.d.ts +24 -0
  254. package/src/lib/server/feed.js +59 -0
  255. package/src/lib/server/feed.js.map +1 -0
  256. package/src/lib/server/fulfill-order.d.ts +97 -0
  257. package/src/lib/server/fulfill-order.js +235 -0
  258. package/src/lib/server/fulfill-order.js.map +1 -0
  259. package/src/lib/server/gate.d.ts +36 -0
  260. package/src/lib/server/gate.js +112 -0
  261. package/src/lib/server/gate.js.map +1 -0
  262. package/src/lib/server/gift-cards.d.ts +42 -0
  263. package/src/lib/server/gift-cards.js +219 -0
  264. package/src/lib/server/gift-cards.js.map +1 -0
  265. package/src/lib/server/low-stock.d.ts +49 -0
  266. package/src/lib/server/low-stock.js +60 -0
  267. package/src/lib/server/low-stock.js.map +1 -0
  268. package/src/lib/server/manual-tax-rate.d.ts +82 -0
  269. package/src/lib/server/manual-tax-rate.js +113 -0
  270. package/src/lib/server/manual-tax-rate.js.map +1 -0
  271. package/src/lib/server/member-feed.d.ts +24 -0
  272. package/src/lib/server/member-feed.js +72 -0
  273. package/src/lib/server/member-feed.js.map +1 -0
  274. package/src/lib/server/member-name-search.d.ts +37 -0
  275. package/src/lib/server/member-name-search.js +42 -0
  276. package/src/lib/server/member-name-search.js.map +1 -0
  277. package/src/lib/server/member-post.d.ts +23 -0
  278. package/src/lib/server/member-post.js +166 -0
  279. package/src/lib/server/member-post.js.map +1 -0
  280. package/src/lib/server/membership-account.d.ts +37 -0
  281. package/src/lib/server/membership-account.js +148 -0
  282. package/src/lib/server/membership-account.js.map +1 -0
  283. package/src/lib/server/membership-admin-password.d.ts +33 -0
  284. package/src/lib/server/membership-admin-password.js +202 -0
  285. package/src/lib/server/membership-admin-password.js.map +1 -0
  286. package/src/lib/server/membership-content.d.ts +23 -0
  287. package/src/lib/server/membership-content.js +105 -0
  288. package/src/lib/server/membership-content.js.map +1 -0
  289. package/src/lib/server/membership-login.d.ts +19 -0
  290. package/src/lib/server/membership-login.js +137 -0
  291. package/src/lib/server/membership-login.js.map +1 -0
  292. package/src/lib/server/membership-logout.d.ts +19 -0
  293. package/src/lib/server/membership-logout.js +35 -0
  294. package/src/lib/server/membership-logout.js.map +1 -0
  295. package/src/lib/server/membership-recover.d.ts +60 -0
  296. package/src/lib/server/membership-recover.js +244 -0
  297. package/src/lib/server/membership-recover.js.map +1 -0
  298. package/src/lib/server/membership-register.d.ts +22 -0
  299. package/src/lib/server/membership-register.js +241 -0
  300. package/src/lib/server/membership-register.js.map +1 -0
  301. package/src/lib/server/membership-reset.d.ts +26 -0
  302. package/src/lib/server/membership-reset.js +88 -0
  303. package/src/lib/server/membership-reset.js.map +1 -0
  304. package/src/lib/server/membership-wishlist.d.ts +23 -0
  305. package/src/lib/server/membership-wishlist.js +89 -0
  306. package/src/lib/server/membership-wishlist.js.map +1 -0
  307. package/src/lib/server/membership.d.ts +96 -0
  308. package/src/lib/server/membership.js +222 -0
  309. package/src/lib/server/membership.js.map +1 -0
  310. package/src/lib/server/native-checkout.d.ts +170 -0
  311. package/src/lib/server/native-checkout.js +193 -0
  312. package/src/lib/server/native-checkout.js.map +1 -0
  313. package/src/lib/server/newsletter.d.ts +23 -0
  314. package/src/lib/server/newsletter.js +323 -0
  315. package/src/lib/server/newsletter.js.map +1 -0
  316. package/src/lib/server/notify-restock.d.ts +22 -0
  317. package/src/lib/server/notify-restock.js +84 -0
  318. package/src/lib/server/notify-restock.js.map +1 -0
  319. package/src/lib/server/order-analytics.d.ts +134 -0
  320. package/src/lib/server/order-analytics.js +263 -0
  321. package/src/lib/server/order-analytics.js.map +1 -0
  322. package/src/lib/server/order-figures.d.ts +33 -0
  323. package/src/lib/server/order-figures.js +220 -0
  324. package/src/lib/server/order-figures.js.map +1 -0
  325. package/src/lib/server/paid-media-delivery.d.ts +173 -0
  326. package/src/lib/server/paid-media-delivery.js +212 -0
  327. package/src/lib/server/paid-media-delivery.js.map +1 -0
  328. package/src/lib/server/pos-order.d.ts +54 -0
  329. package/src/lib/server/pos-order.js +1137 -0
  330. package/src/lib/server/pos-order.js.map +1 -0
  331. package/src/lib/server/process-abandoned.d.ts +54 -0
  332. package/src/lib/server/process-abandoned.js +222 -0
  333. package/src/lib/server/process-abandoned.js.map +1 -0
  334. package/src/lib/server/process-restock.d.ts +42 -0
  335. package/src/lib/server/process-restock.js +219 -0
  336. package/src/lib/server/process-restock.js.map +1 -0
  337. package/src/lib/server/product-card.d.ts +29 -0
  338. package/src/lib/server/product-card.js +54 -0
  339. package/src/lib/server/product-card.js.map +1 -0
  340. package/src/lib/server/product.d.ts +70 -0
  341. package/src/lib/server/product.js +105 -0
  342. package/src/lib/server/product.js.map +1 -0
  343. package/src/lib/server/promotion-hold.d.ts +144 -0
  344. package/src/lib/server/promotion-hold.js +174 -0
  345. package/src/lib/server/promotion-hold.js.map +1 -0
  346. package/src/lib/server/reconcile-stock.d.ts +106 -0
  347. package/src/lib/server/reconcile-stock.js +349 -0
  348. package/src/lib/server/reconcile-stock.js.map +1 -0
  349. package/src/lib/server/refund.d.ts +45 -0
  350. package/src/lib/server/refund.js +607 -0
  351. package/src/lib/server/refund.js.map +1 -0
  352. package/src/lib/server/related.d.ts +24 -0
  353. package/src/lib/server/related.js +104 -0
  354. package/src/lib/server/related.js.map +1 -0
  355. package/src/lib/server/reservation-availability.d.ts +23 -0
  356. package/src/lib/server/reservation-availability.js +111 -0
  357. package/src/lib/server/reservation-availability.js.map +1 -0
  358. package/src/lib/server/reserve-stock.d.ts +156 -0
  359. package/src/lib/server/reserve-stock.js +146 -0
  360. package/src/lib/server/reserve-stock.js.map +1 -0
  361. package/src/lib/server/reserve.d.ts +25 -0
  362. package/src/lib/server/reserve.js +426 -0
  363. package/src/lib/server/reserve.js.map +1 -0
  364. package/src/lib/server/restock-flag.d.ts +202 -0
  365. package/src/lib/server/restock-flag.js +449 -0
  366. package/src/lib/server/restock-flag.js.map +1 -0
  367. package/src/lib/server/reviews.d.ts +51 -0
  368. package/src/lib/server/reviews.js +132 -0
  369. package/src/lib/server/reviews.js.map +1 -0
  370. package/src/lib/server/site-page-enricher.d.ts +42 -0
  371. package/src/lib/server/site-page-enricher.js +149 -0
  372. package/src/lib/server/site-page-enricher.js.map +1 -0
  373. package/src/lib/server/site-page-resolver.d.ts +26 -0
  374. package/src/lib/server/site-page-resolver.js +223 -0
  375. package/src/lib/server/site-page-resolver.js.map +1 -0
  376. package/src/lib/server/stock-hold.d.ts +127 -0
  377. package/src/lib/server/stock-hold.js +258 -0
  378. package/src/lib/server/stock-hold.js.map +1 -0
  379. package/src/lib/server/storefront-tax-record.d.ts +22 -0
  380. package/src/lib/server/storefront-tax-record.js +218 -0
  381. package/src/lib/server/storefront-tax-record.js.map +1 -0
  382. package/src/lib/server/storefront-tax.d.ts +197 -0
  383. package/src/lib/server/storefront-tax.js +245 -0
  384. package/src/lib/server/storefront-tax.js.map +1 -0
  385. package/src/lib/server/stream.d.ts +25 -0
  386. package/src/lib/server/stream.js +191 -0
  387. package/src/lib/server/stream.js.map +1 -0
  388. package/src/lib/server/subscription-portal.d.ts +24 -0
  389. package/src/lib/server/subscription-portal.js +91 -0
  390. package/src/lib/server/subscription-portal.js.map +1 -0
  391. package/src/lib/server/supplier-outbox.d.ts +190 -0
  392. package/src/lib/server/supplier-outbox.js +342 -0
  393. package/src/lib/server/supplier-outbox.js.map +1 -0
  394. package/src/lib/server/supplier-update.d.ts +23 -0
  395. package/src/lib/server/supplier-update.js +289 -0
  396. package/src/lib/server/supplier-update.js.map +1 -0
  397. package/src/lib/server/tax-profile.d.ts +29 -0
  398. package/src/lib/server/tax-profile.js +40 -0
  399. package/src/lib/server/tax-profile.js.map +1 -0
  400. package/src/lib/server.d.ts +32 -0
  401. package/src/lib/server.js +311 -0
  402. package/src/lib/server.js.map +1 -0
  403. package/src/lib/site.d.ts +27 -0
  404. package/src/lib/site.js +134 -0
  405. package/src/lib/site.js.map +1 -0
  406. package/src/lib/utils/escape-html.d.ts +41 -0
  407. package/src/lib/utils/escape-html.js +41 -0
  408. package/src/lib/utils/escape-html.js.map +1 -0
  409. package/src/lib/utils/generate-preset-id.d.ts +19 -0
  410. package/src/lib/utils/generate-preset-id.js +25 -0
  411. package/src/lib/utils/generate-preset-id.js.map +1 -0
  412. package/src/lib/utils/member-continue.d.ts +45 -0
  413. package/src/lib/utils/member-continue.js +49 -0
  414. package/src/lib/utils/member-continue.js.map +1 -0
  415. package/src/lib/utils/use-storefront-purchase-event.d.ts +60 -0
  416. package/src/lib/utils/use-storefront-purchase-event.js +142 -0
  417. package/src/lib/utils/use-storefront-purchase-event.js.map +1 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/native-checkout.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { isServerReleaseFlagOnForOrg } from '@aglyn/tenant-data-admin'\n\n/**\n * The storefront Payment Element (AGL-1944), the half of native payments\n * AGL-1132 said mattered more: a shopper on a merchant's own domain pays\n * without being bounced to `checkout.stripe.com` and back.\n *\n * ## Why a Checkout Session and not a bare PaymentIntent\n *\n * AGL-1132 recommended \"PaymentIntent + `<PaymentElement/>`\" for this surface,\n * and AGL-1944 asked for that to be a conscious re-decision rather than an\n * inherited one. It is: the Payment Element ships, the bare PaymentIntent does\n * not, and the difference is worth stating because it is the whole safety\n * argument.\n *\n * A PaymentIntent-backed flow would have to own totals, tax, shipping and the\n * platform fee itself — which means a SECOND place that computes what a shopper\n * owes. Two paths that price a cart independently is how a storefront starts\n * under-collecting tax without anyone noticing: nothing fails, the number is\n * just quietly wrong on one of them. It also emits `payment_intent.succeeded`\n * rather than `checkout.session.completed`, so every fulfilment branch in\n * `billing-webhook.ts` — orders, inventory, coupons, gift cards, licence keys,\n * subscriptions — would need a parallel implementation reading a different\n * object shape.\n *\n * `ui_mode: 'custom'` is the Payment Element backed by a Checkout Session. The\n * shopper sees `<PaymentElement/>` inside the merchant's own layout — no Stripe\n * chrome, which is the thing AGL-1944 objected to about embedded Checkout — and\n * the server keeps the session builder it already has. So:\n *\n * - tax, shipping, inventory, coupons, the Connect destination and the fee\n * ladder are the SAME code, not a parallel implementation. Parity is by\n * construction rather than by test;\n * - `checkout.session.completed` still fires, so `billing-webhook.ts` needs\n * no change at all and stays the only thing that fulfils an order.\n *\n * ## The API version, and why it is pinned per-request\n *\n * Measured against live Stripe in test mode: `ui_mode: 'custom'` is refused at\n * the platform account's default API version with\n *\n * Invalid Stripe API version: 2020-08-27. In order to use `ui_mode:\n * custom`, you must upgrade to Stripe API version 2025-03-31.basil.\n *\n * Upgrading the ACCOUNT default would re-version every Stripe call in this\n * repo at once — a change whose blast radius nobody could review in one pass.\n * A `Stripe-Version` header pins one request instead, and the pin is contained\n * in a way worth being explicit about: webhook deliveries are versioned per\n * ENDPOINT, not per request, so the events `billing-webhook.ts` parses keep\n * arriving in exactly the shape it already reads. The only thing read off the\n * pinned response is `client_secret` and `id`, neither of which the version\n * affects.\n *\n * Same probe, same params (line items, a manual tax line,\n * `shipping_address_collection`, a `fixed_amount` shipping option, Connect\n * `transfer_data[destination]` and `application_fee_amount`), hosted vs custom:\n * `amount_total` **6212 in both**. That is the tax-and-shipping parity claim,\n * measured rather than assumed.\n *\n * ## What this module deliberately does NOT do\n *\n * It does not fulfil anything, and it hands the browser nothing it could use to\n * claim an order was paid. The client gets a `clientSecret` — which is an\n * instruction to Stripe, not an assertion about money — and the same\n * `session_id` the redirect flow already put in the return URL. Fulfilment\n * remains `checkout.session.completed` in `billing-webhook.ts`, exactly as with\n * the redirect. An in-page flow makes trusting the browser tempting; the\n * browser is the one participant that can lie about having paid.\n */\n\n/**\n * The API version `ui_mode: 'custom'` needs, pinned on the session-create\n * request only. Stated once so the header and the doc comment cannot drift.\n */\nexport const NATIVE_CHECKOUT_STRIPE_VERSION = '2025-03-31.basil'\n\n/** The flag both halves of native checkout share (AGL-1132, AGL-1944). */\nexport const NATIVE_CHECKOUT_FLAG = 'release_native_checkout' as const\n\nexport interface NativeCheckoutMode {\n /** True only when the flag is on for this org AND a publishable key exists. */\n native: boolean\n /** Empty unless `native` — the client needs it to boot Stripe.js. */\n publishableKey: string\n}\n\n/**\n * Two conditions, not one — the same pairing the console's route settled on.\n *\n * A `ui_mode` session returns a `client_secret` and NO `url`, so a browser that\n * cannot mount Stripe.js has nothing to navigate to. If the flag alone decided\n * this, flipping it in an environment without\n * `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY` would replace a working redirect with a\n * dead Buy button on every storefront at once. Requiring the key here means the\n * worst case of a premature flag flip is the redirect we already ship.\n *\n * That is not hypothetical for the storefront. Measured on 2026-08-18 through\n * the Vercel REST API (`vercel env ls` cannot see team-shared vars):\n * `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY` exists as a project-level var on\n * `aglyn-console` and is set NOWHERE on `aglyn-tenant`, which is the project\n * that serves storefronts. So today this resolves to `native: false` in\n * production however the flag is set, and the shopper keeps the redirect.\n *\n * The org — never the host — is the rollout bucket, per AGL-1656: a hostId and\n * an orgId hash to different buckets, so bucketing on the host could land a\n * merchant's storefront and their console on opposite sides of the same\n * percentage.\n */\nexport async function resolveNativeCheckoutMode(\n orgId: string | null | undefined,\n): Promise<NativeCheckoutMode> {\n const publishableKey = String(\n process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY ?? '',\n ).trim()\n if (!publishableKey) return { native: false, publishableKey: '' }\n // A flag lookup that throws must not take a sale down with it, and the catch\n // is a `try` rather than `.catch()` deliberately: Remote Config being\n // unreachable rejects, but the resolver being absent entirely — an\n // environment where the module never loaded — throws SYNCHRONOUSLY, before\n // there is a promise to attach a handler to. Only the block form covers both.\n // Either way the answer is the same and it is the safe one: the redirect.\n let on: boolean\n try {\n on = await isServerReleaseFlagOnForOrg(NATIVE_CHECKOUT_FLAG, orgId ?? null)\n } catch {\n return { native: false, publishableKey: '' }\n }\n return on ? { native: true, publishableKey } : { native: false, publishableKey: '' }\n}\n\n/**\n * Rewrite a built session param set for the Payment Element, in place.\n *\n * DELETES `success_url` and `cancel_url` rather than leaving them alongside\n * `return_url`: Stripe rejects the pair outright on a `ui_mode` session, and a\n * 400 here would surface to the shopper as \"Checkout failed\" on a store that\n * was working an hour ago. Everything else in the param set — line items, the\n * tax line, `automatic_tax`, shipping options, the Connect destination, the\n * application fee, every `metadata[…]` key the webhook reads — is left exactly\n * as the caller built it. That is the point: this function must be incapable of\n * changing what the shopper is charged.\n *\n * `returnUrl` is the caller's OWN `success_url` string, `{CHECKOUT_SESSION_ID}`\n * placeholder and all. Stripe substitutes it on a `ui_mode: 'custom'` return\n * exactly as it does on a hosted redirect (measured), so the storefront's\n * post-purchase reporting — which reads `session_id` off the query to name the\n * order it just completed (AGL-1641) — keeps working unchanged.\n */\nexport function applyNativeCheckoutParams(\n params: URLSearchParams,\n returnUrl: string,\n): void {\n params.delete('success_url')\n params.delete('cancel_url')\n params.set('ui_mode', 'custom')\n params.set('return_url', returnUrl)\n}\n\n/**\n * The `Stripe-Version` pin, as headers to spread into the session-create fetch.\n * Empty for the hosted path so its request is byte-identical to today's.\n */\nexport function nativeCheckoutStripeHeaders(\n mode: NativeCheckoutMode,\n): Record<string, string> {\n return mode.native\n ? { 'Stripe-Version': NATIVE_CHECKOUT_STRIPE_VERSION }\n : {}\n}\n\nexport interface NativeCheckoutPayload {\n /** Stripe's own instruction to the browser. Not a claim that money moved. */\n clientSecret: string\n publishableKey: string\n /** The order document id, and the `session_id` the return URL carries. */\n sessionId: string\n}\n\nexport interface HostedCheckoutPayload {\n url: string\n}\n\n/**\n * Read the created session into the payload the storefront gets back.\n *\n * Returns `null` when the session is unusable, which BOTH callers already treat\n * as \"nothing was sold\" — release the idempotency claim and answer 502. The\n * null case is the whole reason this is a function rather than a ternary: the\n * hosted path's liveness check was `!session.url`, and a `ui_mode` session\n * never has one. Left unchanged, switching a store to native mode would have\n * made every successful session look like a Stripe failure — a 502 to the\n * shopper, a released claim, and a real Checkout Session left open on the\n * account. So the check has to move with the mode, and it does.\n */\nexport function readCheckoutSessionPayload(\n session: { url?: string; id?: string; client_secret?: string },\n mode: NativeCheckoutMode,\n): NativeCheckoutPayload | HostedCheckoutPayload | null {\n if (!mode.native) return session?.url ? { url: session.url } : null\n if (!session?.client_secret || !session?.id) return null\n return {\n clientSecret: String(session.client_secret),\n publishableKey: mode.publishableKey,\n sessionId: String(session.id),\n }\n}\n"],"names":["isServerReleaseFlagOnForOrg","NATIVE_CHECKOUT_STRIPE_VERSION","NATIVE_CHECKOUT_FLAG","resolveNativeCheckoutMode","orgId","process","publishableKey","String","env","NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY","trim","native","on","applyNativeCheckoutParams","params","returnUrl","delete","set","nativeCheckoutStripeHeaders","mode","readCheckoutSessionPayload","session","url","client_secret","id","clientSecret","sessionId"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,2BAA2B,QAAQ,2BAA0B;AAEtE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkEC,GAED;;;CAGC,GACD,OAAO,MAAMC,iCAAiC,mBAAkB;AAEhE,wEAAwE,GACxE,OAAO,MAAMC,uBAAuB,0BAAkC;AAStE;;;;;;;;;;;;;;;;;;;;;CAqBC,GACD,OAAO,eAAeC,0BACpBC,KAAgC;QAG9BC;IADF,MAAMC,iBAAiBC,QACrBF,kDAAAA,QAAQG,GAAG,CAACC,kCAAkC,YAA9CJ,kDAAkD,IAClDK,IAAI;IACN,IAAI,CAACJ,gBAAgB,OAAO;QAAEK,QAAQ;QAAOL,gBAAgB;IAAG;IAChE,6EAA6E;IAC7E,sEAAsE;IACtE,mEAAmE;IACnE,2EAA2E;IAC3E,8EAA8E;IAC9E,0EAA0E;IAC1E,IAAIM;IACJ,IAAI;QACFA,KAAK,MAAMZ,4BAA4BE,sBAAsBE,gBAAAA,QAAS;IACxE,EAAE,eAAM;QACN,OAAO;YAAEO,QAAQ;YAAOL,gBAAgB;QAAG;IAC7C;IACA,OAAOM,KAAK;QAAED,QAAQ;QAAML;IAAe,IAAI;QAAEK,QAAQ;QAAOL,gBAAgB;IAAG;AACrF;AAEA;;;;;;;;;;;;;;;;;CAiBC,GACD,OAAO,SAASO,0BACdC,MAAuB,EACvBC,SAAiB;IAEjBD,OAAOE,MAAM,CAAC;IACdF,OAAOE,MAAM,CAAC;IACdF,OAAOG,GAAG,CAAC,WAAW;IACtBH,OAAOG,GAAG,CAAC,cAAcF;AAC3B;AAEA;;;CAGC,GACD,OAAO,SAASG,4BACdC,IAAwB;IAExB,OAAOA,KAAKR,MAAM,GACd;QAAE,kBAAkBV;IAA+B,IACnD,CAAC;AACP;AAcA;;;;;;;;;;;CAWC,GACD,OAAO,SAASmB,2BACdC,OAA8D,EAC9DF,IAAwB;IAExB,IAAI,CAACA,KAAKR,MAAM,EAAE,OAAOU,CAAAA,2BAAAA,QAASC,GAAG,IAAG;QAAEA,KAAKD,QAAQC,GAAG;IAAC,IAAI;IAC/D,IAAI,EAACD,2BAAAA,QAASE,aAAa,KAAI,EAACF,2BAAAA,QAASG,EAAE,GAAE,OAAO;IACpD,OAAO;QACLC,cAAclB,OAAOc,QAAQE,aAAa;QAC1CjB,gBAAgBa,KAAKb,cAAc;QACnCoB,WAAWnB,OAAOc,QAAQG,EAAE;IAC9B;AACF"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import type { PluginApiHandler } from '@aglyn/aglyn/server';
18
+ /**
19
+ * Newsletter opt-in (AGL-301): footer signups and checkout opt-ins land
20
+ * in the contacts CRM with an explicit consent timestamp, feeding the
21
+ * email-campaign audiences.
22
+ */
23
+ export declare const newsletterHandler: PluginApiHandler;
@@ -0,0 +1,323 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ import { _ as _extends } from "@swc/helpers/_/_extends";
17
+ import { hostPublicOrigin } from "@aglyn/aglyn/app-utils/host-naming";
18
+ import { mergeEmailTopics, normalizeEmailTopic, topicRequiresDoubleOptIn, EMAIL_TOPICS_COLLECTION } from "@aglyn/aglyn/app-utils/email-topics";
19
+ import { enrollListMember, firebaseAdmin, hostSendingIdentity, meterHostEmail, consentGroupForSite, orgDataCollectionForHost, recordPendingTopicConfirmation, resolveCampaignTouch, resolveOrgIdForHost, siteRequiresDoubleOptIn } from "@aglyn/tenant-data-admin";
20
+ import { captureHostContact } from "@aglyn/tenant-runtime";
21
+ import { buildConfirmUrl } from "@aglyn/tenant-data-admin/server/email-unsubscribe-link";
22
+ import { sendEmail } from "@aglyn/shared-util-email";
23
+ import { isDocumentId } from "@aglyn/tenant-data-admin/server/document-id";
24
+ const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
25
+ /**
26
+ * The stream a newsletter signup joins.
27
+ *
28
+ * The built-in id, not the campaign default: somebody typing their address
29
+ * into a footer box is asking for the newsletter, and attributing it to
30
+ * "Promotions and offers" would confirm them for a stream they did not name.
31
+ */ const NEWSLETTER_TOPIC_ID = 'newsletter';
32
+ /**
33
+ * Enrolls one address into an org list's members (AGL-2499).
34
+ *
35
+ * The document id comes from `enrollListMember`, which is the only writer of
36
+ * that collection — the workflow `enrollList` step reaches it too, and when
37
+ * the two derived their own ids the same person subscribing by both routes
38
+ * became two members of one list.
39
+ *
40
+ * Best-effort and silent on any problem: list enrollment rides along with
41
+ * the newsletter signup, which must still succeed (and still upsert the
42
+ * contact) even when the list id is stale, was mistyped into the besigner
43
+ * prop, or belongs to an org this host cannot resolve.
44
+ */ async function enrollInList(options) {
45
+ if (!isDocumentId(options.listId)) return;
46
+ try {
47
+ var _contactsRef_parent;
48
+ const contactsRef = await orgDataCollectionForHost(options.hostId, 'contacts');
49
+ const listRef = (_contactsRef_parent = contactsRef.parent) == null ? void 0 : _contactsRef_parent.collection('lists').doc(options.listId);
50
+ if (!listRef) return;
51
+ const listSnapshot = await listRef.get();
52
+ // A stale/mistyped id must not silently CREATE a list — campaign-send's
53
+ // `list` audience would then read a list nobody set up.
54
+ if (!listSnapshot.exists) return;
55
+ await enrollListMember(_extends({
56
+ listRef,
57
+ group: await consentGroupForSite(options.hostId),
58
+ email: options.email
59
+ }, options.name ? {
60
+ name: options.name
61
+ } : {}, {
62
+ source: 'newsletter'
63
+ }, options.marketingConsent ? {
64
+ marketingConsent: true
65
+ } : {}));
66
+ } catch (error) {
67
+ console.error('list enrollment failed', error);
68
+ }
69
+ }
70
+ /**
71
+ * DOUBLE OPT-IN, when this site asks for one
72
+ * (`docs/specs/email-competitive-gaps.md` P8).
73
+ *
74
+ * A footer signup is where a confirmation belongs — ActiveCampaign's shape,
75
+ * where forms are the thing that default to it — and it is the only capture
76
+ * path in the product where the person is present, expecting a reply, and has
77
+ * just typed the address themselves.
78
+ *
79
+ * ## The confirmation message is TRANSACTIONAL
80
+ *
81
+ * It carries no marketing context, so it passes no frequency cap and adds no
82
+ * unsubscribe header: somebody who just asked to subscribe is owed the answer
83
+ * to what they asked, and gating it behind a marketing ceiling would drop
84
+ * exactly the message that lets them out of the quarantine. The suppression
85
+ * lists are not consulted here either — `recordPendingTopicConfirmation`
86
+ * refuses an address that left this stream before any message is composed,
87
+ * which is the check that matters: a signup form must not become a way to
88
+ * mail somebody who unsubscribed by asking them again.
89
+ *
90
+ * A hard-bounced address is deliberately NOT refused here, and the asymmetry
91
+ * with the opt-out above is the point. An opt-out is a stated intention; a
92
+ * bounce is a measurement of a mailbox at a moment, and refusing to send at
93
+ * submission time would settle it on evidence that cannot be older than the
94
+ * question being asked. The confirmation goes out, and if it is delivered and
95
+ * clicked, `confirmTopicSubscription` lifts the platform bounce — the round
96
+ * trip is what proves the record stale. A complaint or an unsubscribe is
97
+ * never lifted by that path.
98
+ *
99
+ * ## Best-effort, like the enrollment beside it
100
+ *
101
+ * A failure to send leaves the address pending and unmailable, which is the
102
+ * safe direction: they signed up, nothing reaches them, and signing up again
103
+ * re-sends. The signup itself still succeeds — the contact is captured with
104
+ * its consent record either way, and losing the capture because a confirmation
105
+ * bounced would be a worse trade for everybody.
106
+ *
107
+ * @returns whether the address was put in the quarantine, so the caller can
108
+ * decide whether to enroll them on a list yet.
109
+ */ async function requestConfirmation(options) {
110
+ const topic = options.topics.find((candidate)=>candidate.id === NEWSLETTER_TOPIC_ID);
111
+ const { result } = await recordPendingTopicConfirmation(options.hostId, options.email, NEWSLETTER_TOPIC_ID);
112
+ if (result !== 'pending') return false;
113
+ try {
114
+ var _hostPublicOrigin, _options_host_cname, _options_host_subdomain, _ref;
115
+ const siteBase = (_hostPublicOrigin = hostPublicOrigin({
116
+ cname: String((_options_host_cname = options.host.cname) != null ? _options_host_cname : ''),
117
+ subdomain: String((_options_host_subdomain = options.host.subdomain) != null ? _options_host_subdomain : '')
118
+ })) != null ? _hostPublicOrigin : '';
119
+ const url = buildConfirmUrl({
120
+ siteBase,
121
+ hostId: options.hostId,
122
+ email: options.email,
123
+ topicId: NEWSLETTER_TOPIC_ID
124
+ });
125
+ // No origin or no signing secret means no link, and a confirmation
126
+ // message with nothing to click is worse than none: the person believes
127
+ // they have subscribed and nothing ever arrives. Said out loud rather
128
+ // than shipped quietly, exactly as the unsubscribe seam does.
129
+ if (!url) {
130
+ console.warn('[newsletter] confirmation not sent — no public origin or no ' + 'EMAIL_UNSUBSCRIBE_SECRET');
131
+ return true;
132
+ }
133
+ const stream = (_ref = topic == null ? void 0 : topic.name) != null ? _ref : 'our newsletter';
134
+ const result = await sendEmail({
135
+ to: options.email,
136
+ subject: `Confirm your subscription`,
137
+ text: `Please confirm that you want to receive ${stream} at this ` + `address:\n\n${url}\n\nThe link works for three days. If you did ` + 'not sign up, ignore this message — nothing will be sent.',
138
+ sendingIdentity: await hostSendingIdentity(options.hostId),
139
+ audience: 'tenant',
140
+ context: 'newsletter confirmation'
141
+ });
142
+ /*
143
+ * Counted against the site, as TRANSACTIONAL.
144
+ *
145
+ * It is a message this site sent, so it costs what a message costs and
146
+ * the meter has to see it. `'transactional'` is the honest class: the
147
+ * person just typed their address into a form and is waiting for the
148
+ * answer, which is why the message carries no marketing context and no
149
+ * unsubscribe header either. Only a delivery is counted, so a send that
150
+ * never left is not billed as one.
151
+ */ if (result.sent) await meterHostEmail(options.hostId);
152
+ } catch (error) {
153
+ console.error('confirmation send failed', error);
154
+ }
155
+ return true;
156
+ }
157
+ /**
158
+ * The site's topic catalog, for the one question the signup asks of it.
159
+ *
160
+ * Fails soft to the built-ins, like the preference page's read: a site whose
161
+ * org cannot be resolved still gets the four defaults, so the decision is
162
+ * made against a catalog rather than against nothing.
163
+ */ async function loadTopics(hostId) {
164
+ try {
165
+ var _ref;
166
+ const orgId = await resolveOrgIdForHost(hostId);
167
+ if (!orgId) return mergeEmailTopics(null);
168
+ const snapshot = await firebaseAdmin.app().firestore().collection('orgs').doc(orgId).collection(EMAIL_TOPICS_COLLECTION).get();
169
+ return mergeEmailTopics(((_ref = snapshot == null ? void 0 : snapshot.docs) != null ? _ref : []).map((doc)=>normalizeEmailTopic(doc.id, doc.data())).filter((topic)=>!!topic));
170
+ } catch (error) {
171
+ console.error('[newsletter] topic catalog read failed', error);
172
+ return mergeEmailTopics(null);
173
+ }
174
+ }
175
+ /**
176
+ * Everything the confirmation decision needs, in one round trip.
177
+ *
178
+ * Three reads — the host document, the org lookup and the topic catalog — and
179
+ * the first is issued IN PARALLEL with the other two rather than after them,
180
+ * because none of the three depends on another's answer. A signup is a person
181
+ * waiting for a form to come back, and serialized reads on that path are
182
+ * latency the person feels for a decision that is usually "no".
183
+ *
184
+ * The host document is returned rather than discarded: the confirmation link
185
+ * needs the site's public origin, which is on that same document, and reading
186
+ * it twice would be a read per signup to recover something already in hand.
187
+ *
188
+ * Fails soft on the site flag, which `siteRequiresDoubleOptIn` already does —
189
+ * a default a failed read could switch ON would quarantine every new signup
190
+ * on a site whose owner never asked for confirmations.
191
+ */ async function loadHostGate(hostId) {
192
+ const [topics, siteDefault, snapshot] = await Promise.all([
193
+ loadTopics(hostId),
194
+ siteRequiresDoubleOptIn(hostId),
195
+ firebaseAdmin.app().firestore().collection('hosts').doc(hostId).get().catch(()=>null)
196
+ ]);
197
+ return {
198
+ topics,
199
+ siteDefault,
200
+ host: {
201
+ cname: snapshot == null ? void 0 : snapshot.get('cname'),
202
+ subdomain: snapshot == null ? void 0 : snapshot.get('subdomain')
203
+ }
204
+ };
205
+ }
206
+ // Best-effort per-instance flood damper.
207
+ const attemptsByIp = new Map();
208
+ import { NO_CLIENT_ADDRESS_BUCKET, readClientIp } from "@aglyn/aglyn/app-utils/request-ip";
209
+ /**
210
+ * Newsletter opt-in (AGL-301): footer signups and checkout opt-ins land
211
+ * in the contacts CRM with an explicit consent timestamp, feeding the
212
+ * email-campaign audiences.
213
+ */ export const newsletterHandler = async (req, res)=>{
214
+ var _req_body, _body_hostId, _body_email, _body_listId, _readClientIp, _attemptsByIp_get;
215
+ var _req_socket;
216
+ if (req.method !== 'POST') {
217
+ return res.status(405).json({
218
+ error: 'Method not allowed'
219
+ });
220
+ }
221
+ const body = typeof req.body === 'string' ? JSON.parse(req.body) : (_req_body = req.body) != null ? _req_body : {};
222
+ const hostId = String((_body_hostId = body.hostId) != null ? _body_hostId : '');
223
+ const email = String((_body_email = body.email) != null ? _body_email : '').trim().toLowerCase();
224
+ const listId = String((_body_listId = body.listId) != null ? _body_listId : '').trim();
225
+ if (!hostId || !EMAIL_PATTERN.test(email)) {
226
+ return res.status(400).json({
227
+ error: 'Enter a valid email'
228
+ });
229
+ }
230
+ // Keeps counting under the no-address bucket rather than being skipped: an
231
+ // unauthenticated signup endpoint that stops counting is an open write into
232
+ // the contacts CRM.
233
+ const ip = (_readClientIp = readClientIp(req.headers, {
234
+ remoteAddress: (_req_socket = req.socket) == null ? void 0 : _req_socket.remoteAddress
235
+ })) != null ? _readClientIp : NO_CLIENT_ADDRESS_BUCKET;
236
+ const now = Date.now();
237
+ const attempts = ((_attemptsByIp_get = attemptsByIp.get(ip)) != null ? _attemptsByIp_get : []).filter((at)=>now - at < 60000);
238
+ attempts.push(now);
239
+ attemptsByIp.set(ip, attempts);
240
+ if (attempts.length > 10) {
241
+ return res.status(429).json({
242
+ error: 'Too many attempts'
243
+ });
244
+ }
245
+ try {
246
+ // A newsletter signup is an identify moment: the visitor was anonymous
247
+ // while they browsed and this request is the first thing that names them.
248
+ // Both channels are asked once, here, and the later touch is credited.
249
+ const campaignTouch = await resolveCampaignTouch({
250
+ hostId,
251
+ wire: body.campaignTouch,
252
+ email,
253
+ atMs: now
254
+ });
255
+ await captureHostContact(_extends({
256
+ hostId,
257
+ email,
258
+ source: 'newsletter',
259
+ // The stage the name is for (AGL-2612): somebody who asked to hear
260
+ // from the site and nothing more yet.
261
+ initialLifecycleStage: 'subscriber',
262
+ marketingConsent: true,
263
+ interaction: {
264
+ refId: `newsletter-${now}`,
265
+ summary: 'Subscribed to the newsletter'
266
+ }
267
+ }, campaignTouch ? {
268
+ campaignTouch
269
+ } : {}));
270
+ /*
271
+ * The confirmation, when this site asks for one.
272
+ *
273
+ * AFTER the contact upsert and never instead of it. The person typed
274
+ * their address and ticked a box, and that consent record is a fact
275
+ * whether or not they go on to click a link — withholding the capture
276
+ * until they do would lose the record of what they actually did. What the
277
+ * confirmation gates is the SEND, on the person's own topic entry, which
278
+ * is where `filterTopicSendable` reads it.
279
+ */ const { topics, siteDefault, host } = await loadHostGate(hostId);
280
+ const confirming = topicRequiresDoubleOptIn(topics.find((topic)=>topic.id === NEWSLETTER_TOPIC_ID), siteDefault) && await requestConfirmation({
281
+ hostId,
282
+ email,
283
+ topics,
284
+ host
285
+ });
286
+ if (listId) {
287
+ /*
288
+ * The same basis the contact upsert records, on the membership too.
289
+ *
290
+ * A list membership had no consent field at all, so `audience: 'list'`
291
+ * gave the send-time join nothing to read even for the one audience
292
+ * whose members literally asked for a newsletter
293
+ * (`docs/specs/email-overhaul.md` §1d). This is a DECLARED opt-in and
294
+ * not an inference from an act: the request this handler serves is
295
+ * "subscribe me", which is the checkbox.
296
+ */ await enrollInList({
297
+ hostId,
298
+ listId,
299
+ email,
300
+ marketingConsent: true
301
+ });
302
+ }
303
+ /*
304
+ * The caller is told which of the two things happened, so the signup form
305
+ * can say "check your email" rather than "you're subscribed".
306
+ *
307
+ * A form that reported success identically either way would be the site
308
+ * telling somebody they are on the list while the send path refuses them,
309
+ * and the only way they could find out is by noticing that nothing
310
+ * arrives.
311
+ */ return res.status(200).json({
312
+ ok: true,
313
+ confirmationRequired: confirming
314
+ });
315
+ } catch (error) {
316
+ console.error(error);
317
+ return res.status(500).json({
318
+ error: 'Signup failed'
319
+ });
320
+ }
321
+ };
322
+
323
+ //# sourceMappingURL=newsletter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/newsletter.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { PluginApiHandler } from '@aglyn/aglyn/server'\nimport { hostPublicOrigin } from '@aglyn/aglyn/app-utils/host-naming'\nimport {\n mergeEmailTopics,\n normalizeEmailTopic,\n topicRequiresDoubleOptIn,\n EMAIL_TOPICS_COLLECTION,\n type EmailTopic,\n} from '@aglyn/aglyn/app-utils/email-topics'\nimport {\n enrollListMember,\n firebaseAdmin,\n hostSendingIdentity,\n meterHostEmail,\n consentGroupForSite,\n orgDataCollectionForHost,\n recordPendingTopicConfirmation,\n resolveCampaignTouch,\n resolveOrgIdForHost,\n siteRequiresDoubleOptIn,\n} from '@aglyn/tenant-data-admin'\nimport { captureHostContact } from '@aglyn/tenant-runtime'\nimport { buildConfirmUrl } from '@aglyn/tenant-data-admin/server/email-unsubscribe-link'\nimport { sendEmail } from '@aglyn/shared-util-email'\nimport { isDocumentId } from '@aglyn/tenant-data-admin/server/document-id'\n\nconst EMAIL_PATTERN = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n\n/**\n * The stream a newsletter signup joins.\n *\n * The built-in id, not the campaign default: somebody typing their address\n * into a footer box is asking for the newsletter, and attributing it to\n * \"Promotions and offers\" would confirm them for a stream they did not name.\n */\nconst NEWSLETTER_TOPIC_ID = 'newsletter'\n\n/**\n * Enrolls one address into an org list's members (AGL-2499).\n *\n * The document id comes from `enrollListMember`, which is the only writer of\n * that collection — the workflow `enrollList` step reaches it too, and when\n * the two derived their own ids the same person subscribing by both routes\n * became two members of one list.\n *\n * Best-effort and silent on any problem: list enrollment rides along with\n * the newsletter signup, which must still succeed (and still upsert the\n * contact) even when the list id is stale, was mistyped into the besigner\n * prop, or belongs to an org this host cannot resolve.\n */\nasync function enrollInList(options: {\n hostId: string\n listId: string\n email: string\n name?: string\n /** The opt-in the signup itself carries — see the call site. */\n marketingConsent?: boolean\n}): Promise<void> {\n if (!isDocumentId(options.listId)) return\n try {\n const contactsRef = await orgDataCollectionForHost(options.hostId, 'contacts')\n const listRef = contactsRef.parent?.collection('lists').doc(options.listId)\n if (!listRef) return\n const listSnapshot = await listRef.get()\n // A stale/mistyped id must not silently CREATE a list — campaign-send's\n // `list` audience would then read a list nobody set up.\n if (!listSnapshot.exists) return\n await enrollListMember({\n listRef,\n group: await consentGroupForSite(options.hostId),\n email: options.email,\n ...(options.name ? { name: options.name } : {}),\n source: 'newsletter',\n ...(options.marketingConsent ? { marketingConsent: true } : {}),\n })\n } catch (error) {\n console.error('list enrollment failed', error)\n }\n}\n\n/**\n * DOUBLE OPT-IN, when this site asks for one\n * (`docs/specs/email-competitive-gaps.md` P8).\n *\n * A footer signup is where a confirmation belongs — ActiveCampaign's shape,\n * where forms are the thing that default to it — and it is the only capture\n * path in the product where the person is present, expecting a reply, and has\n * just typed the address themselves.\n *\n * ## The confirmation message is TRANSACTIONAL\n *\n * It carries no marketing context, so it passes no frequency cap and adds no\n * unsubscribe header: somebody who just asked to subscribe is owed the answer\n * to what they asked, and gating it behind a marketing ceiling would drop\n * exactly the message that lets them out of the quarantine. The suppression\n * lists are not consulted here either — `recordPendingTopicConfirmation`\n * refuses an address that left this stream before any message is composed,\n * which is the check that matters: a signup form must not become a way to\n * mail somebody who unsubscribed by asking them again.\n *\n * A hard-bounced address is deliberately NOT refused here, and the asymmetry\n * with the opt-out above is the point. An opt-out is a stated intention; a\n * bounce is a measurement of a mailbox at a moment, and refusing to send at\n * submission time would settle it on evidence that cannot be older than the\n * question being asked. The confirmation goes out, and if it is delivered and\n * clicked, `confirmTopicSubscription` lifts the platform bounce — the round\n * trip is what proves the record stale. A complaint or an unsubscribe is\n * never lifted by that path.\n *\n * ## Best-effort, like the enrollment beside it\n *\n * A failure to send leaves the address pending and unmailable, which is the\n * safe direction: they signed up, nothing reaches them, and signing up again\n * re-sends. The signup itself still succeeds — the contact is captured with\n * its consent record either way, and losing the capture because a confirmation\n * bounced would be a worse trade for everybody.\n *\n * @returns whether the address was put in the quarantine, so the caller can\n * decide whether to enroll them on a list yet.\n */\nasync function requestConfirmation(options: {\n hostId: string\n email: string\n topics: EmailTopic[]\n /** The host document the setting was read from — see {@link loadHostGate}. */\n host: { cname?: unknown; subdomain?: unknown }\n}): Promise<boolean> {\n const topic = options.topics.find(\n (candidate) => candidate.id === NEWSLETTER_TOPIC_ID,\n )\n const { result } = await recordPendingTopicConfirmation(\n options.hostId,\n options.email,\n NEWSLETTER_TOPIC_ID,\n )\n if (result !== 'pending') return false\n try {\n const siteBase =\n hostPublicOrigin({\n cname: String(options.host.cname ?? ''),\n subdomain: String(options.host.subdomain ?? ''),\n }) ?? ''\n const url = buildConfirmUrl({\n siteBase,\n hostId: options.hostId,\n email: options.email,\n topicId: NEWSLETTER_TOPIC_ID,\n })\n // No origin or no signing secret means no link, and a confirmation\n // message with nothing to click is worse than none: the person believes\n // they have subscribed and nothing ever arrives. Said out loud rather\n // than shipped quietly, exactly as the unsubscribe seam does.\n if (!url) {\n console.warn(\n '[newsletter] confirmation not sent — no public origin or no ' +\n 'EMAIL_UNSUBSCRIBE_SECRET',\n )\n return true\n }\n const stream = topic?.name ?? 'our newsletter'\n const result = await sendEmail({\n to: options.email,\n subject: `Confirm your subscription`,\n text:\n `Please confirm that you want to receive ${stream} at this ` +\n `address:\\n\\n${url}\\n\\nThe link works for three days. If you did ` +\n 'not sign up, ignore this message — nothing will be sent.',\n sendingIdentity: await hostSendingIdentity(options.hostId),\n audience: 'tenant',\n context: 'newsletter confirmation',\n })\n /*\n * Counted against the site, as TRANSACTIONAL.\n *\n * It is a message this site sent, so it costs what a message costs and\n * the meter has to see it. `'transactional'` is the honest class: the\n * person just typed their address into a form and is waiting for the\n * answer, which is why the message carries no marketing context and no\n * unsubscribe header either. Only a delivery is counted, so a send that\n * never left is not billed as one.\n */\n if (result.sent) await meterHostEmail(options.hostId)\n } catch (error) {\n console.error('confirmation send failed', error)\n }\n return true\n}\n\n/**\n * The site's topic catalog, for the one question the signup asks of it.\n *\n * Fails soft to the built-ins, like the preference page's read: a site whose\n * org cannot be resolved still gets the four defaults, so the decision is\n * made against a catalog rather than against nothing.\n */\nasync function loadTopics(hostId: string): Promise<EmailTopic[]> {\n try {\n const orgId = await resolveOrgIdForHost(hostId)\n if (!orgId) return mergeEmailTopics(null)\n const snapshot = await firebaseAdmin\n .app()\n .firestore()\n .collection('orgs')\n .doc(orgId)\n .collection(EMAIL_TOPICS_COLLECTION)\n .get()\n return mergeEmailTopics(\n (snapshot?.docs ?? [])\n .map((doc: any) => normalizeEmailTopic(doc.id, doc.data()))\n .filter((topic: EmailTopic | null): topic is EmailTopic => !!topic),\n )\n } catch (error) {\n console.error('[newsletter] topic catalog read failed', error)\n return mergeEmailTopics(null)\n }\n}\n\n/**\n * Everything the confirmation decision needs, in one round trip.\n *\n * Three reads — the host document, the org lookup and the topic catalog — and\n * the first is issued IN PARALLEL with the other two rather than after them,\n * because none of the three depends on another's answer. A signup is a person\n * waiting for a form to come back, and serialized reads on that path are\n * latency the person feels for a decision that is usually \"no\".\n *\n * The host document is returned rather than discarded: the confirmation link\n * needs the site's public origin, which is on that same document, and reading\n * it twice would be a read per signup to recover something already in hand.\n *\n * Fails soft on the site flag, which `siteRequiresDoubleOptIn` already does —\n * a default a failed read could switch ON would quarantine every new signup\n * on a site whose owner never asked for confirmations.\n */\nasync function loadHostGate(hostId: string): Promise<{\n topics: EmailTopic[]\n siteDefault: boolean\n host: { cname?: unknown; subdomain?: unknown }\n}> {\n const [topics, siteDefault, snapshot] = await Promise.all([\n loadTopics(hostId),\n siteRequiresDoubleOptIn(hostId),\n firebaseAdmin\n .app()\n .firestore()\n .collection('hosts')\n .doc(hostId)\n .get()\n .catch(() => null),\n ])\n return {\n topics,\n siteDefault,\n host: {\n cname: snapshot?.get('cname'),\n subdomain: snapshot?.get('subdomain'),\n },\n }\n}\n\n// Best-effort per-instance flood damper.\nconst attemptsByIp = new Map<string, number[]>()\nimport {\n NO_CLIENT_ADDRESS_BUCKET,\n readClientIp,\n} from '@aglyn/aglyn/app-utils/request-ip'\n\n/**\n * Newsletter opt-in (AGL-301): footer signups and checkout opt-ins land\n * in the contacts CRM with an explicit consent timestamp, feeding the\n * email-campaign audiences.\n */\nexport const newsletterHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const body =\n typeof req.body === 'string' ? JSON.parse(req.body) : (req.body ?? {})\n const hostId = String(body.hostId ?? '')\n const email = String(body.email ?? '')\n .trim()\n .toLowerCase()\n const listId = String(body.listId ?? '').trim()\n if (!hostId || !EMAIL_PATTERN.test(email)) {\n return res.status(400).json({ error: 'Enter a valid email' })\n }\n // Keeps counting under the no-address bucket rather than being skipped: an\n // unauthenticated signup endpoint that stops counting is an open write into\n // the contacts CRM.\n const ip =\n readClientIp(req.headers, { remoteAddress: req.socket?.remoteAddress }) ??\n NO_CLIENT_ADDRESS_BUCKET\n const now = Date.now()\n const attempts = (attemptsByIp.get(ip) ?? []).filter(\n (at) => now - at < 60_000,\n )\n attempts.push(now)\n attemptsByIp.set(ip, attempts)\n if (attempts.length > 10) {\n return res.status(429).json({ error: 'Too many attempts' })\n }\n try {\n // A newsletter signup is an identify moment: the visitor was anonymous\n // while they browsed and this request is the first thing that names them.\n // Both channels are asked once, here, and the later touch is credited.\n const campaignTouch = await resolveCampaignTouch({\n hostId,\n wire: body.campaignTouch,\n email,\n atMs: now,\n })\n await captureHostContact({\n hostId,\n email,\n source: 'newsletter',\n // The stage the name is for (AGL-2612): somebody who asked to hear\n // from the site and nothing more yet.\n initialLifecycleStage: 'subscriber',\n marketingConsent: true,\n interaction: {\n refId: `newsletter-${now}`,\n summary: 'Subscribed to the newsletter',\n },\n ...(campaignTouch ? { campaignTouch } : {}),\n })\n /*\n * The confirmation, when this site asks for one.\n *\n * AFTER the contact upsert and never instead of it. The person typed\n * their address and ticked a box, and that consent record is a fact\n * whether or not they go on to click a link — withholding the capture\n * until they do would lose the record of what they actually did. What the\n * confirmation gates is the SEND, on the person's own topic entry, which\n * is where `filterTopicSendable` reads it.\n */\n const { topics, siteDefault, host } = await loadHostGate(hostId)\n const confirming =\n topicRequiresDoubleOptIn(\n topics.find((topic) => topic.id === NEWSLETTER_TOPIC_ID),\n siteDefault,\n ) && (await requestConfirmation({ hostId, email, topics, host }))\n if (listId) {\n /*\n * The same basis the contact upsert records, on the membership too.\n *\n * A list membership had no consent field at all, so `audience: 'list'`\n * gave the send-time join nothing to read even for the one audience\n * whose members literally asked for a newsletter\n * (`docs/specs/email-overhaul.md` §1d). This is a DECLARED opt-in and\n * not an inference from an act: the request this handler serves is\n * \"subscribe me\", which is the checkbox.\n */\n await enrollInList({ hostId, listId, email, marketingConsent: true })\n }\n /*\n * The caller is told which of the two things happened, so the signup form\n * can say \"check your email\" rather than \"you're subscribed\".\n *\n * A form that reported success identically either way would be the site\n * telling somebody they are on the list while the send path refuses them,\n * and the only way they could find out is by noticing that nothing\n * arrives.\n */\n return res.status(200).json({ ok: true, confirmationRequired: confirming })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Signup failed' })\n }\n}\n"],"names":["hostPublicOrigin","mergeEmailTopics","normalizeEmailTopic","topicRequiresDoubleOptIn","EMAIL_TOPICS_COLLECTION","enrollListMember","firebaseAdmin","hostSendingIdentity","meterHostEmail","consentGroupForSite","orgDataCollectionForHost","recordPendingTopicConfirmation","resolveCampaignTouch","resolveOrgIdForHost","siteRequiresDoubleOptIn","captureHostContact","buildConfirmUrl","sendEmail","isDocumentId","EMAIL_PATTERN","NEWSLETTER_TOPIC_ID","enrollInList","options","listId","contactsRef","hostId","listRef","parent","collection","doc","listSnapshot","get","exists","group","email","name","source","marketingConsent","error","console","requestConfirmation","topic","topics","find","candidate","id","result","siteBase","cname","String","host","subdomain","url","topicId","warn","stream","to","subject","text","sendingIdentity","audience","context","sent","loadTopics","orgId","snapshot","app","firestore","docs","map","data","filter","loadHostGate","siteDefault","Promise","all","catch","attemptsByIp","Map","NO_CLIENT_ADDRESS_BUCKET","readClientIp","newsletterHandler","req","res","body","method","status","json","JSON","parse","trim","toLowerCase","test","ip","headers","remoteAddress","socket","now","Date","attempts","at","push","set","length","campaignTouch","wire","atMs","initialLifecycleStage","interaction","refId","summary","confirming","ok","confirmationRequired"],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import type { PluginApiHandler } from '@aglyn/aglyn/server';
18
+ /**
19
+ * Back-in-stock signup (AGL-326): captures an email for a sold-out
20
+ * product; the restock cron emails and clears it once stock returns.
21
+ */
22
+ export declare const notifyRestockHandler: PluginApiHandler;
@@ -0,0 +1,84 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ import { firebaseAdmin } from "@aglyn/tenant-data-admin";
17
+ import { isDocumentId } from "@aglyn/tenant-data-admin/server/document-id";
18
+ const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
19
+ /**
20
+ * Back-in-stock signup (AGL-326): captures an email for a sold-out
21
+ * product; the restock cron emails and clears it once stock returns.
22
+ */ export const notifyRestockHandler = async (req, res)=>{
23
+ var _req_body, _body_hostId, _body_productId, _body_email;
24
+ if (req.method !== 'POST') {
25
+ return res.status(405).json({
26
+ error: 'Method not allowed'
27
+ });
28
+ }
29
+ const body = typeof req.body === 'string' ? JSON.parse(req.body) : (_req_body = req.body) != null ? _req_body : {};
30
+ const hostId = String((_body_hostId = body.hostId) != null ? _body_hostId : '');
31
+ const productId = String((_body_productId = body.productId) != null ? _body_productId : '');
32
+ const email = String((_body_email = body.email) != null ? _body_email : '').trim().toLowerCase();
33
+ // AGL-1771/AGL-1774: both were checked non-empty and nothing else, on a
34
+ // handler that needs NO credentials — no session, no member gate, no
35
+ // signature — and they are not equally dangerous.
36
+ //
37
+ // `hostId` is a path component here: `.doc()` appends a SLASH-SEPARATED path
38
+ // rather than taking one opaque id, so `a/b/c` created the alert under
39
+ // `hosts/a/b/c/restockAlerts/…`, beneath a document that does not exist and
40
+ // therefore invisible to every console list, which resolve the host first.
41
+ //
42
+ // `productId` is worse precisely because it is NOT a path component here —
43
+ // it is stored as a FIELD, and `process-restock.ts` makes it a path
44
+ // component later. An even-component value threw synchronously out of that
45
+ // cron's `.doc()`, aborting the whole platform-wide run; and because the
46
+ // alert was never marked `notifiedAtMs` it came back on every later run, so
47
+ // one anonymous POST stopped back-in-stock email for every merchant,
48
+ // permanently. A field is not a path until someone makes it one, and the
49
+ // place to refuse is where it enters (AGL-1774 guards the cron as well, for
50
+ // the rows written before this).
51
+ //
52
+ // Answered separately from the email, following `762621581`: a caller told
53
+ // only "Enter a valid email" when the ids are the problem goes looking for
54
+ // the wrong mistake. A shopper never sees this — the storefront widget fills
55
+ // both ids — so the message is for whoever is driving the endpoint.
56
+ if (!isDocumentId(hostId) || !isDocumentId(productId)) {
57
+ return res.status(400).json({
58
+ error: 'Missing or invalid hostId or productId'
59
+ });
60
+ }
61
+ if (!EMAIL_PATTERN.test(email)) {
62
+ return res.status(400).json({
63
+ error: 'Enter a valid email'
64
+ });
65
+ }
66
+ try {
67
+ await firebaseAdmin.app().firestore().collection('hosts').doc(hostId).collection('restockAlerts').add({
68
+ productId,
69
+ email,
70
+ notifiedAtMs: null,
71
+ createdAtMs: Date.now()
72
+ });
73
+ return res.status(200).json({
74
+ ok: true
75
+ });
76
+ } catch (error) {
77
+ console.error(error);
78
+ return res.status(500).json({
79
+ error: 'Signup failed'
80
+ });
81
+ }
82
+ };
83
+
84
+ //# sourceMappingURL=notify-restock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/notify-restock.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { PluginApiHandler } from '@aglyn/aglyn/server'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport { isDocumentId } from '@aglyn/tenant-data-admin/server/document-id'\n\nconst EMAIL_PATTERN = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n\n/**\n * Back-in-stock signup (AGL-326): captures an email for a sold-out\n * product; the restock cron emails and clears it once stock returns.\n */\nexport const notifyRestockHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const body =\n typeof req.body === 'string' ? JSON.parse(req.body) : (req.body ?? {})\n const hostId = String(body.hostId ?? '')\n const productId = String(body.productId ?? '')\n const email = String(body.email ?? '').trim().toLowerCase()\n // AGL-1771/AGL-1774: both were checked non-empty and nothing else, on a\n // handler that needs NO credentials — no session, no member gate, no\n // signature — and they are not equally dangerous.\n //\n // `hostId` is a path component here: `.doc()` appends a SLASH-SEPARATED path\n // rather than taking one opaque id, so `a/b/c` created the alert under\n // `hosts/a/b/c/restockAlerts/…`, beneath a document that does not exist and\n // therefore invisible to every console list, which resolve the host first.\n //\n // `productId` is worse precisely because it is NOT a path component here —\n // it is stored as a FIELD, and `process-restock.ts` makes it a path\n // component later. An even-component value threw synchronously out of that\n // cron's `.doc()`, aborting the whole platform-wide run; and because the\n // alert was never marked `notifiedAtMs` it came back on every later run, so\n // one anonymous POST stopped back-in-stock email for every merchant,\n // permanently. A field is not a path until someone makes it one, and the\n // place to refuse is where it enters (AGL-1774 guards the cron as well, for\n // the rows written before this).\n //\n // Answered separately from the email, following `762621581`: a caller told\n // only \"Enter a valid email\" when the ids are the problem goes looking for\n // the wrong mistake. A shopper never sees this — the storefront widget fills\n // both ids — so the message is for whoever is driving the endpoint.\n if (!isDocumentId(hostId) || !isDocumentId(productId)) {\n return res\n .status(400)\n .json({ error: 'Missing or invalid hostId or productId' })\n }\n if (!EMAIL_PATTERN.test(email)) {\n return res.status(400).json({ error: 'Enter a valid email' })\n }\n try {\n await firebaseAdmin\n .app()\n .firestore()\n .collection('hosts')\n .doc(hostId)\n .collection('restockAlerts')\n .add({ productId, email, notifiedAtMs: null, createdAtMs: Date.now() })\n return res.status(200).json({ ok: true })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Signup failed' })\n }\n}\n"],"names":["firebaseAdmin","isDocumentId","EMAIL_PATTERN","notifyRestockHandler","req","res","body","method","status","json","error","JSON","parse","hostId","String","productId","email","trim","toLowerCase","test","app","firestore","collection","doc","add","notifiedAtMs","createdAtMs","Date","now","ok","console"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAGD,SAASA,aAAa,QAAQ,2BAA0B;AACxD,SAASC,YAAY,QAAQ,8CAA6C;AAE1E,MAAMC,gBAAgB;AAEtB;;;CAGC,GACD,OAAO,MAAMC,uBAAyC,OAAOC,KAAKC;QAKPD,WACnCE,cACGA,iBACJA;IAPrB,IAAIF,IAAIG,MAAM,KAAK,QAAQ;QACzB,OAAOF,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAAqB;IAC5D;IACA,MAAMJ,OACJ,OAAOF,IAAIE,IAAI,KAAK,WAAWK,KAAKC,KAAK,CAACR,IAAIE,IAAI,KAAKF,YAAAA,IAAIE,IAAI,YAARF,YAAY,CAAC;IACtE,MAAMS,SAASC,QAAOR,eAAAA,KAAKO,MAAM,YAAXP,eAAe;IACrC,MAAMS,YAAYD,QAAOR,kBAAAA,KAAKS,SAAS,YAAdT,kBAAkB;IAC3C,MAAMU,QAAQF,QAAOR,cAAAA,KAAKU,KAAK,YAAVV,cAAc,IAAIW,IAAI,GAAGC,WAAW;IACzD,wEAAwE;IACxE,qEAAqE;IACrE,kDAAkD;IAClD,EAAE;IACF,6EAA6E;IAC7E,uEAAuE;IACvE,4EAA4E;IAC5E,2EAA2E;IAC3E,EAAE;IACF,2EAA2E;IAC3E,oEAAoE;IACpE,2EAA2E;IAC3E,yEAAyE;IACzE,4EAA4E;IAC5E,qEAAqE;IACrE,yEAAyE;IACzE,4EAA4E;IAC5E,iCAAiC;IACjC,EAAE;IACF,2EAA2E;IAC3E,2EAA2E;IAC3E,6EAA6E;IAC7E,oEAAoE;IACpE,IAAI,CAACjB,aAAaY,WAAW,CAACZ,aAAac,YAAY;QACrD,OAAOV,IACJG,MAAM,CAAC,KACPC,IAAI,CAAC;YAAEC,OAAO;QAAyC;IAC5D;IACA,IAAI,CAACR,cAAciB,IAAI,CAACH,QAAQ;QAC9B,OAAOX,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAAsB;IAC7D;IACA,IAAI;QACF,MAAMV,cACHoB,GAAG,GACHC,SAAS,GACTC,UAAU,CAAC,SACXC,GAAG,CAACV,QACJS,UAAU,CAAC,iBACXE,GAAG,CAAC;YAAET;YAAWC;YAAOS,cAAc;YAAMC,aAAaC,KAAKC,GAAG;QAAG;QACvE,OAAOvB,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEoB,IAAI;QAAK;IACzC,EAAE,OAAOnB,OAAO;QACdoB,QAAQpB,KAAK,CAACA;QACd,OAAOL,IAAIG,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAAgB;IACvD;AACF,EAAC"}