@aglyn/plugins-commerce 1.0.0-beta.284 → 1.0.0-beta.287

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 (420) hide show
  1. package/README.md +1 -1
  2. package/package.json +13 -13
  3. package/src/lib/components/cart.js +25 -12
  4. package/src/lib/components/cart.js.map +1 -1
  5. package/src/lib/components/commerce-console-page.js +70 -13
  6. package/src/lib/components/commerce-console-page.js.map +1 -1
  7. package/src/lib/components/console/order-detail-dialog.component.js +2 -1
  8. package/src/lib/components/console/order-detail-dialog.component.js.map +1 -1
  9. package/src/lib/components/console/payment-methods-card.component.js +1 -1
  10. package/src/lib/components/console/payment-methods-card.component.js.map +1 -1
  11. package/src/lib/components/console/pos/pos-cart-panel.component.d.ts +5 -0
  12. package/src/lib/components/console/pos/pos-cart-panel.component.js +18 -12
  13. package/src/lib/components/console/pos/pos-cart-panel.component.js.map +1 -1
  14. package/src/lib/components/console/pos/pos-hardware-api.d.ts +76 -5
  15. package/src/lib/components/console/pos/pos-hardware-api.js +53 -11
  16. package/src/lib/components/console/pos/pos-hardware-api.js.map +1 -1
  17. package/src/lib/components/console/pos/pos-hardware-buyer.component.d.ts +18 -0
  18. package/src/lib/components/console/pos/pos-hardware-buyer.component.js +629 -0
  19. package/src/lib/components/console/pos/pos-hardware-buyer.component.js.map +1 -0
  20. package/src/lib/components/console/pos/pos-hardware-sale-api.d.ts +162 -0
  21. package/src/lib/components/console/pos/pos-hardware-sale-api.js +122 -0
  22. package/src/lib/components/console/pos/pos-hardware-sale-api.js.map +1 -0
  23. package/src/lib/components/console/pos/pos-hardware-sale-terms.component.d.ts +39 -0
  24. package/src/lib/components/console/pos/pos-hardware-sale-terms.component.js +251 -0
  25. package/src/lib/components/console/pos/pos-hardware-sale-terms.component.js.map +1 -0
  26. package/src/lib/components/console/pos/use-pos-display.js +50 -0
  27. package/src/lib/components/console/pos/use-pos-display.js.map +1 -1
  28. package/src/lib/components/console/pos-cellular-control.component.d.ts +41 -0
  29. package/src/lib/components/console/pos-cellular-control.component.js +211 -0
  30. package/src/lib/components/console/pos-cellular-control.component.js.map +1 -0
  31. package/src/lib/components/console/pos-devices-card.component.d.ts +9 -1
  32. package/src/lib/components/console/pos-devices-card.component.js +47 -15
  33. package/src/lib/components/console/pos-devices-card.component.js.map +1 -1
  34. package/src/lib/components/console/pos-display/pos-display-api.d.ts +6 -0
  35. package/src/lib/components/console/pos-display/pos-display-api.js +8 -1
  36. package/src/lib/components/console/pos-display/pos-display-api.js.map +1 -1
  37. package/src/lib/components/console/pos-display-page.component.d.ts +13 -0
  38. package/src/lib/components/console/pos-display-page.component.js +44 -2
  39. package/src/lib/components/console/pos-display-page.component.js.map +1 -1
  40. package/src/lib/components/console/pos-hardware-card.component.d.ts +12 -4
  41. package/src/lib/components/console/pos-hardware-card.component.js +157 -36
  42. package/src/lib/components/console/pos-hardware-card.component.js.map +1 -1
  43. package/src/lib/components/console/pos-hardware-orders-staff-page.component.js +360 -80
  44. package/src/lib/components/console/pos-hardware-orders-staff-page.component.js.map +1 -1
  45. package/src/lib/components/console/pos-hardware-terms-page.component.d.ts +34 -0
  46. package/src/lib/components/console/pos-hardware-terms-page.component.js +257 -0
  47. package/src/lib/components/console/pos-hardware-terms-page.component.js.map +1 -0
  48. package/src/lib/components/console/pos-hardware-terms-sign-page.component.d.ts +27 -0
  49. package/src/lib/components/console/pos-hardware-terms-sign-page.component.js +104 -0
  50. package/src/lib/components/console/pos-hardware-terms-sign-page.component.js.map +1 -0
  51. package/src/lib/components/console/pos-hardware-waiting-card.component.d.ts +24 -0
  52. package/src/lib/components/console/pos-hardware-waiting-card.component.js +205 -0
  53. package/src/lib/components/console/pos-hardware-waiting-card.component.js.map +1 -0
  54. package/src/lib/components/console/pos-ops/pos-check-in-dialog.component.js +297 -201
  55. package/src/lib/components/console/pos-ops/pos-check-in-dialog.component.js.map +1 -1
  56. package/src/lib/components/console/pos-ops/pos-check-ins-card.component.d.ts +3 -0
  57. package/src/lib/components/console/pos-ops/pos-check-ins-card.component.js +260 -92
  58. package/src/lib/components/console/pos-ops/pos-check-ins-card.component.js.map +1 -1
  59. package/src/lib/components/console/pos-ops/pos-operations-bar.component.d.ts +9 -0
  60. package/src/lib/components/console/pos-ops/pos-operations-bar.component.js +15 -6
  61. package/src/lib/components/console/pos-ops/pos-operations-bar.component.js.map +1 -1
  62. package/src/lib/components/console/pos-ops/pos-rain-check-dialog.component.d.ts +25 -0
  63. package/src/lib/components/console/pos-ops/pos-rain-check-dialog.component.js +136 -0
  64. package/src/lib/components/console/pos-ops/pos-rain-check-dialog.component.js.map +1 -0
  65. package/src/lib/components/console/pos-ops/pos-shift-panel.component.d.ts +6 -0
  66. package/src/lib/components/console/pos-ops/pos-shift-panel.component.js +48 -31
  67. package/src/lib/components/console/pos-ops/pos-shift-panel.component.js.map +1 -1
  68. package/src/lib/components/console/pos-page.component.d.ts +1 -1
  69. package/src/lib/components/console/pos-page.component.js +63 -12
  70. package/src/lib/components/console/pos-page.component.js.map +1 -1
  71. package/src/lib/components/console/pos-reader-splash-setting.component.js +1 -1
  72. package/src/lib/components/console/pos-reader-splash-setting.component.js.map +1 -1
  73. package/src/lib/components/console/product-check-in-fields.component.d.ts +4 -2
  74. package/src/lib/components/console/product-check-in-fields.component.js +60 -14
  75. package/src/lib/components/console/product-check-in-fields.component.js.map +1 -1
  76. package/src/lib/components/console/product-editor-dialog.component.js +44 -1
  77. package/src/lib/components/console/product-editor-dialog.component.js.map +1 -1
  78. package/src/lib/components/console/product-hardware-sku-field.component.d.ts +19 -0
  79. package/src/lib/components/console/product-hardware-sku-field.component.js +92 -0
  80. package/src/lib/components/console/product-hardware-sku-field.component.js.map +1 -0
  81. package/src/lib/components/console/product-kiosk-offer-field.component.d.ts +22 -0
  82. package/src/lib/components/console/product-kiosk-offer-field.component.js +94 -0
  83. package/src/lib/components/console/product-kiosk-offer-field.component.js.map +1 -0
  84. package/src/lib/components/console/product-register-fields.component.d.ts +4 -0
  85. package/src/lib/components/console/product-register-fields.component.js +31 -2
  86. package/src/lib/components/console/product-register-fields.component.js.map +1 -1
  87. package/src/lib/components/console/products-hub-card.component.js +11 -2
  88. package/src/lib/components/console/products-hub-card.component.js.map +1 -1
  89. package/src/lib/components/console/register-hardware-card.component.d.ts +11 -0
  90. package/src/lib/components/console/register-hardware-card.component.js +56 -27
  91. package/src/lib/components/console/register-hardware-card.component.js.map +1 -1
  92. package/src/lib/components/console/reservation-guests-dialog.component.d.ts +28 -0
  93. package/src/lib/components/console/reservation-guests-dialog.component.js +189 -0
  94. package/src/lib/components/console/reservation-guests-dialog.component.js.map +1 -0
  95. package/src/lib/components/console/reservations-card.component.d.ts +9 -0
  96. package/src/lib/components/console/reservations-card.component.js +158 -23
  97. package/src/lib/components/console/reservations-card.component.js.map +1 -1
  98. package/src/lib/components/console/shippingeasy-card.component.js +1 -1
  99. package/src/lib/components/console/shippingeasy-card.component.js.map +1 -1
  100. package/src/lib/components/console/shipstation-card.component.d.ts +1 -1
  101. package/src/lib/components/console/shipstation-card.component.js +3 -3
  102. package/src/lib/components/console/shipstation-card.component.js.map +1 -1
  103. package/src/lib/components/product-detail.d.ts +4 -0
  104. package/src/lib/components/product-detail.js +40 -5
  105. package/src/lib/components/product-detail.js.map +1 -1
  106. package/src/lib/components/product-reviews.js +28 -11
  107. package/src/lib/components/product-reviews.js.map +1 -1
  108. package/src/lib/components/reservation-widget.js +28 -5
  109. package/src/lib/components/reservation-widget.js.map +1 -1
  110. package/src/lib/components/use-day-picker.d.ts +25 -0
  111. package/src/lib/components/use-day-picker.js +57 -0
  112. package/src/lib/components/use-day-picker.js.map +1 -0
  113. package/src/lib/constants/operator-alerts.d.ts +55 -0
  114. package/src/lib/constants/operator-alerts.js +156 -1
  115. package/src/lib/constants/operator-alerts.js.map +1 -1
  116. package/src/lib/declarations.console-server.js +54 -1
  117. package/src/lib/declarations.console-server.js.map +1 -1
  118. package/src/lib/declarations.d.ts +4 -2
  119. package/src/lib/declarations.js +11 -4
  120. package/src/lib/declarations.js.map +1 -1
  121. package/src/lib/declarations.server.js +9 -0
  122. package/src/lib/declarations.server.js.map +1 -1
  123. package/src/lib/model/commerce-cart.d.ts +9 -1
  124. package/src/lib/model/commerce-cart.js +4 -3
  125. package/src/lib/model/commerce-cart.js.map +1 -1
  126. package/src/lib/model/commerce-check-in-product.d.ts +26 -0
  127. package/src/lib/model/commerce-check-in-product.js +64 -2
  128. package/src/lib/model/commerce-check-in-product.js.map +1 -1
  129. package/src/lib/model/commerce-check-in-record.d.ts +127 -0
  130. package/src/lib/model/commerce-check-in-record.js +36 -0
  131. package/src/lib/model/commerce-check-in-record.js.map +1 -0
  132. package/src/lib/model/commerce-check-in.d.ts +91 -87
  133. package/src/lib/model/commerce-check-in.js +186 -62
  134. package/src/lib/model/commerce-check-in.js.map +1 -1
  135. package/src/lib/model/commerce-kiosk-offer.d.ts +31 -0
  136. package/src/lib/model/commerce-kiosk-offer.js +35 -0
  137. package/src/lib/model/commerce-kiosk-offer.js.map +1 -0
  138. package/src/lib/model/commerce-orders.d.ts +76 -0
  139. package/src/lib/model/commerce-orders.js +6 -0
  140. package/src/lib/model/commerce-orders.js.map +1 -1
  141. package/src/lib/model/commerce-pos-day-report.d.ts +54 -1
  142. package/src/lib/model/commerce-pos-day-report.js +39 -2
  143. package/src/lib/model/commerce-pos-day-report.js.map +1 -1
  144. package/src/lib/model/commerce-pos-offline.d.ts +1 -1
  145. package/src/lib/model/commerce-pos-offline.js +6 -1
  146. package/src/lib/model/commerce-pos-offline.js.map +1 -1
  147. package/src/lib/model/commerce-pos-ops.js +3 -2
  148. package/src/lib/model/commerce-pos-ops.js.map +1 -1
  149. package/src/lib/model/commerce-pos.d.ts +36 -3
  150. package/src/lib/model/commerce-pos.js +14 -6
  151. package/src/lib/model/commerce-pos.js.map +1 -1
  152. package/src/lib/model/commerce-receipt.js +3 -2
  153. package/src/lib/model/commerce-receipt.js.map +1 -1
  154. package/src/lib/model/commerce-reservations.d.ts +69 -4
  155. package/src/lib/model/commerce-reservations.js +57 -5
  156. package/src/lib/model/commerce-reservations.js.map +1 -1
  157. package/src/lib/model/commerce.d.ts +66 -0
  158. package/src/lib/model/commerce.js +15 -0
  159. package/src/lib/model/commerce.js.map +1 -1
  160. package/src/lib/model/order-list-fields.d.ts +11 -0
  161. package/src/lib/model/order-list-fields.js +17 -1
  162. package/src/lib/model/order-list-fields.js.map +1 -1
  163. package/src/lib/model/own-store-tax-summary.d.ts +152 -0
  164. package/src/lib/model/own-store-tax-summary.js +288 -0
  165. package/src/lib/model/own-store-tax-summary.js.map +1 -0
  166. package/src/lib/model/plugin-permissions.d.ts +67 -0
  167. package/src/lib/model/plugin-permissions.js +224 -7
  168. package/src/lib/model/plugin-permissions.js.map +1 -1
  169. package/src/lib/model/pos-cellular.d.ts +223 -0
  170. package/src/lib/model/pos-cellular.js +293 -0
  171. package/src/lib/model/pos-cellular.js.map +1 -0
  172. package/src/lib/model/pos-hardware-in-person.d.ts +98 -0
  173. package/src/lib/model/pos-hardware-in-person.js +224 -0
  174. package/src/lib/model/pos-hardware-in-person.js.map +1 -0
  175. package/src/lib/model/pos-hardware-tax-summary.d.ts +166 -0
  176. package/src/lib/model/pos-hardware-tax-summary.js +225 -0
  177. package/src/lib/model/pos-hardware-tax-summary.js.map +1 -0
  178. package/src/lib/model/pos-hardware-terms.d.ts +74 -0
  179. package/src/lib/model/pos-hardware-terms.js +115 -0
  180. package/src/lib/model/pos-hardware-terms.js.map +1 -0
  181. package/src/lib/model/pos-hardware.d.ts +349 -7
  182. package/src/lib/model/pos-hardware.js +291 -20
  183. package/src/lib/model/pos-hardware.js.map +1 -1
  184. package/src/lib/model/product-write.d.ts +10 -0
  185. package/src/lib/model/product-write.js +4 -2
  186. package/src/lib/model/product-write.js.map +1 -1
  187. package/src/lib/plugin.js +16 -0
  188. package/src/lib/plugin.js.map +1 -1
  189. package/src/lib/server/billing-webhook.js +25 -1
  190. package/src/lib/server/billing-webhook.js.map +1 -1
  191. package/src/lib/server/cancel-order.js +7 -5
  192. package/src/lib/server/cancel-order.js.map +1 -1
  193. package/src/lib/server/cart-checkout.js +20 -8
  194. package/src/lib/server/cart-checkout.js.map +1 -1
  195. package/src/lib/server/cart.js +29 -0
  196. package/src/lib/server/cart.js.map +1 -1
  197. package/src/lib/server/checkout.js +42 -13
  198. package/src/lib/server/checkout.js.map +1 -1
  199. package/src/lib/server/collection-membership.d.ts +4 -0
  200. package/src/lib/server/collection-membership.js +5 -10
  201. package/src/lib/server/collection-membership.js.map +1 -1
  202. package/src/lib/server/commerce-site-keys.d.ts +29 -0
  203. package/src/lib/server/commerce-site-keys.js +40 -0
  204. package/src/lib/server/commerce-site-keys.js.map +1 -0
  205. package/src/lib/server/draft-order.js +8 -5
  206. package/src/lib/server/draft-order.js.map +1 -1
  207. package/src/lib/server/fulfill-order.js +7 -5
  208. package/src/lib/server/fulfill-order.js.map +1 -1
  209. package/src/lib/server/gift-cards.js +8 -5
  210. package/src/lib/server/gift-cards.js.map +1 -1
  211. package/src/lib/server/local-fulfillment-status.js +9 -6
  212. package/src/lib/server/local-fulfillment-status.js.map +1 -1
  213. package/src/lib/server/member-post.js +6 -5
  214. package/src/lib/server/member-post.js.map +1 -1
  215. package/src/lib/server/membership-admin-password.d.ts +4 -2
  216. package/src/lib/server/membership-admin-password.js +10 -8
  217. package/src/lib/server/membership-admin-password.js.map +1 -1
  218. package/src/lib/server/membership-admin-remove.d.ts +7 -5
  219. package/src/lib/server/membership-admin-remove.js +9 -9
  220. package/src/lib/server/membership-admin-remove.js.map +1 -1
  221. package/src/lib/server/order-notifications.js +5 -2
  222. package/src/lib/server/order-notifications.js.map +1 -1
  223. package/src/lib/server/order-print.js +7 -5
  224. package/src/lib/server/order-print.js.map +1 -1
  225. package/src/lib/server/order-receipt-send.js +8 -5
  226. package/src/lib/server/order-receipt-send.js.map +1 -1
  227. package/src/lib/server/orders-shipping-prepare.js +9 -6
  228. package/src/lib/server/orders-shipping-prepare.js.map +1 -1
  229. package/src/lib/server/own-store-tax-return-source.d.ts +47 -0
  230. package/src/lib/server/own-store-tax-return-source.js +312 -0
  231. package/src/lib/server/own-store-tax-return-source.js.map +1 -0
  232. package/src/lib/server/payment-methods.js +13 -9
  233. package/src/lib/server/payment-methods.js.map +1 -1
  234. package/src/lib/server/pos-auth.d.ts +37 -6
  235. package/src/lib/server/pos-auth.js +40 -15
  236. package/src/lib/server/pos-auth.js.map +1 -1
  237. package/src/lib/server/pos-cellular-billing.d.ts +81 -0
  238. package/src/lib/server/pos-cellular-billing.js +711 -0
  239. package/src/lib/server/pos-cellular-billing.js.map +1 -0
  240. package/src/lib/server/pos-cellular-notify.d.ts +47 -0
  241. package/src/lib/server/pos-cellular-notify.js +150 -0
  242. package/src/lib/server/pos-cellular-notify.js.map +1 -0
  243. package/src/lib/server/pos-cellular.d.ts +88 -0
  244. package/src/lib/server/pos-cellular.js +339 -0
  245. package/src/lib/server/pos-cellular.js.map +1 -0
  246. package/src/lib/server/pos-check-in.d.ts +5 -1
  247. package/src/lib/server/pos-check-in.js +296 -28
  248. package/src/lib/server/pos-check-in.js.map +1 -1
  249. package/src/lib/server/pos-day-report.js +29 -3
  250. package/src/lib/server/pos-day-report.js.map +1 -1
  251. package/src/lib/server/pos-display.d.ts +16 -0
  252. package/src/lib/server/pos-display.js +129 -2
  253. package/src/lib/server/pos-display.js.map +1 -1
  254. package/src/lib/server/pos-hardware-admin.d.ts +10 -1
  255. package/src/lib/server/pos-hardware-admin.js +250 -84
  256. package/src/lib/server/pos-hardware-admin.js.map +1 -1
  257. package/src/lib/server/pos-hardware-attach.d.ts +105 -0
  258. package/src/lib/server/pos-hardware-attach.js +443 -0
  259. package/src/lib/server/pos-hardware-attach.js.map +1 -0
  260. package/src/lib/server/pos-hardware-buyer-lookup.d.ts +58 -0
  261. package/src/lib/server/pos-hardware-buyer-lookup.js +180 -0
  262. package/src/lib/server/pos-hardware-buyer-lookup.js.map +1 -0
  263. package/src/lib/server/pos-hardware-buyer-record.d.ts +29 -0
  264. package/src/lib/server/pos-hardware-buyer-record.js +86 -0
  265. package/src/lib/server/pos-hardware-buyer-record.js.map +1 -0
  266. package/src/lib/server/pos-hardware-cancel.d.ts +58 -0
  267. package/src/lib/server/pos-hardware-cancel.js +147 -0
  268. package/src/lib/server/pos-hardware-cancel.js.map +1 -0
  269. package/src/lib/server/pos-hardware-closure-notices.d.ts +27 -0
  270. package/src/lib/server/pos-hardware-closure-notices.js +85 -0
  271. package/src/lib/server/pos-hardware-closure-notices.js.map +1 -0
  272. package/src/lib/server/pos-hardware-closure.d.ts +60 -0
  273. package/src/lib/server/pos-hardware-closure.js +153 -0
  274. package/src/lib/server/pos-hardware-closure.js.map +1 -0
  275. package/src/lib/server/pos-hardware-in-person.d.ts +61 -0
  276. package/src/lib/server/pos-hardware-in-person.js +236 -0
  277. package/src/lib/server/pos-hardware-in-person.js.map +1 -0
  278. package/src/lib/server/pos-hardware-notify.d.ts +40 -2
  279. package/src/lib/server/pos-hardware-notify.js +195 -14
  280. package/src/lib/server/pos-hardware-notify.js.map +1 -1
  281. package/src/lib/server/pos-hardware-readers.d.ts +115 -0
  282. package/src/lib/server/pos-hardware-readers.js +430 -0
  283. package/src/lib/server/pos-hardware-readers.js.map +1 -0
  284. package/src/lib/server/pos-hardware-returns.d.ts +116 -0
  285. package/src/lib/server/pos-hardware-returns.js +502 -0
  286. package/src/lib/server/pos-hardware-returns.js.map +1 -0
  287. package/src/lib/server/pos-hardware-sale-fields.d.ts +28 -0
  288. package/src/lib/server/pos-hardware-sale-fields.js +44 -0
  289. package/src/lib/server/pos-hardware-sale-fields.js.map +1 -0
  290. package/src/lib/server/pos-hardware-sale.d.ts +54 -0
  291. package/src/lib/server/pos-hardware-sale.js +369 -0
  292. package/src/lib/server/pos-hardware-sale.js.map +1 -0
  293. package/src/lib/server/pos-hardware-tax-return-source.d.ts +99 -0
  294. package/src/lib/server/pos-hardware-tax-return-source.js +462 -0
  295. package/src/lib/server/pos-hardware-tax-return-source.js.map +1 -0
  296. package/src/lib/server/pos-hardware-terms.d.ts +64 -0
  297. package/src/lib/server/pos-hardware-terms.js +154 -0
  298. package/src/lib/server/pos-hardware-terms.js.map +1 -0
  299. package/src/lib/server/pos-hardware-user-eraser.d.ts +3 -0
  300. package/src/lib/server/pos-hardware-user-eraser.js +31 -7
  301. package/src/lib/server/pos-hardware-user-eraser.js.map +1 -1
  302. package/src/lib/server/pos-hardware-webhook.d.ts +5 -0
  303. package/src/lib/server/pos-hardware-webhook.js +51 -6
  304. package/src/lib/server/pos-hardware-webhook.js.map +1 -1
  305. package/src/lib/server/pos-hardware.d.ts +74 -1
  306. package/src/lib/server/pos-hardware.js +175 -10
  307. package/src/lib/server/pos-hardware.js.map +1 -1
  308. package/src/lib/server/pos-kiosk.d.ts +2 -0
  309. package/src/lib/server/pos-kiosk.js +17 -12
  310. package/src/lib/server/pos-kiosk.js.map +1 -1
  311. package/src/lib/server/pos-offline-card-order.d.ts +64 -0
  312. package/src/lib/server/pos-offline-card-order.js +102 -0
  313. package/src/lib/server/pos-offline-card-order.js.map +1 -0
  314. package/src/lib/server/pos-offline-card-reconcile.d.ts +67 -1
  315. package/src/lib/server/pos-offline-card-reconcile.js +215 -18
  316. package/src/lib/server/pos-offline-card-reconcile.js.map +1 -1
  317. package/src/lib/server/pos-offline-sync.d.ts +39 -0
  318. package/src/lib/server/pos-offline-sync.js +190 -55
  319. package/src/lib/server/pos-offline-sync.js.map +1 -1
  320. package/src/lib/server/pos-ops-gate.d.ts +15 -4
  321. package/src/lib/server/pos-ops-gate.js +42 -9
  322. package/src/lib/server/pos-ops-gate.js.map +1 -1
  323. package/src/lib/server/pos-ops-routes.js +24 -0
  324. package/src/lib/server/pos-ops-routes.js.map +1 -1
  325. package/src/lib/server/pos-order-refund-ids.d.ts +28 -0
  326. package/src/lib/server/pos-order-refund-ids.js +31 -0
  327. package/src/lib/server/pos-order-refund-ids.js.map +1 -0
  328. package/src/lib/server/pos-order.js +52 -49
  329. package/src/lib/server/pos-order.js.map +1 -1
  330. package/src/lib/server/pos-payment.js +1 -1
  331. package/src/lib/server/pos-payment.js.map +1 -1
  332. package/src/lib/server/pos-reader-splash-admin.js +30 -32
  333. package/src/lib/server/pos-reader-splash-admin.js.map +1 -1
  334. package/src/lib/server/pos-reader-splash-images-route.d.ts +17 -0
  335. package/src/lib/server/pos-reader-splash-images-route.js +113 -0
  336. package/src/lib/server/pos-reader-splash-images-route.js.map +1 -0
  337. package/src/lib/server/pos-reader-splash-images.d.ts +51 -0
  338. package/src/lib/server/pos-reader-splash-images.js +123 -0
  339. package/src/lib/server/pos-reader-splash-images.js.map +1 -0
  340. package/src/lib/server/pos-reader-splash.d.ts +68 -1
  341. package/src/lib/server/pos-reader-splash.js +182 -11
  342. package/src/lib/server/pos-reader-splash.js.map +1 -1
  343. package/src/lib/server/pos-readers.d.ts +29 -0
  344. package/src/lib/server/pos-readers.js +134 -35
  345. package/src/lib/server/pos-readers.js.map +1 -1
  346. package/src/lib/server/pos-return.d.ts +10 -0
  347. package/src/lib/server/pos-return.js +26 -1
  348. package/src/lib/server/pos-return.js.map +1 -1
  349. package/src/lib/server/pos-sale.js +6 -1
  350. package/src/lib/server/pos-sale.js.map +1 -1
  351. package/src/lib/server/pos-shift.js +11 -0
  352. package/src/lib/server/pos-shift.js.map +1 -1
  353. package/src/lib/server/pos-staff-pin.js +25 -20
  354. package/src/lib/server/pos-staff-pin.js.map +1 -1
  355. package/src/lib/server/pos-terminal.d.ts +14 -4
  356. package/src/lib/server/pos-terminal.js +34 -38
  357. package/src/lib/server/pos-terminal.js.map +1 -1
  358. package/src/lib/server/printers.d.ts +7 -3
  359. package/src/lib/server/printers.js +18 -12
  360. package/src/lib/server/printers.js.map +1 -1
  361. package/src/lib/server/product-drafts.d.ts +11 -1
  362. package/src/lib/server/product-drafts.js +12 -8
  363. package/src/lib/server/product-drafts.js.map +1 -1
  364. package/src/lib/server/product.d.ts +15 -1
  365. package/src/lib/server/product.js +16 -3
  366. package/src/lib/server/product.js.map +1 -1
  367. package/src/lib/server/products-write.d.ts +26 -2
  368. package/src/lib/server/products-write.js +19 -7
  369. package/src/lib/server/products-write.js.map +1 -1
  370. package/src/lib/server/refund.js +34 -11
  371. package/src/lib/server/refund.js.map +1 -1
  372. package/src/lib/server/reservation-availability-query.d.ts +8 -0
  373. package/src/lib/server/reservation-availability-query.js +11 -1
  374. package/src/lib/server/reservation-availability-query.js.map +1 -1
  375. package/src/lib/server/reservation-availability.d.ts +1 -1
  376. package/src/lib/server/reservation-availability.js +7 -2
  377. package/src/lib/server/reservation-availability.js.map +1 -1
  378. package/src/lib/server/reservation-guests.d.ts +32 -0
  379. package/src/lib/server/reservation-guests.js +285 -0
  380. package/src/lib/server/reservation-guests.js.map +1 -0
  381. package/src/lib/server/reservation-walk-in.d.ts +11 -1
  382. package/src/lib/server/reservation-walk-in.js +56 -12
  383. package/src/lib/server/reservation-walk-in.js.map +1 -1
  384. package/src/lib/server/reserve.js +18 -5
  385. package/src/lib/server/reserve.js.map +1 -1
  386. package/src/lib/server/returns.js +9 -6
  387. package/src/lib/server/returns.js.map +1 -1
  388. package/src/lib/server/reviews.d.ts +22 -2
  389. package/src/lib/server/reviews.js +88 -20
  390. package/src/lib/server/reviews.js.map +1 -1
  391. package/src/lib/server/shipping-connectors.js +10 -11
  392. package/src/lib/server/shipping-connectors.js.map +1 -1
  393. package/src/lib/server/site-page-resolver.js +20 -3
  394. package/src/lib/server/site-page-resolver.js.map +1 -1
  395. package/src/lib/server/store-day.d.ts +33 -0
  396. package/src/lib/server/store-day.js +46 -0
  397. package/src/lib/server/store-day.js.map +1 -0
  398. package/src/lib/server/store-setup-ai-writer.d.ts +2 -1
  399. package/src/lib/server/store-setup-ai-writer.js +5 -6
  400. package/src/lib/server/store-setup-ai-writer.js.map +1 -1
  401. package/src/lib/server/storefront-tax.d.ts +13 -0
  402. package/src/lib/server/storefront-tax.js +8 -1
  403. package/src/lib/server/storefront-tax.js.map +1 -1
  404. package/src/lib/server/tax-return-source.d.ts +32 -3
  405. package/src/lib/server/tax-return-source.js +99 -20
  406. package/src/lib/server/tax-return-source.js.map +1 -1
  407. package/src/lib/server.js +100 -2
  408. package/src/lib/server.js.map +1 -1
  409. package/src/lib/testing/fake-firestore.js +58 -16
  410. package/src/lib/testing/fake-firestore.js.map +1 -1
  411. package/src/lib/testing/pos-ops-memory-firestore.js +18 -4
  412. package/src/lib/testing/pos-ops-memory-firestore.js.map +1 -1
  413. package/src/lib/testing/site-register-permissions.d.ts +29 -0
  414. package/src/lib/testing/site-register-permissions.js +38 -0
  415. package/src/lib/testing/site-register-permissions.js.map +1 -0
  416. package/src/lib/transfer/tracking.server.js +8 -5
  417. package/src/lib/transfer/tracking.server.js.map +1 -1
  418. package/src/lib/utils/add-to-cart.d.ts +2 -0
  419. package/src/lib/utils/add-to-cart.js +6 -3
  420. package/src/lib/utils/add-to-cart.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/member-post.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 {\n firebaseAdmin,\n getOrgForHost,\n hostSendingIdentity,\n meterHostEmail,\n} from '@aglyn/tenant-data-admin'\nimport {\n loadHostEmailWithTokens,\n renderLoadedHostEmailWithTokens,\n} from '@aglyn/tenant-data-admin/server/host-email-tokens'\nimport * as CommerceModel from '../model'\nimport { isEmailConfigured, sendEmail } from '@aglyn/shared-util-email'\nimport {\n consentGroupForHost,\n hostPublicOrigin,\n type PluginApiHandler,\n resolveBrandingProfile,\n} from '@aglyn/aglyn/server'\n\n// Which member subscribers are live enough to email (AGL-316). The list itself\n// now lives in the model as `isTenantSubscriptionLive` (AGL-1849) — it was a\n// private copy here and a second private copy in `gate.ts`, which is the drift\n// AGL-1715 guards against on the org side.\n\n/**\n * Member post publish (AGL-316): manager-gated; writes the post and\n * optionally emails the entitled subscribers (product-scoped or all\n * live subscribers) through the env-gated Resend pipeline.\n */\nexport const memberPostHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const authorization = String(req.headers.authorization ?? '')\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) return res.status(401).json({ error: 'Unauthenticated' })\n const body =\n typeof req.body === 'string' ? JSON.parse(req.body) : (req.body ?? {})\n const hostId = String(body.hostId ?? '')\n const title = String(body.title ?? '').trim().slice(0, 160)\n const postBody = String(body.body ?? '').trim().slice(0, 5000)\n const productId = String(body.productId ?? '').trim()\n const emailSubscribers = Boolean(body.emailSubscribers)\n if (!hostId || !title) {\n return res.status(400).json({ error: 'Missing hostId or title' })\n }\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const hostSnapshot = await hostRef.get()\n // AN ALLOWLIST (AGL-2372), matching `gift-cards.ts` and the AGL-2262\n // correction to the register. The old `!role || role === 'viewer'`\n // denylist admitted every string that was not literally `viewer` — and\n // since AGL-2334 that includes `author`, a real grantable role, on a route\n // that publishes to paying subscribers and sends them mail on the\n // merchant's brand. A denylist that widens whenever the role union grows\n // is the defect; the set is the fix. Absent is refused, not permitted.\n const memberRole = (hostSnapshot.get('memberRoles') ?? {})[decoded.uid]\n if (memberRole !== 'admin' && memberRole !== 'editor') {\n return res.status(403).json({ error: 'Not permitted' })\n }\n const postRef = await hostRef.collection('memberPosts').add({\n title,\n body: postBody,\n ...(productId ? { productId } : {}),\n createdAtMs: Date.now(),\n createdBy: decoded.uid,\n })\n\n let emailed = 0\n if (emailSubscribers && isEmailConfigured()) {\n /*\n * ORDERED (D1). This was `limit(500)` with no `orderBy`, and Firestore\n * answers an unordered limit in DOCUMENT-ID order — so a site past the\n * ceiling mailed whichever five hundred subscriptions happened to hash\n * low, and the same five hundred every time. `__name__` is the order it\n * was already getting; stating it makes the window a decision rather\n * than an accident, and makes it the same window on every publish.\n */\n const subscriptions = await hostRef\n .collection('subscriptions')\n .orderBy(firebaseAdmin.firestore.FieldPath.documentId())\n .limit(500)\n .get()\n const recipients = [\n ...new Set(\n subscriptions.docs\n .filter(\n (docSnapshot) =>\n CommerceModel.isTenantSubscriptionLive(\n docSnapshot.get('status'),\n ) &&\n (!productId ||\n docSnapshot.get('productId') === productId) &&\n docSnapshot.get('customerEmail'),\n )\n .map((docSnapshot) => String(docSnapshot.get('customerEmail'))),\n ),\n ].slice(0, 200)\n // White-label sender identity (White-Label Phase 3): the store's brand\n // via the one shared resolver, from the owning org doc.\n const owner = await getOrgForHost(hostId).catch(() => null)\n const branding = resolveBrandingProfile(owner?.org as never)\n // The site's consent group, off the same org read: a member who left\n // any site the org declared one sender with this one is not mailed.\n const consentGroup = consentGroupForHost(\n (owner?.org as Record<string, unknown> | undefined) ?? null,\n hostId,\n )\n // The site's own origin, for the unsubscribe link the gate mints. Read\n // from the host document already in hand rather than assembled from an\n // apex: a post is mailed and read later, and a wrong origin sends a\n // subscriber's opt-out to a domain the merchant does not control.\n const siteBase =\n hostPublicOrigin({\n cname: hostSnapshot.get('cname'),\n subdomain: hostSnapshot.get('subdomain'),\n }) ?? ''\n /*\n * ONE site, up to two hundred recipients — so the identity is resolved\n * once and reused, not read per message. Hoisted rather than cached\n * inside the helper for the reason its docblock gives: verification can\n * change under a long-lived process, so how long a resolution may be\n * trusted is the caller's declaration.\n */\n const identity = await hostSendingIdentity(hostId)\n // The `member-post` site email (AGL-3370): the site's design, or the\n // built-in copy in its header and footer. Read once for the batch, and\n // the same message for everyone, so rendered once too.\n const designed = await loadHostEmailWithTokens(\n firestore,\n hostId,\n 'member-post',\n siteBase ? { origin: siteBase } : {},\n )\n .then((bundle) =>\n bundle\n ? renderLoadedHostEmailWithTokens(bundle, {\n 'post.title': title,\n 'post.body': postBody,\n })\n : null,\n )\n .catch(() => null)\n for (const to of recipients) {\n /*\n * MARKETING. A member post is a merchant mailing their audience, so\n * it carries the unsubscribe header pair and a visible link, it is\n * checked against both suppression lists, and it counts against how\n * much mail one person receives from this site.\n *\n * Priority stays transactional — this handler answers a click and has\n * nowhere to put a message the hourly governor deferred, and the rule\n * on `'bulk'` is that only a resumable sweep may use it.\n */\n const result = await sendEmail({\n to,\n subject: designed?.subject || title,\n text: designed?.text || postBody || title,\n ...(designed?.html ? { html: designed.html } : {}),\n fromName: branding.fromName,\n sendingIdentity: identity,\n audience: 'tenant',\n context: 'member post',\n /*\n * A post to a site's members is the \"Newsletter\" stream — regular\n * news and stories from the site. Declared so a member who left\n * that stream, while staying subscribed to everything else, is not\n * mailed it; the gate asks the topic only for callers that name\n * one.\n */\n marketing: {\n hostId,\n siteBase,\n topicId: CommerceModel.NEWSLETTER_TOPIC_ID,\n consentHostIds: consentGroup.hostIds,\n consentAwaitsConfirmation: consentGroup.awaitsConfirmation,\n },\n })\n // The cost meter counts messages that LEFT. A suppressed or capped\n // recipient produced no message and therefore no cost, and counting\n // one would bill a merchant for the control working.\n if (result.sent) emailed += 1\n }\n // Cost meter (AGL-1438), once for the batch rather than per recipient —\n // same number, one write. Transactional: a member post is content the\n // subscriber is paying for, not a discretionary campaign, so it counts\n // toward cost and is never refused by the campaign cap.\n await meterHostEmail(hostId, emailed)\n }\n return res.status(200).json({ postId: postRef.id, emailed })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Publish failed' })\n }\n}\n"],"names":["firebaseAdmin","getOrgForHost","hostSendingIdentity","meterHostEmail","loadHostEmailWithTokens","renderLoadedHostEmailWithTokens","CommerceModel","isEmailConfigured","sendEmail","consentGroupForHost","hostPublicOrigin","resolveBrandingProfile","memberPostHandler","req","res","body","method","status","json","error","authorization","String","headers","idToken","startsWith","slice","length","undefined","JSON","parse","hostId","title","trim","postBody","productId","emailSubscribers","Boolean","hostSnapshot","decoded","app","auth","verifyIdToken","firestore","hostRef","collection","doc","get","memberRole","uid","postRef","add","createdAtMs","Date","now","createdBy","emailed","subscriptions","orderBy","FieldPath","documentId","limit","recipients","Set","docs","filter","docSnapshot","isTenantSubscriptionLive","map","owner","catch","branding","org","consentGroup","siteBase","cname","subdomain","identity","designed","origin","then","bundle","to","result","subject","text","html","fromName","sendingIdentity","audience","context","marketing","topicId","NEWSLETTER_TOPIC_ID","consentHostIds","hostIds","consentAwaitsConfirmation","awaitsConfirmation","sent","postId","id","console"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/member-post.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 { holdsSiteKey, siteKeyRefusal } from '@aglyn/aglyn/app-utils/host-permissions'\nimport {\n firebaseAdmin,\n getOrgForHost,\n hostSendingIdentity,\n meterHostEmail,\n} from '@aglyn/tenant-data-admin'\nimport {\n loadHostEmailWithTokens,\n renderLoadedHostEmailWithTokens,\n} from '@aglyn/tenant-data-admin/server/host-email-tokens'\nimport * as CommerceModel from '../model'\nimport { isEmailConfigured, sendEmail } from '@aglyn/shared-util-email'\nimport {\n consentGroupForHost,\n hostPublicOrigin,\n type PluginApiHandler,\n resolveBrandingProfile,\n} from '@aglyn/aglyn/server'\n\n// Which member subscribers are live enough to email (AGL-316). The list itself\n// now lives in the model as `isTenantSubscriptionLive` (AGL-1849) — it was a\n// private copy here and a second private copy in `gate.ts`, which is the drift\n// AGL-1715 guards against on the org side.\n\n/**\n * Member post publish (AGL-316): manager-gated; writes the post and\n * optionally emails the entitled subscribers (product-scoped or all\n * live subscribers) through the env-gated Resend pipeline.\n */\nexport const memberPostHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const authorization = String(req.headers.authorization ?? '')\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) return res.status(401).json({ error: 'Unauthenticated' })\n const body =\n typeof req.body === 'string' ? JSON.parse(req.body) : (req.body ?? {})\n const hostId = String(body.hostId ?? '')\n const title = String(body.title ?? '').trim().slice(0, 160)\n const postBody = String(body.body ?? '').trim().slice(0, 5000)\n const productId = String(body.productId ?? '').trim()\n const emailSubscribers = Boolean(body.emailSubscribers)\n if (!hostId || !title) {\n return res.status(400).json({ error: 'Missing hostId or title' })\n }\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const hostSnapshot = await hostRef.get()\n // AN ALLOWLIST (AGL-2372), matching `gift-cards.ts` and the AGL-2262\n // correction to the register. The old `!role || role === 'viewer'`\n // denylist admitted every string that was not literally `viewer` — and\n // since AGL-2334 that includes `author`, a real grantable role, on a route\n // that publishes to paying subscribers and sends them mail on the\n // merchant's brand. A denylist that widens whenever the role union grows\n // is the defect; the set is the fix. Absent is refused, not permitted.\n // \"Publish site content\" (`site.publish`, AGL-4047): a post goes to paying\n // subscribers the moment it exists, as the rules' delete asks it.\n if (!holdsSiteKey(hostSnapshot, decoded.uid, 'site.publish')) {\n return res.status(403).json({ error: siteKeyRefusal('site.publish') })\n }\n const postRef = await hostRef.collection('memberPosts').add({\n title,\n body: postBody,\n ...(productId ? { productId } : {}),\n createdAtMs: Date.now(),\n createdBy: decoded.uid,\n })\n\n let emailed = 0\n if (emailSubscribers && isEmailConfigured()) {\n /*\n * ORDERED (D1). This was `limit(500)` with no `orderBy`, and Firestore\n * answers an unordered limit in DOCUMENT-ID order — so a site past the\n * ceiling mailed whichever five hundred subscriptions happened to hash\n * low, and the same five hundred every time. `__name__` is the order it\n * was already getting; stating it makes the window a decision rather\n * than an accident, and makes it the same window on every publish.\n */\n const subscriptions = await hostRef\n .collection('subscriptions')\n .orderBy(firebaseAdmin.firestore.FieldPath.documentId())\n .limit(500)\n .get()\n const recipients = [\n ...new Set(\n subscriptions.docs\n .filter(\n (docSnapshot) =>\n CommerceModel.isTenantSubscriptionLive(\n docSnapshot.get('status'),\n ) &&\n (!productId ||\n docSnapshot.get('productId') === productId) &&\n docSnapshot.get('customerEmail'),\n )\n .map((docSnapshot) => String(docSnapshot.get('customerEmail'))),\n ),\n ].slice(0, 200)\n // White-label sender identity (White-Label Phase 3): the store's brand\n // via the one shared resolver, from the owning org doc.\n const owner = await getOrgForHost(hostId).catch(() => null)\n const branding = resolveBrandingProfile(owner?.org as never)\n // The site's consent group, off the same org read: a member who left\n // any site the org declared one sender with this one is not mailed.\n const consentGroup = consentGroupForHost(\n (owner?.org as Record<string, unknown> | undefined) ?? null,\n hostId,\n )\n // The site's own origin, for the unsubscribe link the gate mints. Read\n // from the host document already in hand rather than assembled from an\n // apex: a post is mailed and read later, and a wrong origin sends a\n // subscriber's opt-out to a domain the merchant does not control.\n const siteBase =\n hostPublicOrigin({\n cname: hostSnapshot.get('cname'),\n subdomain: hostSnapshot.get('subdomain'),\n }) ?? ''\n /*\n * ONE site, up to two hundred recipients — so the identity is resolved\n * once and reused, not read per message. Hoisted rather than cached\n * inside the helper for the reason its docblock gives: verification can\n * change under a long-lived process, so how long a resolution may be\n * trusted is the caller's declaration.\n */\n const identity = await hostSendingIdentity(hostId)\n // The `member-post` site email (AGL-3370): the site's design, or the\n // built-in copy in its header and footer. Read once for the batch, and\n // the same message for everyone, so rendered once too.\n const designed = await loadHostEmailWithTokens(\n firestore,\n hostId,\n 'member-post',\n siteBase ? { origin: siteBase } : {},\n )\n .then((bundle) =>\n bundle\n ? renderLoadedHostEmailWithTokens(bundle, {\n 'post.title': title,\n 'post.body': postBody,\n })\n : null,\n )\n .catch(() => null)\n for (const to of recipients) {\n /*\n * MARKETING. A member post is a merchant mailing their audience, so\n * it carries the unsubscribe header pair and a visible link, it is\n * checked against both suppression lists, and it counts against how\n * much mail one person receives from this site.\n *\n * Priority stays transactional — this handler answers a click and has\n * nowhere to put a message the hourly governor deferred, and the rule\n * on `'bulk'` is that only a resumable sweep may use it.\n */\n const result = await sendEmail({\n to,\n subject: designed?.subject || title,\n text: designed?.text || postBody || title,\n ...(designed?.html ? { html: designed.html } : {}),\n fromName: branding.fromName,\n sendingIdentity: identity,\n audience: 'tenant',\n context: 'member post',\n /*\n * A post to a site's members is the \"Newsletter\" stream — regular\n * news and stories from the site. Declared so a member who left\n * that stream, while staying subscribed to everything else, is not\n * mailed it; the gate asks the topic only for callers that name\n * one.\n */\n marketing: {\n hostId,\n siteBase,\n topicId: CommerceModel.NEWSLETTER_TOPIC_ID,\n consentHostIds: consentGroup.hostIds,\n consentAwaitsConfirmation: consentGroup.awaitsConfirmation,\n },\n })\n // The cost meter counts messages that LEFT. A suppressed or capped\n // recipient produced no message and therefore no cost, and counting\n // one would bill a merchant for the control working.\n if (result.sent) emailed += 1\n }\n // Cost meter (AGL-1438), once for the batch rather than per recipient —\n // same number, one write. Transactional: a member post is content the\n // subscriber is paying for, not a discretionary campaign, so it counts\n // toward cost and is never refused by the campaign cap.\n await meterHostEmail(hostId, emailed)\n }\n return res.status(200).json({ postId: postRef.id, emailed })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Publish failed' })\n }\n}\n"],"names":["holdsSiteKey","siteKeyRefusal","firebaseAdmin","getOrgForHost","hostSendingIdentity","meterHostEmail","loadHostEmailWithTokens","renderLoadedHostEmailWithTokens","CommerceModel","isEmailConfigured","sendEmail","consentGroupForHost","hostPublicOrigin","resolveBrandingProfile","memberPostHandler","req","res","body","method","status","json","error","authorization","String","headers","idToken","startsWith","slice","length","undefined","JSON","parse","hostId","title","trim","postBody","productId","emailSubscribers","Boolean","decoded","app","auth","verifyIdToken","firestore","hostRef","collection","doc","hostSnapshot","get","uid","postRef","add","createdAtMs","Date","now","createdBy","emailed","subscriptions","orderBy","FieldPath","documentId","limit","recipients","Set","docs","filter","docSnapshot","isTenantSubscriptionLive","map","owner","catch","branding","org","consentGroup","siteBase","cname","subdomain","identity","designed","origin","then","bundle","to","result","subject","text","html","fromName","sendingIdentity","audience","context","marketing","topicId","NEWSLETTER_TOPIC_ID","consentHostIds","hostIds","consentAwaitsConfirmation","awaitsConfirmation","sent","postId","id","console"],"mappings":"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"}
@@ -26,8 +26,10 @@ import type { PluginApiHandler } from '@aglyn/aglyn/server';
26
26
  * account exists, so hiding failures would only leave them guessing whether
27
27
  * their click did anything. Every outcome here is reported honestly.
28
28
  *
29
- * Auth is the console id-token check `hosts/members` uses (site admin via
30
- * the `memberRoles` projection, or an org role carrying manageMembers) — NOT
29
+ * Auth is the console id-token check `hosts/members` uses ("Manage site
30
+ * settings" here, `site.manage` (AGL-4047), whose preset is the site admin
31
+ * the `memberRoles` projection named before, or an org role carrying
32
+ * manageMembers) — NOT
31
33
  * the visitor session cookie the rest of the membership endpoints take. A
32
34
  * console route, so it is registered on the console's API surface.
33
35
  *
@@ -1,3 +1,4 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
1
2
  /**
2
3
  * @license
3
4
  * Copyright 2026 Aglyn LLC
@@ -13,7 +14,7 @@
13
14
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
15
  * See the License for the specific language governing permissions and
15
16
  * limitations under the License.
16
- */ import { _ as _extends } from "@swc/helpers/_/_extends";
17
+ */ import { holdsSiteKey, siteKeyRefusal } from "@aglyn/aglyn/app-utils/host-permissions";
17
18
  import { resolveBrandingProfile, validateNewPassword } from "@aglyn/aglyn/server";
18
19
  import { hostPublicOrigin } from "@aglyn/aglyn/server";
19
20
  import { isEmailConfigured, sendEmail } from "@aglyn/shared-util-email";
@@ -33,8 +34,10 @@ import { isMemberDocumentId, memberCredentialRef, readMemberPasswordHash, retire
33
34
  * account exists, so hiding failures would only leave them guessing whether
34
35
  * their click did anything. Every outcome here is reported honestly.
35
36
  *
36
- * Auth is the console id-token check `hosts/members` uses (site admin via
37
- * the `memberRoles` projection, or an org role carrying manageMembers) — NOT
37
+ * Auth is the console id-token check `hosts/members` uses ("Manage site
38
+ * settings" here, `site.manage` (AGL-4047), whose preset is the site admin
39
+ * the `memberRoles` projection named before, or an org role carrying
40
+ * manageMembers) — NOT
38
41
  * the visitor session cookie the rest of the membership endpoints take. A
39
42
  * console route, so it is registered on the console's API surface.
40
43
  *
@@ -73,7 +76,7 @@ import { isMemberDocumentId, memberCredentialRef, readMemberPasswordHash, retire
73
76
  });
74
77
  }
75
78
  try {
76
- var _ref3, _memberSnapshot_get, _hostPublicOrigin, _ref4, _hostSnapshot_get;
79
+ var _memberSnapshot_get, _hostPublicOrigin, _ref3, _hostSnapshot_get;
77
80
  var _this, _req_body3;
78
81
  const app = firebaseAdmin.app();
79
82
  const decoded = await app.auth().verifyIdToken(idToken);
@@ -90,13 +93,12 @@ import { isMemberDocumentId, memberCredentialRef, readMemberPasswordHash, retire
90
93
  const hostRef = firestore.collection('hosts').doc(hostId);
91
94
  const hostSnapshot = await hostRef.get();
92
95
  const host = hostSnapshot.data();
93
- const memberRole = ((_ref3 = host == null ? void 0 : host['memberRoles']) != null ? _ref3 : {})[decoded.uid];
94
96
  const orgPermissions = await resolveOrgPermissions(decoded.uid, {
95
97
  hostId
96
98
  });
97
- if (!host || memberRole !== 'admin' && !orgPermissions.permissions.manageMembers) {
99
+ if (!host || !holdsSiteKey(host, decoded.uid, 'site.manage') && !orgPermissions.permissions.manageMembers) {
98
100
  return res.status(403).json({
99
- error: 'Not a site admin'
101
+ error: siteKeyRefusal('site.manage')
100
102
  });
101
103
  }
102
104
  const memberRef = hostRef.collection('siteMembers').doc(memberId);
@@ -119,7 +121,7 @@ import { isMemberDocumentId, memberCredentialRef, readMemberPasswordHash, retire
119
121
  cname: hostSnapshot.get('cname'),
120
122
  subdomain: hostSnapshot.get('subdomain')
121
123
  })) != null ? _hostPublicOrigin : '';
122
- const siteName = String((_ref4 = (_hostSnapshot_get = hostSnapshot.get('displayName')) != null ? _hostSnapshot_get : hostSnapshot.get('subdomain')) != null ? _ref4 : 'the site');
124
+ const siteName = String((_ref3 = (_hostSnapshot_get = hostSnapshot.get('displayName')) != null ? _hostSnapshot_get : hostSnapshot.get('subdomain')) != null ? _ref3 : 'the site');
123
125
  // White-label sender identity (White-Label Phase 3): the store's brand via
124
126
  // the one shared resolver; the copy already uses the site name.
125
127
  const branding = resolveBrandingProfile((_this = await getOrgForHost(hostId).catch(()=>null)) == null ? void 0 : _this.org);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/membership-admin-password.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 { resolveBrandingProfile, validateNewPassword } from '@aglyn/aglyn/server'\nimport { hostPublicOrigin } from '@aglyn/aglyn/server'\nimport { isEmailConfigured, sendEmail } from '@aglyn/shared-util-email'\nimport { renderHostEmailWithTokens } from '@aglyn/tenant-data-admin/server/host-email-tokens'\nimport {\n consumePasswordResetSend,\n firebaseAdmin,\n getOrgForHost,\n hostSendingIdentity,\n isImpersonationSession,\n meterHostEmail,\n passwordResetThrottleMessage,\n} from '@aglyn/tenant-data-admin'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\nimport {\n hashMemberPassword,\n MEMBER_SESSIONS_VALID_FROM_FIELD,\n mintPasswordResetToken,\n} from './membership'\nimport {\n isMemberDocumentId,\n memberCredentialRef,\n readMemberPasswordHash,\n retiredCredentialFields,\n} from './member-credentials'\n\n/**\n * Console-side password help for a SITE MEMBER (AGL-914) — a visitor account\n * on a published site, not an Aglyn console user.\n *\n * Sibling to `membership-recover`, and deliberately its opposite in tone.\n * That endpoint is public, so it answers `{ok:true}` no matter what to avoid\n * confirming whether an address has an account. This one is driven by a site\n * admin looking straight at the member's record — they already know the\n * account exists, so hiding failures would only leave them guessing whether\n * their click did anything. Every outcome here is reported honestly.\n *\n * Auth is the console id-token check `hosts/members` uses (site admin via\n * the `memberRoles` projection, or an org role carrying manageMembers) — NOT\n * the visitor session cookie the rest of the membership endpoints take. A\n * console route, so it is registered on the console's API surface.\n *\n * The hash is written to the member's credential document (AGL-3308), which\n * no client can read, and any credential field on the profile is deleted in\n * the same batch.\n */\nexport const membershipAdminPasswordHandler: PluginApiHandler = async (\n req,\n res,\n) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const authorization = String(req.headers['authorization'] ?? '')\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : ''\n if (!idToken) return res.status(401).json({ error: 'Unauthenticated' })\n\n const hostId = String(req.body?.hostId ?? '')\n const memberId = String(req.body?.memberId ?? '')\n const action = String(req.body?.action ?? '')\n if (!hostId || !memberId) {\n return res.status(400).json({ error: 'Missing hostId or memberId' })\n }\n if (!isMemberDocumentId(memberId)) {\n return res.status(400).json({ error: 'Unknown member' })\n }\n if (action !== 'sendPasswordReset' && action !== 'setPassword') {\n return res.status(400).json({ error: 'Unknown action' })\n }\n\n try {\n const app = firebaseAdmin.app()\n const decoded = await app.auth().verifyIdToken(idToken)\n if (!decoded.email_verified && !isImpersonationSession(decoded)) {\n // Same shape as emailUnverifiedResponse (AGL-479), written by hand\n // because that helper returns a fetch `Response` and this is a\n // (req, res) plugin handler.\n return res.status(403).json({\n error: 'Verify your email to continue',\n reason: 'email-unverified',\n })\n }\n const firestore = app.firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const hostSnapshot = await hostRef.get()\n const host = hostSnapshot.data()\n const memberRole = (host?.['memberRoles'] ?? {})[decoded.uid]\n const orgPermissions = await resolveOrgPermissions(decoded.uid, { hostId })\n if (\n !host ||\n (memberRole !== 'admin' && !orgPermissions.permissions.manageMembers)\n ) {\n return res.status(403).json({ error: 'Not a site admin' })\n }\n\n const memberRef = hostRef.collection('siteMembers').doc(memberId)\n const memberSnapshot = await memberRef.get()\n if (!memberSnapshot.exists) {\n return res.status(404).json({ error: 'Unknown member' })\n }\n const email = String(memberSnapshot.get('email') ?? '')\n if (!email) {\n return res.status(400).json({ error: 'This member has no email address' })\n }\n\n // `hostPublicOrigin`, not a hand-rolled apex (AGL-2195). This URL is\n // minted server-side and mailed out, so a wrong apex is not a display\n // bug — it is a live link on somebody else's domain.\n const siteBase =\n hostPublicOrigin({\n cname: hostSnapshot.get('cname'),\n subdomain: hostSnapshot.get('subdomain'),\n }) ?? ''\n const siteName = String(\n hostSnapshot.get('displayName') ?? hostSnapshot.get('subdomain') ?? 'the site',\n )\n // White-label sender identity (White-Label Phase 3): the store's brand via\n // the one shared resolver; the copy already uses the site name.\n const branding = resolveBrandingProfile(\n (await getOrgForHost(hostId).catch(() => null))?.org as never,\n )\n\n if (action === 'sendPasswordReset') {\n // Throttled per recipient and per actor (AGL-920). Same caps as the\n // console surfaces — a site member's inbox is no less worth protecting\n // than a console user's.\n const throttle = await consumePasswordResetSend({\n actorKey: decoded.uid,\n recipientKey: email,\n })\n if (!throttle.allowed) {\n res.setHeader('Retry-After', String(throttle.retryAfterSeconds))\n return res\n .status(429)\n .json({ error: passwordResetThrottleMessage(throttle) })\n }\n if (!isEmailConfigured()) {\n return res.status(502).json({\n error: 'Email is not configured, so no reset link could be sent',\n })\n }\n // Same single-use, hash-bound token the self-serve flow mints\n // (AGL-552) — completing the reset rewrites `passwordScrypt`, which\n // retires the token without any server-side storage.\n const token = mintPasswordResetToken(\n hostId,\n memberId,\n await readMemberPasswordHash(hostRef, memberSnapshot),\n )\n const resetUrl = `${siteBase}/recover?token=${encodeURIComponent(token)}`\n const intro = `An administrator of ${siteName} started a password reset for your account.`\n const note =\n 'The link works once and expires in 1 hour. Your current ' +\n 'password keeps working until you use it.'\n // The `member-password-reset` site email (AGL-3370), as the member's\n // own request sends it, with the words for who started it.\n const designed = await renderHostEmailWithTokens(\n firestore,\n hostId,\n 'member-password-reset',\n { 'site.name': siteName, 'reset.intro': intro, resetUrl, 'reset.note': note },\n siteBase ? { origin: siteBase } : {},\n ).catch(() => null)\n const result = await sendEmail({\n to: email,\n subject: designed?.subject || `Reset your ${siteName} password`,\n text:\n designed?.text ||\n `${intro.replace(/\\.$/, '')}. Set a new password here:\\n\\n` +\n `${resetUrl}\\n\\n${note}`,\n ...(designed?.html ? { html: designed.html } : {}),\n fromName: branding.fromName,\n sendingIdentity: await hostSendingIdentity(hostId),\n audience: 'tenant',\n context: 'membership admin recovery',\n // Owed to the recipient by their own account: the phishing\n // screen's soft rules never hold it (AGL-3356).\n owedFor: 'account',\n })\n if (!result.sent) {\n return res.status(502).json({\n error: 'The reset email could not be delivered',\n })\n }\n // Cost meter (AGL-1438). Transactional: a reset a quota refused leaves\n // the member locked out of a site they pay for.\n await meterHostEmail(hostId)\n return res.status(200).json({ ok: true })\n }\n\n const validated = validateNewPassword(req.body?.password)\n if (validated.error) {\n return res.status(400).json({ error: validated.error })\n }\n // Rewriting the hash alone would NOT sign the member out: their session\n // is an HMAC cookie that never encodes the password (unlike a reset\n // token, which binds to it on purpose). The cut-off field is what\n // actually revokes the old sessions — see readActiveMemberSession. It\n // stays on the profile, which every session check already reads.\n //\n // One batch: the profile `update` refuses a member removed since the\n // read above, and that refusal takes the credential write with it.\n const batch = firestore.batch()\n batch.set(\n memberCredentialRef(hostRef, memberId),\n { passwordScrypt: hashMemberPassword(validated.password) },\n { merge: true },\n )\n batch.update(memberRef, {\n ...retiredCredentialFields(),\n [MEMBER_SESSIONS_VALID_FROM_FIELD]: Date.now(),\n })\n await batch.commit()\n // Best-effort: the password has already changed, so a mail failure must\n // not fail the request and send the admin round again. Report it so\n // they know to reach the member another way.\n let notified = false\n if (isEmailConfigured()) {\n // The `member-password-changed` site email (AGL-3370).\n const designed = await renderHostEmailWithTokens(\n firestore,\n hostId,\n 'member-password-changed',\n { 'site.name': siteName, signInUrl: siteBase },\n siteBase ? { origin: siteBase } : {},\n ).catch(() => null)\n // How to get back in (AGL-3432): the member does not know the password\n // the administrator chose, so the email says to ask for it or to set\n // their own through \"Forgot password?\".\n const result = await sendEmail({\n to: email,\n subject: designed?.subject || `Your ${siteName} password was changed`,\n text:\n designed?.text ||\n `An administrator of ${siteName} set a new password on your ` +\n 'account and signed you out on every device. To sign back in at ' +\n `${siteBase}, get the new password from them, or choose ` +\n '\"Forgot password?\" on the sign-in page to set your own.\\n\\n' +\n 'If you did not expect this, contact the site owner.',\n ...(designed?.html ? { html: designed.html } : {}),\n fromName: branding.fromName,\n sendingIdentity: await hostSendingIdentity(hostId),\n audience: 'tenant',\n context: 'membership admin password change',\n // Owed to the recipient by their own account: the phishing\n // screen's soft rules never hold it (AGL-3356).\n owedFor: 'account',\n })\n notified = result.sent\n // Cost meter (AGL-1438). Transactional: a security notice.\n if (result.sent) await meterHostEmail(hostId)\n }\n return res.status(200).json({ ok: true, notified })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Password operation failed' })\n }\n}\n"],"names":["resolveBrandingProfile","validateNewPassword","hostPublicOrigin","isEmailConfigured","sendEmail","renderHostEmailWithTokens","consumePasswordResetSend","firebaseAdmin","getOrgForHost","hostSendingIdentity","isImpersonationSession","meterHostEmail","passwordResetThrottleMessage","resolveOrgPermissions","hashMemberPassword","MEMBER_SESSIONS_VALID_FROM_FIELD","mintPasswordResetToken","isMemberDocumentId","memberCredentialRef","readMemberPasswordHash","retiredCredentialFields","membershipAdminPasswordHandler","req","res","method","status","json","error","authorization","String","headers","idToken","startsWith","slice","length","hostId","body","memberId","action","memberSnapshot","hostSnapshot","app","decoded","auth","verifyIdToken","email_verified","reason","firestore","hostRef","collection","doc","get","host","data","memberRole","uid","orgPermissions","permissions","manageMembers","memberRef","exists","email","siteBase","cname","subdomain","siteName","branding","catch","org","throttle","actorKey","recipientKey","allowed","setHeader","retryAfterSeconds","token","resetUrl","encodeURIComponent","intro","note","designed","origin","result","to","subject","text","replace","html","fromName","sendingIdentity","audience","context","owedFor","sent","ok","validated","password","batch","set","passwordScrypt","merge","update","Date","now","commit","notified","signInUrl","console"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/membership-admin-password.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 { holdsSiteKey, siteKeyRefusal } from '@aglyn/aglyn/app-utils/host-permissions'\nimport type { PluginApiHandler } from '@aglyn/aglyn/server'\nimport { resolveBrandingProfile, validateNewPassword } from '@aglyn/aglyn/server'\nimport { hostPublicOrigin } from '@aglyn/aglyn/server'\nimport { isEmailConfigured, sendEmail } from '@aglyn/shared-util-email'\nimport { renderHostEmailWithTokens } from '@aglyn/tenant-data-admin/server/host-email-tokens'\nimport {\n consumePasswordResetSend,\n firebaseAdmin,\n getOrgForHost,\n hostSendingIdentity,\n isImpersonationSession,\n meterHostEmail,\n passwordResetThrottleMessage,\n} from '@aglyn/tenant-data-admin'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\nimport {\n hashMemberPassword,\n MEMBER_SESSIONS_VALID_FROM_FIELD,\n mintPasswordResetToken,\n} from './membership'\nimport {\n isMemberDocumentId,\n memberCredentialRef,\n readMemberPasswordHash,\n retiredCredentialFields,\n} from './member-credentials'\n\n/**\n * Console-side password help for a SITE MEMBER (AGL-914) — a visitor account\n * on a published site, not an Aglyn console user.\n *\n * Sibling to `membership-recover`, and deliberately its opposite in tone.\n * That endpoint is public, so it answers `{ok:true}` no matter what to avoid\n * confirming whether an address has an account. This one is driven by a site\n * admin looking straight at the member's record — they already know the\n * account exists, so hiding failures would only leave them guessing whether\n * their click did anything. Every outcome here is reported honestly.\n *\n * Auth is the console id-token check `hosts/members` uses (\"Manage site\n * settings\" here, `site.manage` (AGL-4047), whose preset is the site admin\n * the `memberRoles` projection named before, or an org role carrying\n * manageMembers) — NOT\n * the visitor session cookie the rest of the membership endpoints take. A\n * console route, so it is registered on the console's API surface.\n *\n * The hash is written to the member's credential document (AGL-3308), which\n * no client can read, and any credential field on the profile is deleted in\n * the same batch.\n */\nexport const membershipAdminPasswordHandler: PluginApiHandler = async (\n req,\n res,\n) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const authorization = String(req.headers['authorization'] ?? '')\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : ''\n if (!idToken) return res.status(401).json({ error: 'Unauthenticated' })\n\n const hostId = String(req.body?.hostId ?? '')\n const memberId = String(req.body?.memberId ?? '')\n const action = String(req.body?.action ?? '')\n if (!hostId || !memberId) {\n return res.status(400).json({ error: 'Missing hostId or memberId' })\n }\n if (!isMemberDocumentId(memberId)) {\n return res.status(400).json({ error: 'Unknown member' })\n }\n if (action !== 'sendPasswordReset' && action !== 'setPassword') {\n return res.status(400).json({ error: 'Unknown action' })\n }\n\n try {\n const app = firebaseAdmin.app()\n const decoded = await app.auth().verifyIdToken(idToken)\n if (!decoded.email_verified && !isImpersonationSession(decoded)) {\n // Same shape as emailUnverifiedResponse (AGL-479), written by hand\n // because that helper returns a fetch `Response` and this is a\n // (req, res) plugin handler.\n return res.status(403).json({\n error: 'Verify your email to continue',\n reason: 'email-unverified',\n })\n }\n const firestore = app.firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const hostSnapshot = await hostRef.get()\n const host = hostSnapshot.data()\n const orgPermissions = await resolveOrgPermissions(decoded.uid, { hostId })\n if (\n !host ||\n (!holdsSiteKey(host, decoded.uid, 'site.manage') && !orgPermissions.permissions.manageMembers)\n ) {\n return res.status(403).json({ error: siteKeyRefusal('site.manage') })\n }\n\n const memberRef = hostRef.collection('siteMembers').doc(memberId)\n const memberSnapshot = await memberRef.get()\n if (!memberSnapshot.exists) {\n return res.status(404).json({ error: 'Unknown member' })\n }\n const email = String(memberSnapshot.get('email') ?? '')\n if (!email) {\n return res.status(400).json({ error: 'This member has no email address' })\n }\n\n // `hostPublicOrigin`, not a hand-rolled apex (AGL-2195). This URL is\n // minted server-side and mailed out, so a wrong apex is not a display\n // bug — it is a live link on somebody else's domain.\n const siteBase =\n hostPublicOrigin({\n cname: hostSnapshot.get('cname'),\n subdomain: hostSnapshot.get('subdomain'),\n }) ?? ''\n const siteName = String(\n hostSnapshot.get('displayName') ?? hostSnapshot.get('subdomain') ?? 'the site',\n )\n // White-label sender identity (White-Label Phase 3): the store's brand via\n // the one shared resolver; the copy already uses the site name.\n const branding = resolveBrandingProfile(\n (await getOrgForHost(hostId).catch(() => null))?.org as never,\n )\n\n if (action === 'sendPasswordReset') {\n // Throttled per recipient and per actor (AGL-920). Same caps as the\n // console surfaces — a site member's inbox is no less worth protecting\n // than a console user's.\n const throttle = await consumePasswordResetSend({\n actorKey: decoded.uid,\n recipientKey: email,\n })\n if (!throttle.allowed) {\n res.setHeader('Retry-After', String(throttle.retryAfterSeconds))\n return res\n .status(429)\n .json({ error: passwordResetThrottleMessage(throttle) })\n }\n if (!isEmailConfigured()) {\n return res.status(502).json({\n error: 'Email is not configured, so no reset link could be sent',\n })\n }\n // Same single-use, hash-bound token the self-serve flow mints\n // (AGL-552) — completing the reset rewrites `passwordScrypt`, which\n // retires the token without any server-side storage.\n const token = mintPasswordResetToken(\n hostId,\n memberId,\n await readMemberPasswordHash(hostRef, memberSnapshot),\n )\n const resetUrl = `${siteBase}/recover?token=${encodeURIComponent(token)}`\n const intro = `An administrator of ${siteName} started a password reset for your account.`\n const note =\n 'The link works once and expires in 1 hour. Your current ' +\n 'password keeps working until you use it.'\n // The `member-password-reset` site email (AGL-3370), as the member's\n // own request sends it, with the words for who started it.\n const designed = await renderHostEmailWithTokens(\n firestore,\n hostId,\n 'member-password-reset',\n { 'site.name': siteName, 'reset.intro': intro, resetUrl, 'reset.note': note },\n siteBase ? { origin: siteBase } : {},\n ).catch(() => null)\n const result = await sendEmail({\n to: email,\n subject: designed?.subject || `Reset your ${siteName} password`,\n text:\n designed?.text ||\n `${intro.replace(/\\.$/, '')}. Set a new password here:\\n\\n` +\n `${resetUrl}\\n\\n${note}`,\n ...(designed?.html ? { html: designed.html } : {}),\n fromName: branding.fromName,\n sendingIdentity: await hostSendingIdentity(hostId),\n audience: 'tenant',\n context: 'membership admin recovery',\n // Owed to the recipient by their own account: the phishing\n // screen's soft rules never hold it (AGL-3356).\n owedFor: 'account',\n })\n if (!result.sent) {\n return res.status(502).json({\n error: 'The reset email could not be delivered',\n })\n }\n // Cost meter (AGL-1438). Transactional: a reset a quota refused leaves\n // the member locked out of a site they pay for.\n await meterHostEmail(hostId)\n return res.status(200).json({ ok: true })\n }\n\n const validated = validateNewPassword(req.body?.password)\n if (validated.error) {\n return res.status(400).json({ error: validated.error })\n }\n // Rewriting the hash alone would NOT sign the member out: their session\n // is an HMAC cookie that never encodes the password (unlike a reset\n // token, which binds to it on purpose). The cut-off field is what\n // actually revokes the old sessions — see readActiveMemberSession. It\n // stays on the profile, which every session check already reads.\n //\n // One batch: the profile `update` refuses a member removed since the\n // read above, and that refusal takes the credential write with it.\n const batch = firestore.batch()\n batch.set(\n memberCredentialRef(hostRef, memberId),\n { passwordScrypt: hashMemberPassword(validated.password) },\n { merge: true },\n )\n batch.update(memberRef, {\n ...retiredCredentialFields(),\n [MEMBER_SESSIONS_VALID_FROM_FIELD]: Date.now(),\n })\n await batch.commit()\n // Best-effort: the password has already changed, so a mail failure must\n // not fail the request and send the admin round again. Report it so\n // they know to reach the member another way.\n let notified = false\n if (isEmailConfigured()) {\n // The `member-password-changed` site email (AGL-3370).\n const designed = await renderHostEmailWithTokens(\n firestore,\n hostId,\n 'member-password-changed',\n { 'site.name': siteName, signInUrl: siteBase },\n siteBase ? { origin: siteBase } : {},\n ).catch(() => null)\n // How to get back in (AGL-3432): the member does not know the password\n // the administrator chose, so the email says to ask for it or to set\n // their own through \"Forgot password?\".\n const result = await sendEmail({\n to: email,\n subject: designed?.subject || `Your ${siteName} password was changed`,\n text:\n designed?.text ||\n `An administrator of ${siteName} set a new password on your ` +\n 'account and signed you out on every device. To sign back in at ' +\n `${siteBase}, get the new password from them, or choose ` +\n '\"Forgot password?\" on the sign-in page to set your own.\\n\\n' +\n 'If you did not expect this, contact the site owner.',\n ...(designed?.html ? { html: designed.html } : {}),\n fromName: branding.fromName,\n sendingIdentity: await hostSendingIdentity(hostId),\n audience: 'tenant',\n context: 'membership admin password change',\n // Owed to the recipient by their own account: the phishing\n // screen's soft rules never hold it (AGL-3356).\n owedFor: 'account',\n })\n notified = result.sent\n // Cost meter (AGL-1438). Transactional: a security notice.\n if (result.sent) await meterHostEmail(hostId)\n }\n return res.status(200).json({ ok: true, notified })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Password operation failed' })\n }\n}\n"],"names":["holdsSiteKey","siteKeyRefusal","resolveBrandingProfile","validateNewPassword","hostPublicOrigin","isEmailConfigured","sendEmail","renderHostEmailWithTokens","consumePasswordResetSend","firebaseAdmin","getOrgForHost","hostSendingIdentity","isImpersonationSession","meterHostEmail","passwordResetThrottleMessage","resolveOrgPermissions","hashMemberPassword","MEMBER_SESSIONS_VALID_FROM_FIELD","mintPasswordResetToken","isMemberDocumentId","memberCredentialRef","readMemberPasswordHash","retiredCredentialFields","membershipAdminPasswordHandler","req","res","method","status","json","error","authorization","String","headers","idToken","startsWith","slice","length","hostId","body","memberId","action","memberSnapshot","hostSnapshot","app","decoded","auth","verifyIdToken","email_verified","reason","firestore","hostRef","collection","doc","get","host","data","orgPermissions","uid","permissions","manageMembers","memberRef","exists","email","siteBase","cname","subdomain","siteName","branding","catch","org","throttle","actorKey","recipientKey","allowed","setHeader","retryAfterSeconds","token","resetUrl","encodeURIComponent","intro","note","designed","origin","result","to","subject","text","replace","html","fromName","sendingIdentity","audience","context","owedFor","sent","ok","validated","password","batch","set","passwordScrypt","merge","update","Date","now","commit","notified","signInUrl","console"],"mappings":"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"}
@@ -14,7 +14,7 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */
17
- import { type PluginApiHandler } from '@aglyn/aglyn/server';
17
+ import type { PluginApiHandler } from '@aglyn/aglyn/server';
18
18
  /**
19
19
  * Remove a SITE MEMBER from the console (AGL-3308): the profile and its
20
20
  * credential document, in one batch.
@@ -24,10 +24,12 @@ import { type PluginApiHandler } from '@aglyn/aglyn/server';
24
24
  * the site owner removed. The rules therefore refuse a client delete of any
25
25
  * member that has a credential document, and this route is the removal.
26
26
  *
27
- * Who may call it is who the rules let delete a profile before it: a site
28
- * role that writes content (`admin`, `editor` or `author` in the host's
29
- * `memberRoles`). A frozen site is refused by the dispatcher's lockdown gate
30
- * before this runs, the half the rules spelled `hostWritesFrozen`.
27
+ * Who may call it is who the rules let delete a profile before it: whoever
28
+ * holds "Edit site content" here (`site.edit`, AGL-4047), the key the rules'
29
+ * `canWriteHostContent` asks, whose preset is the site Admin, Editor and
30
+ * Author that wrote content before. A frozen site is refused by the
31
+ * dispatcher's lockdown gate before this runs, the half the rules spelled
32
+ * `hostWritesFrozen`.
31
33
  *
32
34
  * Removing the profile also ends every session the member holds:
33
35
  * `readActiveMemberSession` reads a missing profile as signed out.
@@ -13,7 +13,7 @@
13
13
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
- */ import { hostRoleCanWrite } from "@aglyn/aglyn/server";
16
+ */ import { holdsSiteKey, siteKeyRefusal } from "@aglyn/aglyn/app-utils/host-permissions";
17
17
  import { firebaseAdmin, isImpersonationSession } from "@aglyn/tenant-data-admin";
18
18
  import { isMemberDocumentId, memberCredentialRef } from "./member-credentials.js";
19
19
  /**
@@ -25,10 +25,12 @@ import { isMemberDocumentId, memberCredentialRef } from "./member-credentials.js
25
25
  * the site owner removed. The rules therefore refuse a client delete of any
26
26
  * member that has a credential document, and this route is the removal.
27
27
  *
28
- * Who may call it is who the rules let delete a profile before it: a site
29
- * role that writes content (`admin`, `editor` or `author` in the host's
30
- * `memberRoles`). A frozen site is refused by the dispatcher's lockdown gate
31
- * before this runs, the half the rules spelled `hostWritesFrozen`.
28
+ * Who may call it is who the rules let delete a profile before it: whoever
29
+ * holds "Edit site content" here (`site.edit`, AGL-4047), the key the rules'
30
+ * `canWriteHostContent` asks, whose preset is the site Admin, Editor and
31
+ * Author that wrote content before. A frozen site is refused by the
32
+ * dispatcher's lockdown gate before this runs, the half the rules spelled
33
+ * `hostWritesFrozen`.
32
34
  *
33
35
  * Removing the profile also ends every session the member holds:
34
36
  * `readActiveMemberSession` reads a missing profile as signed out.
@@ -61,7 +63,6 @@ import { isMemberDocumentId, memberCredentialRef } from "./member-credentials.js
61
63
  });
62
64
  }
63
65
  try {
64
- var _hostSnapshot_get;
65
66
  const app = firebaseAdmin.app();
66
67
  const decoded = await app.auth().verifyIdToken(idToken);
67
68
  if (!decoded.email_verified && !isImpersonationSession(decoded)) {
@@ -75,10 +76,9 @@ import { isMemberDocumentId, memberCredentialRef } from "./member-credentials.js
75
76
  const firestore = app.firestore();
76
77
  const hostRef = firestore.collection('hosts').doc(hostId);
77
78
  const hostSnapshot = await hostRef.get();
78
- const memberRole = ((_hostSnapshot_get = hostSnapshot.get('memberRoles')) != null ? _hostSnapshot_get : {})[decoded.uid];
79
- if (!hostSnapshot.exists || !hostRoleCanWrite(memberRole)) {
79
+ if (!hostSnapshot.exists || !holdsSiteKey(hostSnapshot, decoded.uid, 'site.edit')) {
80
80
  return res.status(403).json({
81
- error: 'Not permitted'
81
+ error: siteKeyRefusal('site.edit')
82
82
  });
83
83
  }
84
84
  const batch = firestore.batch();
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/membership-admin-remove.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 { hostRoleCanWrite, type PluginApiHandler } from '@aglyn/aglyn/server'\nimport { firebaseAdmin, isImpersonationSession } from '@aglyn/tenant-data-admin'\nimport { isMemberDocumentId, memberCredentialRef } from './member-credentials'\n\n/**\n * Remove a SITE MEMBER from the console (AGL-3308): the profile and its\n * credential document, in one batch.\n *\n * The password hash lives in a collection no client can reach, so a client\n * delete of the profile would leave it behind — a credential for an account\n * the site owner removed. The rules therefore refuse a client delete of any\n * member that has a credential document, and this route is the removal.\n *\n * Who may call it is who the rules let delete a profile before it: a site\n * role that writes content (`admin`, `editor` or `author` in the host's\n * `memberRoles`). A frozen site is refused by the dispatcher's lockdown gate\n * before this runs, the half the rules spelled `hostWritesFrozen`.\n *\n * Removing the profile also ends every session the member holds:\n * `readActiveMemberSession` reads a missing profile as signed out.\n *\n * Idempotent. A member already gone answers 200, and a credential document\n * left without its profile is cleared on the way.\n */\nexport const membershipAdminRemoveHandler: PluginApiHandler = async (\n req,\n res,\n) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const authorization = String(req.headers['authorization'] ?? '')\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : ''\n if (!idToken) return res.status(401).json({ error: 'Unauthenticated' })\n\n const hostId = String(req.body?.hostId ?? '')\n const memberId = String(req.body?.memberId ?? '')\n if (!hostId || !memberId) {\n return res.status(400).json({ error: 'Missing hostId or memberId' })\n }\n if (!isMemberDocumentId(memberId)) {\n return res.status(400).json({ error: 'Unknown member' })\n }\n\n try {\n const app = firebaseAdmin.app()\n const decoded = await app.auth().verifyIdToken(idToken)\n if (!decoded.email_verified && !isImpersonationSession(decoded)) {\n // The shape of emailUnverifiedResponse (AGL-479), by hand for the same\n // reason as membership-admin-password: this is a (req, res) handler.\n return res.status(403).json({\n error: 'Verify your email to continue',\n reason: 'email-unverified',\n })\n }\n const firestore = app.firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const hostSnapshot = await hostRef.get()\n const memberRole = (hostSnapshot.get('memberRoles') ?? {})[decoded.uid]\n if (!hostSnapshot.exists || !hostRoleCanWrite(memberRole)) {\n return res.status(403).json({ error: 'Not permitted' })\n }\n\n const batch = firestore.batch()\n batch.delete(hostRef.collection('siteMembers').doc(memberId))\n batch.delete(memberCredentialRef(hostRef, memberId))\n await batch.commit()\n return res.status(200).json({ ok: true })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Could not remove the member' })\n }\n}\n"],"names":["hostRoleCanWrite","firebaseAdmin","isImpersonationSession","isMemberDocumentId","memberCredentialRef","membershipAdminRemoveHandler","req","res","method","status","json","error","authorization","String","headers","idToken","startsWith","slice","length","hostId","body","memberId","hostSnapshot","app","decoded","auth","verifyIdToken","email_verified","reason","firestore","hostRef","collection","doc","get","memberRole","uid","exists","batch","delete","commit","ok","console"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,gBAAgB,QAA+B,sBAAqB;AAC7E,SAASC,aAAa,EAAEC,sBAAsB,QAAQ,2BAA0B;AAChF,SAASC,kBAAkB,EAAEC,mBAAmB,QAAQ,0BAAsB;AAE9E;;;;;;;;;;;;;;;;;;;CAmBC,GACD,OAAO,MAAMC,+BAAiD,OAC5DC,KACAC;QAK6BD;QAMPA,WACEA;IAVxB,IAAIA,IAAIE,MAAM,KAAK,QAAQ;QACzB,OAAOD,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAAqB;IAC5D;IACA,MAAMC,gBAAgBC,QAAOP,6BAAAA,IAAIQ,OAAO,CAAC,gBAAgB,YAA5BR,6BAAgC;IAC7D,MAAMS,UAAUH,cAAcI,UAAU,CAAC,aACrCJ,cAAcK,KAAK,CAAC,UAAUC,MAAM,IACpC;IACJ,IAAI,CAACH,SAAS,OAAOR,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;QAAEC,OAAO;IAAkB;IAErE,MAAMQ,SAASN,gBAAOP,YAAAA,IAAIc,IAAI,qBAARd,UAAUa,MAAM,mBAAI;IAC1C,MAAME,WAAWR,iBAAOP,aAAAA,IAAIc,IAAI,qBAARd,WAAUe,QAAQ,oBAAI;IAC9C,IAAI,CAACF,UAAU,CAACE,UAAU;QACxB,OAAOd,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAA6B;IACpE;IACA,IAAI,CAACR,mBAAmBkB,WAAW;QACjC,OAAOd,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAAiB;IACxD;IAEA,IAAI;YAckBW;QAbpB,MAAMC,MAAMtB,cAAcsB,GAAG;QAC7B,MAAMC,UAAU,MAAMD,IAAIE,IAAI,GAAGC,aAAa,CAACX;QAC/C,IAAI,CAACS,QAAQG,cAAc,IAAI,CAACzB,uBAAuBsB,UAAU;YAC/D,uEAAuE;YACvE,qEAAqE;YACrE,OAAOjB,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAC1BC,OAAO;gBACPiB,QAAQ;YACV;QACF;QACA,MAAMC,YAAYN,IAAIM,SAAS;QAC/B,MAAMC,UAAUD,UAAUE,UAAU,CAAC,SAASC,GAAG,CAACb;QAClD,MAAMG,eAAe,MAAMQ,QAAQG,GAAG;QACtC,MAAMC,aAAa,EAACZ,oBAAAA,aAAaW,GAAG,CAAC,0BAAjBX,oBAAmC,CAAC,EAAE,CAACE,QAAQW,GAAG,CAAC;QACvE,IAAI,CAACb,aAAac,MAAM,IAAI,CAACpC,iBAAiBkC,aAAa;YACzD,OAAO3B,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAO;YAAgB;QACvD;QAEA,MAAM0B,QAAQR,UAAUQ,KAAK;QAC7BA,MAAMC,MAAM,CAACR,QAAQC,UAAU,CAAC,eAAeC,GAAG,CAACX;QACnDgB,MAAMC,MAAM,CAAClC,oBAAoB0B,SAAST;QAC1C,MAAMgB,MAAME,MAAM;QAClB,OAAOhC,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAE8B,IAAI;QAAK;IACzC,EAAE,OAAO7B,OAAO;QACd8B,QAAQ9B,KAAK,CAACA;QACd,OAAOJ,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAA8B;IACrE;AACF,EAAC"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/membership-admin-remove.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 { holdsSiteKey, siteKeyRefusal } from '@aglyn/aglyn/app-utils/host-permissions'\nimport type { PluginApiHandler } from '@aglyn/aglyn/server'\nimport { firebaseAdmin, isImpersonationSession } from '@aglyn/tenant-data-admin'\nimport { isMemberDocumentId, memberCredentialRef } from './member-credentials'\n\n/**\n * Remove a SITE MEMBER from the console (AGL-3308): the profile and its\n * credential document, in one batch.\n *\n * The password hash lives in a collection no client can reach, so a client\n * delete of the profile would leave it behind — a credential for an account\n * the site owner removed. The rules therefore refuse a client delete of any\n * member that has a credential document, and this route is the removal.\n *\n * Who may call it is who the rules let delete a profile before it: whoever\n * holds \"Edit site content\" here (`site.edit`, AGL-4047), the key the rules'\n * `canWriteHostContent` asks, whose preset is the site Admin, Editor and\n * Author that wrote content before. A frozen site is refused by the\n * dispatcher's lockdown gate before this runs, the half the rules spelled\n * `hostWritesFrozen`.\n *\n * Removing the profile also ends every session the member holds:\n * `readActiveMemberSession` reads a missing profile as signed out.\n *\n * Idempotent. A member already gone answers 200, and a credential document\n * left without its profile is cleared on the way.\n */\nexport const membershipAdminRemoveHandler: PluginApiHandler = async (\n req,\n res,\n) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const authorization = String(req.headers['authorization'] ?? '')\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : ''\n if (!idToken) return res.status(401).json({ error: 'Unauthenticated' })\n\n const hostId = String(req.body?.hostId ?? '')\n const memberId = String(req.body?.memberId ?? '')\n if (!hostId || !memberId) {\n return res.status(400).json({ error: 'Missing hostId or memberId' })\n }\n if (!isMemberDocumentId(memberId)) {\n return res.status(400).json({ error: 'Unknown member' })\n }\n\n try {\n const app = firebaseAdmin.app()\n const decoded = await app.auth().verifyIdToken(idToken)\n if (!decoded.email_verified && !isImpersonationSession(decoded)) {\n // The shape of emailUnverifiedResponse (AGL-479), by hand for the same\n // reason as membership-admin-password: this is a (req, res) handler.\n return res.status(403).json({\n error: 'Verify your email to continue',\n reason: 'email-unverified',\n })\n }\n const firestore = app.firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const hostSnapshot = await hostRef.get()\n if (!hostSnapshot.exists || !holdsSiteKey(hostSnapshot, decoded.uid, 'site.edit')) {\n return res.status(403).json({ error: siteKeyRefusal('site.edit') })\n }\n\n const batch = firestore.batch()\n batch.delete(hostRef.collection('siteMembers').doc(memberId))\n batch.delete(memberCredentialRef(hostRef, memberId))\n await batch.commit()\n return res.status(200).json({ ok: true })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Could not remove the member' })\n }\n}\n"],"names":["holdsSiteKey","siteKeyRefusal","firebaseAdmin","isImpersonationSession","isMemberDocumentId","memberCredentialRef","membershipAdminRemoveHandler","req","res","method","status","json","error","authorization","String","headers","idToken","startsWith","slice","length","hostId","body","memberId","app","decoded","auth","verifyIdToken","email_verified","reason","firestore","hostRef","collection","doc","hostSnapshot","get","exists","uid","batch","delete","commit","ok","console"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,YAAY,EAAEC,cAAc,QAAQ,0CAAyC;AAEtF,SAASC,aAAa,EAAEC,sBAAsB,QAAQ,2BAA0B;AAChF,SAASC,kBAAkB,EAAEC,mBAAmB,QAAQ,0BAAsB;AAE9E;;;;;;;;;;;;;;;;;;;;;CAqBC,GACD,OAAO,MAAMC,+BAAiD,OAC5DC,KACAC;QAK6BD;QAMPA,WACEA;IAVxB,IAAIA,IAAIE,MAAM,KAAK,QAAQ;QACzB,OAAOD,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAAqB;IAC5D;IACA,MAAMC,gBAAgBC,QAAOP,6BAAAA,IAAIQ,OAAO,CAAC,gBAAgB,YAA5BR,6BAAgC;IAC7D,MAAMS,UAAUH,cAAcI,UAAU,CAAC,aACrCJ,cAAcK,KAAK,CAAC,UAAUC,MAAM,IACpC;IACJ,IAAI,CAACH,SAAS,OAAOR,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;QAAEC,OAAO;IAAkB;IAErE,MAAMQ,SAASN,gBAAOP,YAAAA,IAAIc,IAAI,qBAARd,UAAUa,MAAM,mBAAI;IAC1C,MAAME,WAAWR,iBAAOP,aAAAA,IAAIc,IAAI,qBAARd,WAAUe,QAAQ,oBAAI;IAC9C,IAAI,CAACF,UAAU,CAACE,UAAU;QACxB,OAAOd,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAA6B;IACpE;IACA,IAAI,CAACR,mBAAmBkB,WAAW;QACjC,OAAOd,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAAiB;IACxD;IAEA,IAAI;QACF,MAAMW,MAAMrB,cAAcqB,GAAG;QAC7B,MAAMC,UAAU,MAAMD,IAAIE,IAAI,GAAGC,aAAa,CAACV;QAC/C,IAAI,CAACQ,QAAQG,cAAc,IAAI,CAACxB,uBAAuBqB,UAAU;YAC/D,uEAAuE;YACvE,qEAAqE;YACrE,OAAOhB,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAC1BC,OAAO;gBACPgB,QAAQ;YACV;QACF;QACA,MAAMC,YAAYN,IAAIM,SAAS;QAC/B,MAAMC,UAAUD,UAAUE,UAAU,CAAC,SAASC,GAAG,CAACZ;QAClD,MAAMa,eAAe,MAAMH,QAAQI,GAAG;QACtC,IAAI,CAACD,aAAaE,MAAM,IAAI,CAACnC,aAAaiC,cAAcT,QAAQY,GAAG,EAAE,cAAc;YACjF,OAAO5B,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAOX,eAAe;YAAa;QACnE;QAEA,MAAMoC,QAAQR,UAAUQ,KAAK;QAC7BA,MAAMC,MAAM,CAACR,QAAQC,UAAU,CAAC,eAAeC,GAAG,CAACV;QACnDe,MAAMC,MAAM,CAACjC,oBAAoByB,SAASR;QAC1C,MAAMe,MAAME,MAAM;QAClB,OAAO/B,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAE6B,IAAI;QAAK;IACzC,EAAE,OAAO5B,OAAO;QACd6B,QAAQ7B,KAAK,CAACA;QACd,OAAOJ,IAAIE,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAA8B;IACrE;AACF,EAAC"}
@@ -15,6 +15,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
15
15
  * See the License for the specific language governing permissions and
16
16
  * limitations under the License.
17
17
  */ import * as Aglyn from "@aglyn/aglyn/server";
18
+ import { withPosHardwareReceiptNotice } from "../model/pos-hardware-terms.js";
18
19
  import { pluginSmsMessaging } from "@aglyn/aglyn/plugin-manager/plugin-sms-messaging";
19
20
  import { resolveSiteTimeZone } from "@aglyn/aglyn/app-utils/collection-entry-date";
20
21
  import { resolveHostToken } from "@aglyn/aglyn/app-utils/host-tokens";
@@ -460,7 +461,8 @@ function emailOf(value) {
460
461
  businessName: String((_resolveHostToken = resolveHostToken('businessName', store.host)) != null ? _resolveHostToken : ''),
461
462
  currency: store.currency,
462
463
  statusUrl: statusLink(store, ref),
463
- receiptFooter: String((_store_settings_receiptFooter = store.settings['receiptFooter']) != null ? _store_settings_receiptFooter : ''),
464
+ // A sale of card reader hardware states the terms it is sold under (AGL-4038).
465
+ receiptFooter: withPosHardwareReceiptNotice(String((_store_settings_receiptFooter = store.settings['receiptFooter']) != null ? _store_settings_receiptFooter : ''), order),
464
466
  downloadLinks: event === 'receipt' ? downloadLinksFor(store, ref, order) : undefined,
465
467
  options
466
468
  });
@@ -591,7 +593,8 @@ function emailOf(value) {
591
593
  businessName: String((_resolveHostToken = resolveHostToken('businessName', store.host)) != null ? _resolveHostToken : ''),
592
594
  currency: store.currency,
593
595
  statusUrl: statusLink(store, ref),
594
- receiptFooter: String((_store_settings_receiptFooter = store.settings['receiptFooter']) != null ? _store_settings_receiptFooter : ''),
596
+ // A sale of card reader hardware states the terms it is sold under (AGL-4038).
597
+ receiptFooter: withPosHardwareReceiptNotice(String((_store_settings_receiptFooter = store.settings['receiptFooter']) != null ? _store_settings_receiptFooter : ''), order),
595
598
  downloadLinks: downloadLinksFor(store, ref, order)
596
599
  });
597
600
  if (!message) return {