@openlup/platform 0.17.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (525) hide show
  1. package/CHANGELOG.md +143 -0
  2. package/README.md +299 -6
  3. package/dist/catalog/activeSkuSelection.d.ts +2 -0
  4. package/dist/catalog/activeSkuSelection.js +20 -0
  5. package/dist/catalog/adminBindings.d.ts +72 -0
  6. package/dist/catalog/adminBindings.js +1 -0
  7. package/dist/catalog/adminBundleHandler.d.ts +26 -0
  8. package/dist/catalog/adminBundleHandler.js +111 -0
  9. package/dist/catalog/adminBundleReadHandler.d.ts +21 -0
  10. package/dist/catalog/adminBundleReadHandler.js +185 -0
  11. package/dist/catalog/adminBundleWritePort.d.ts +85 -0
  12. package/dist/catalog/adminBundleWritePort.js +1 -0
  13. package/dist/catalog/bundleAvailability.d.ts +63 -0
  14. package/dist/catalog/bundleAvailability.js +78 -0
  15. package/dist/catalog/bundleAvailabilityService.d.ts +65 -0
  16. package/dist/catalog/bundleAvailabilityService.js +89 -0
  17. package/dist/catalog/bundleBindings.d.ts +33 -0
  18. package/dist/catalog/bundleBindings.js +1 -0
  19. package/dist/catalog/bundleCatalogQueries.d.ts +28 -0
  20. package/dist/catalog/bundleCatalogQueries.js +84 -0
  21. package/dist/catalog/bundleCatalogReadPort.d.ts +173 -0
  22. package/dist/catalog/bundleCatalogReadPort.js +36 -0
  23. package/dist/catalog/bundleCatalogStore.d.ts +5 -0
  24. package/dist/catalog/bundleCatalogStore.js +156 -0
  25. package/dist/catalog/bundlePriceSelection.d.ts +13 -0
  26. package/dist/catalog/bundlePriceSelection.js +26 -0
  27. package/dist/catalog/bundleRuleCodes.d.ts +41 -0
  28. package/dist/catalog/bundleRuleCodes.js +110 -0
  29. package/dist/catalog/bundleRules.d.ts +69 -0
  30. package/dist/catalog/bundleRules.js +102 -0
  31. package/dist/catalog/catalogDraftStore.d.ts +4 -0
  32. package/dist/catalog/catalogDraftStore.js +63 -0
  33. package/dist/catalog/catalogSkuEnvelope.d.ts +7 -0
  34. package/dist/catalog/catalogSkuEnvelope.js +142 -0
  35. package/dist/catalog/catalogSkuEnvelopeMapper.d.ts +38 -0
  36. package/dist/catalog/catalogSkuEnvelopeMapper.js +88 -0
  37. package/dist/catalog/contracts/bundleAdmin.d.ts +143 -0
  38. package/dist/catalog/contracts/bundleAdmin.js +1 -0
  39. package/dist/catalog/contracts/bundleAvailability.d.ts +61 -0
  40. package/dist/catalog/contracts/bundleAvailability.js +1 -0
  41. package/dist/catalog/contracts/bundleRead.d.ts +241 -0
  42. package/dist/catalog/contracts/bundleRead.js +1 -0
  43. package/dist/catalog/contracts/draft.d.ts +79 -0
  44. package/dist/catalog/contracts/draft.js +47 -0
  45. package/dist/catalog/contracts/foundation.d.ts +78 -0
  46. package/dist/catalog/contracts/foundation.js +53 -0
  47. package/dist/catalog/contracts/index.d.ts +14 -0
  48. package/dist/catalog/contracts/index.js +6 -0
  49. package/dist/catalog/contracts/lifecycle.d.ts +77 -0
  50. package/dist/catalog/contracts/lifecycle.js +1 -0
  51. package/dist/catalog/contracts/productMediaContracts.d.ts +77 -0
  52. package/dist/catalog/contracts/productMediaContracts.js +149 -0
  53. package/dist/catalog/contracts/productTypes.d.ts +54 -0
  54. package/dist/catalog/contracts/productTypes.js +16 -0
  55. package/dist/catalog/contracts/resolveProductMedia.d.ts +3 -0
  56. package/dist/catalog/contracts/resolveProductMedia.js +78 -0
  57. package/dist/catalog/contracts/sellable.d.ts +23 -0
  58. package/dist/catalog/contracts/sellable.js +1 -0
  59. package/dist/catalog/draftHandler.d.ts +25 -0
  60. package/dist/catalog/draftHandler.js +118 -0
  61. package/dist/catalog/draftPorts.d.ts +18 -0
  62. package/dist/catalog/draftPorts.js +8 -0
  63. package/dist/catalog/draftSchemas.d.ts +423 -0
  64. package/dist/catalog/draftSchemas.js +89 -0
  65. package/dist/catalog/draftValidation.d.ts +21 -0
  66. package/dist/catalog/draftValidation.js +125 -0
  67. package/dist/catalog/effectiveBundleCatalogQueries.d.ts +8 -0
  68. package/dist/catalog/effectiveBundleCatalogQueries.js +79 -0
  69. package/dist/catalog/effectiveBundleCatalogStore.d.ts +4 -0
  70. package/dist/catalog/effectiveBundleCatalogStore.js +156 -0
  71. package/dist/catalog/foundationSchemas.d.ts +98 -0
  72. package/dist/catalog/foundationSchemas.js +110 -0
  73. package/dist/catalog/index.d.ts +44 -0
  74. package/dist/catalog/index.js +26 -0
  75. package/dist/catalog/mediaSourcePack.d.ts +51 -0
  76. package/dist/catalog/mediaSourcePack.js +247 -0
  77. package/dist/catalog/productTypeSchemas.d.ts +46 -0
  78. package/dist/catalog/productTypeSchemas.js +72 -0
  79. package/dist/catalog/publicationPorts.d.ts +42 -0
  80. package/dist/catalog/publicationPorts.js +1 -0
  81. package/dist/catalog/readPorts.d.ts +51 -0
  82. package/dist/catalog/readPorts.js +1 -0
  83. package/dist/catalog/referenceCatalogHandler.d.ts +7 -0
  84. package/dist/catalog/referenceCatalogHandler.js +27 -0
  85. package/dist/catalog/resolvedSkuProjection.d.ts +88 -0
  86. package/dist/catalog/resolvedSkuProjection.js +60 -0
  87. package/dist/catalog/sellableBundleHandler.d.ts +46 -0
  88. package/dist/catalog/sellableBundleHandler.js +84 -0
  89. package/dist/catalog/sellableSchemas.d.ts +46 -0
  90. package/dist/catalog/sellableSchemas.js +27 -0
  91. package/dist/catalog/sql.d.ts +46 -0
  92. package/dist/catalog/sql.js +13 -0
  93. package/dist/catalog/validation.d.ts +2 -0
  94. package/dist/catalog/validation.js +3 -0
  95. package/dist/communications/contracts/index.d.ts +1 -0
  96. package/dist/communications/contracts/marketingSync.d.ts +194 -0
  97. package/dist/communications/contracts/marketingSync.js +1 -0
  98. package/dist/communications/index.d.ts +1 -0
  99. package/dist/communications/klaviyo/consentPush.d.ts +51 -0
  100. package/dist/communications/klaviyo/consentPush.js +19 -0
  101. package/dist/communications/klaviyo/index.d.ts +110 -0
  102. package/dist/communications/klaviyo/index.js +860 -0
  103. package/dist/communications/newsletterIntegrationContracts.d.ts +3 -3
  104. package/dist/communications/resend/index.d.ts +40 -0
  105. package/dist/communications/resend/index.js +310 -0
  106. package/dist/composition/catalogContribution.d.ts +18 -0
  107. package/dist/composition/catalogContribution.js +21 -0
  108. package/dist/composition/index.d.ts +3 -0
  109. package/dist/composition/index.js +3 -0
  110. package/dist/composition/inventoryContribution.d.ts +16 -0
  111. package/dist/composition/inventoryContribution.js +17 -0
  112. package/dist/composition/shippingContribution.d.ts +16 -0
  113. package/dist/composition/shippingContribution.js +17 -0
  114. package/dist/identity/customerContracts.d.ts +2 -2
  115. package/dist/inventory/adminInventory.d.ts +13 -0
  116. package/dist/inventory/adminInventory.js +214 -0
  117. package/dist/inventory/bindings.d.ts +35 -0
  118. package/dist/inventory/bindings.js +45 -0
  119. package/dist/inventory/contracts/forecast.d.ts +86 -0
  120. package/dist/inventory/contracts/forecast.js +1 -0
  121. package/dist/inventory/contracts/index.d.ts +4 -0
  122. package/dist/inventory/contracts/index.js +1 -0
  123. package/dist/inventory/contracts/inventory.d.ts +157 -0
  124. package/dist/inventory/contracts/inventory.js +1 -0
  125. package/dist/inventory/contracts/newPurchaseStock.d.ts +20 -0
  126. package/dist/inventory/contracts/newPurchaseStock.js +1 -0
  127. package/dist/inventory/contracts/reservation.d.ts +18 -0
  128. package/dist/inventory/contracts/reservation.js +1 -0
  129. package/dist/inventory/forecast.d.ts +24 -0
  130. package/dist/inventory/forecast.js +200 -0
  131. package/dist/inventory/index.d.ts +14 -0
  132. package/dist/inventory/index.js +11 -0
  133. package/dist/inventory/inventoryHandlers.d.ts +53 -0
  134. package/dist/inventory/inventoryHandlers.js +151 -0
  135. package/dist/inventory/newPurchaseStockAvailability.d.ts +47 -0
  136. package/dist/inventory/newPurchaseStockAvailability.js +75 -0
  137. package/dist/inventory/ports.d.ts +21 -0
  138. package/dist/inventory/ports.js +16 -0
  139. package/dist/inventory/renewalReservation.d.ts +27 -0
  140. package/dist/inventory/renewalReservation.js +61 -0
  141. package/dist/inventory/reservationPolicy.d.ts +6 -0
  142. package/dist/inventory/reservationPolicy.js +19 -0
  143. package/dist/inventory/schemas.d.ts +137 -0
  144. package/dist/inventory/schemas.js +112 -0
  145. package/dist/inventory/sql.d.ts +14 -0
  146. package/dist/inventory/sql.js +5 -0
  147. package/dist/inventory/subscriptionForecastCalculation.d.ts +4 -0
  148. package/dist/inventory/subscriptionForecastCalculation.js +133 -0
  149. package/dist/pricing/acquisitionFinancialPolicy.d.ts +26 -0
  150. package/dist/pricing/acquisitionFinancialPolicy.js +1 -0
  151. package/dist/pricing/acquisitionOfferPricing.d.ts +27 -0
  152. package/dist/pricing/acquisitionOfferPricing.js +25 -0
  153. package/dist/pricing/acquisitionOfferPricingHandler.d.ts +20 -0
  154. package/dist/pricing/acquisitionOfferPricingHandler.js +105 -0
  155. package/dist/pricing/acquisitionOfferPricingSchemas.d.ts +31 -0
  156. package/dist/pricing/acquisitionOfferPricingSchemas.js +33 -0
  157. package/dist/pricing/acquisitionQuoteBinding.d.ts +12 -0
  158. package/dist/pricing/acquisitionQuoteBinding.js +12 -0
  159. package/dist/pricing/acquisitionReadiness.d.ts +31 -0
  160. package/dist/pricing/acquisitionReadiness.js +117 -0
  161. package/dist/pricing/acquisitionTerms.d.ts +19 -0
  162. package/dist/pricing/acquisitionTerms.js +126 -0
  163. package/dist/pricing/acquisitionTermsSchemas.d.ts +59 -0
  164. package/dist/pricing/acquisitionTermsSchemas.js +42 -0
  165. package/dist/pricing/catalogFactsProvenance.d.ts +33 -0
  166. package/dist/pricing/catalogFactsProvenance.js +64 -0
  167. package/dist/pricing/catalogFactsSchemas.d.ts +45 -0
  168. package/dist/pricing/catalogFactsSchemas.js +58 -0
  169. package/dist/pricing/commerceOfferPricing.d.ts +25 -0
  170. package/dist/pricing/commerceOfferPricing.js +139 -0
  171. package/dist/pricing/commerceOfferPricingCache.d.ts +10 -0
  172. package/dist/pricing/commerceOfferPricingCache.js +44 -0
  173. package/dist/pricing/commerceOfferPricingHandler.d.ts +21 -0
  174. package/dist/pricing/commerceOfferPricingHandler.js +78 -0
  175. package/dist/pricing/commercePriceAuthority.d.ts +28 -0
  176. package/dist/pricing/commercePriceAuthority.js +140 -0
  177. package/dist/pricing/commercePricingPolicyErrors.d.ts +40 -0
  178. package/dist/pricing/commercePricingPolicyErrors.js +67 -0
  179. package/dist/pricing/commerceQuoteBatchHandler.d.ts +37 -0
  180. package/dist/pricing/commerceQuoteBatchHandler.js +83 -0
  181. package/dist/pricing/commerceQuoteCatalogReadPort.d.ts +27 -0
  182. package/dist/pricing/commerceQuoteCatalogReadPort.js +8 -0
  183. package/dist/pricing/commerceQuoteHandler.d.ts +35 -0
  184. package/dist/pricing/commerceQuoteHandler.js +122 -0
  185. package/dist/pricing/commerceQuoteSnapshotExpect.d.ts +9 -0
  186. package/dist/pricing/commerceQuoteSnapshotExpect.js +22 -0
  187. package/dist/pricing/contracts/acquisitionTerms.d.ts +32 -0
  188. package/dist/pricing/contracts/acquisitionTerms.js +2 -0
  189. package/dist/pricing/contracts/authority.d.ts +27 -0
  190. package/dist/pricing/contracts/authority.js +1 -0
  191. package/dist/pricing/contracts/evidence.d.ts +33 -0
  192. package/dist/pricing/contracts/evidence.js +1 -0
  193. package/dist/pricing/contracts/index.d.ts +17 -0
  194. package/dist/pricing/contracts/index.js +7 -0
  195. package/dist/pricing/contracts/money.d.ts +27 -0
  196. package/dist/pricing/contracts/money.js +1 -0
  197. package/dist/pricing/contracts/offer.d.ts +52 -0
  198. package/dist/pricing/contracts/offer.js +4 -0
  199. package/dist/pricing/contracts/offerPolicy.d.ts +16 -0
  200. package/dist/pricing/contracts/offerPolicy.js +5 -0
  201. package/dist/pricing/contracts/policy.d.ts +22 -0
  202. package/dist/pricing/contracts/policy.js +3 -0
  203. package/dist/pricing/contracts/provenance.d.ts +24 -0
  204. package/dist/pricing/contracts/provenance.js +2 -0
  205. package/dist/pricing/contracts/quote.d.ts +94 -0
  206. package/dist/pricing/contracts/quote.js +1 -0
  207. package/dist/pricing/dbBackedCommerceQuoteHelpers.d.ts +78 -0
  208. package/dist/pricing/dbBackedCommerceQuoteHelpers.js +146 -0
  209. package/dist/pricing/dbBackedCommerceQuotePort.d.ts +74 -0
  210. package/dist/pricing/dbBackedCommerceQuotePort.js +223 -0
  211. package/dist/pricing/dbBackedCommerceQuoteSnapshotVerifier.d.ts +4 -0
  212. package/dist/pricing/dbBackedCommerceQuoteSnapshotVerifier.js +25 -0
  213. package/dist/pricing/index.d.ts +52 -0
  214. package/dist/pricing/index.js +36 -0
  215. package/dist/pricing/offerPolicyReadiness.d.ts +21 -0
  216. package/dist/pricing/offerPolicyReadiness.js +79 -0
  217. package/dist/pricing/offerPolicySchemas.d.ts +25 -0
  218. package/dist/pricing/offerPolicySchemas.js +15 -0
  219. package/dist/pricing/offerPricingSchemas.d.ts +315 -0
  220. package/dist/pricing/offerPricingSchemas.js +65 -0
  221. package/dist/pricing/ports.d.ts +13 -0
  222. package/dist/pricing/ports.js +9 -0
  223. package/dist/pricing/pricingPolicyToken.d.ts +39 -0
  224. package/dist/pricing/pricingPolicyToken.js +84 -0
  225. package/dist/pricing/quoteAcquisitionTerms.d.ts +2 -0
  226. package/dist/pricing/quoteAcquisitionTerms.js +4 -0
  227. package/dist/pricing/quoteCodeRejectionDetails.d.ts +20 -0
  228. package/dist/pricing/quoteCodeRejectionDetails.js +22 -0
  229. package/dist/pricing/quotePorts.d.ts +30 -0
  230. package/dist/pricing/quotePorts.js +20 -0
  231. package/dist/pricing/quotePrimitives.d.ts +13 -0
  232. package/dist/pricing/quotePrimitives.js +18 -0
  233. package/dist/pricing/quotePromotionEvidence.d.ts +39 -0
  234. package/dist/pricing/quotePromotionEvidence.js +73 -0
  235. package/dist/pricing/quoteSchemas.d.ts +1176 -0
  236. package/dist/pricing/quoteSchemas.js +196 -0
  237. package/dist/pricing/quoteShippingPromotions.d.ts +20 -0
  238. package/dist/pricing/quoteShippingPromotions.js +1 -0
  239. package/dist/pricing/subscriptionPricePolicy.d.ts +52 -0
  240. package/dist/pricing/subscriptionPricePolicy.js +96 -0
  241. package/dist/pricing/taxProfileGuard.d.ts +2 -0
  242. package/dist/pricing/taxProfileGuard.js +24 -0
  243. package/dist/pricing/validation.d.ts +2 -0
  244. package/dist/pricing/validation.js +3 -0
  245. package/dist/promotions/adminBindings.d.ts +72 -0
  246. package/dist/promotions/adminBindings.js +1 -0
  247. package/dist/promotions/adminPromotionCodesContracts.d.ts +498 -0
  248. package/dist/promotions/adminPromotionCodesContracts.js +211 -0
  249. package/dist/promotions/adminPromotionCodesDataPort.d.ts +47 -0
  250. package/dist/promotions/adminPromotionCodesDataPort.js +1 -0
  251. package/dist/promotions/adminPromotionCodesHandler.d.ts +14 -0
  252. package/dist/promotions/adminPromotionCodesHandler.js +194 -0
  253. package/dist/promotions/adminPromotionsContracts.d.ts +235 -0
  254. package/dist/promotions/adminPromotionsContracts.js +99 -0
  255. package/dist/promotions/adminPromotionsDataPort.d.ts +42 -0
  256. package/dist/promotions/adminPromotionsDataPort.js +1 -0
  257. package/dist/promotions/adminPromotionsHandler.d.ts +16 -0
  258. package/dist/promotions/adminPromotionsHandler.js +47 -0
  259. package/dist/promotions/commerceQuoteReadPreparation.d.ts +3 -0
  260. package/dist/promotions/commerceQuoteReadPreparation.js +27 -0
  261. package/dist/promotions/contracts/index.d.ts +387 -0
  262. package/dist/promotions/contracts/index.js +1 -0
  263. package/dist/promotions/index.d.ts +25 -0
  264. package/dist/promotions/index.js +22 -0
  265. package/dist/promotions/promoDataPort.d.ts +47 -0
  266. package/dist/promotions/promoDataPort.js +14 -0
  267. package/dist/promotions/promotionAcceptanceTelemetry.d.ts +29 -0
  268. package/dist/promotions/promotionAcceptanceTelemetry.js +45 -0
  269. package/dist/promotions/promotionBenefitDefinition.d.ts +37 -0
  270. package/dist/promotions/promotionBenefitDefinition.js +33 -0
  271. package/dist/promotions/promotionClaimSweepPort.d.ts +14 -0
  272. package/dist/promotions/promotionClaimSweepPort.js +8 -0
  273. package/dist/promotions/promotionClaims.d.ts +5 -0
  274. package/dist/promotions/promotionClaims.js +57 -0
  275. package/dist/promotions/promotionCodePreview.d.ts +11 -0
  276. package/dist/promotions/promotionCodePreview.js +154 -0
  277. package/dist/promotions/promotionCodeQuotePort.d.ts +25 -0
  278. package/dist/promotions/promotionCodeQuotePort.js +1 -0
  279. package/dist/promotions/promotionQuoteAcceptance.d.ts +70 -0
  280. package/dist/promotions/promotionQuoteAcceptance.js +202 -0
  281. package/dist/promotions/promotionQuoteBinding.d.ts +73 -0
  282. package/dist/promotions/promotionQuoteBinding.js +87 -0
  283. package/dist/promotions/quoteAcquisitionBinding.d.ts +6 -0
  284. package/dist/promotions/quoteAcquisitionBinding.js +1 -0
  285. package/dist/promotions/quoteAutomaticPromotions.d.ts +26 -0
  286. package/dist/promotions/quoteAutomaticPromotions.js +60 -0
  287. package/dist/promotions/quotePromoDiscounts.d.ts +36 -0
  288. package/dist/promotions/quotePromoDiscounts.js +146 -0
  289. package/dist/promotions/quotePromotionCodeAdjustments.d.ts +27 -0
  290. package/dist/promotions/quotePromotionCodeAdjustments.js +223 -0
  291. package/dist/promotions/quotePromotionV2.d.ts +30 -0
  292. package/dist/promotions/quotePromotionV2.js +186 -0
  293. package/dist/promotions/quoteShippingPolicy.d.ts +23 -0
  294. package/dist/promotions/quoteShippingPolicy.js +47 -0
  295. package/dist/promotions/sql.d.ts +14 -0
  296. package/dist/promotions/sql.js +5 -0
  297. package/dist/promotions/validation.d.ts +2 -0
  298. package/dist/promotions/validation.js +3 -0
  299. package/dist/shipping/addressCanonLookupHandler.d.ts +7 -0
  300. package/dist/shipping/addressCanonLookupHandler.js +105 -0
  301. package/dist/shipping/addressCanonical.d.ts +2 -0
  302. package/dist/shipping/addressCanonical.js +26 -0
  303. package/dist/shipping/addressPorts.d.ts +39 -0
  304. package/dist/shipping/addressPorts.js +1 -0
  305. package/dist/shipping/addressSchemas.d.ts +30 -0
  306. package/dist/shipping/addressSchemas.js +1 -0
  307. package/dist/shipping/adminShippingRateHandler.d.ts +4 -0
  308. package/dist/shipping/adminShippingRateHandler.js +64 -0
  309. package/dist/shipping/bindings.d.ts +57 -0
  310. package/dist/shipping/bindings.js +46 -0
  311. package/dist/shipping/contracts/address.d.ts +144 -0
  312. package/dist/shipping/contracts/address.js +1 -0
  313. package/dist/shipping/contracts/delivery.d.ts +110 -0
  314. package/dist/shipping/contracts/delivery.js +1 -0
  315. package/dist/shipping/contracts/index.d.ts +4 -0
  316. package/dist/shipping/contracts/index.js +1 -0
  317. package/dist/shipping/contracts/shippingRate.d.ts +11 -0
  318. package/dist/shipping/contracts/shippingRate.js +1 -0
  319. package/dist/shipping/contracts/threshold.d.ts +22 -0
  320. package/dist/shipping/contracts/threshold.js +1 -0
  321. package/dist/shipping/customerAddressesHandler.d.ts +5 -0
  322. package/dist/shipping/customerAddressesHandler.js +80 -0
  323. package/dist/shipping/customerSelfServiceAddressModels.d.ts +27 -0
  324. package/dist/shipping/customerSelfServiceAddressModels.js +101 -0
  325. package/dist/shipping/deliverySelectionHandler.d.ts +10 -0
  326. package/dist/shipping/deliverySelectionHandler.js +77 -0
  327. package/dist/shipping/deliverySelectionPort.d.ts +8 -0
  328. package/dist/shipping/deliverySelectionPort.js +1 -0
  329. package/dist/shipping/deliverySelectionSummary.d.ts +2 -0
  330. package/dist/shipping/deliverySelectionSummary.js +53 -0
  331. package/dist/shipping/handlerSchemas.d.ts +21 -0
  332. package/dist/shipping/handlerSchemas.js +1 -0
  333. package/dist/shipping/index.d.ts +20 -0
  334. package/dist/shipping/index.js +10 -0
  335. package/dist/shipping/pickupPointSearchPort.d.ts +4 -0
  336. package/dist/shipping/pickupPointSearchPort.js +1 -0
  337. package/dist/shipping/readDeliverySelectionEvidence.d.ts +2 -0
  338. package/dist/shipping/readDeliverySelectionEvidence.js +44 -0
  339. package/dist/shipping/shippingRatePorts.d.ts +52 -0
  340. package/dist/shipping/shippingRatePorts.js +1 -0
  341. package/dist/shipping/thresholdPolicy.d.ts +42 -0
  342. package/dist/shipping/thresholdPolicy.js +60 -0
  343. package/package.json +62 -2
  344. package/release-gates.json +263 -53
  345. package/sql/20260807140000_read_parity_catalog.sql +94 -0
  346. package/sql/20260810120000_storefront_catalog_attributes.sql +51 -0
  347. package/sql/20260812140000_sellable_bundle_catalog.sql +124 -0
  348. package/sql/20260812210000_bundle_operator_write_boundary.sql +567 -0
  349. package/sql/20260814000000_fulfillment_stock_evidence.sql +239 -0
  350. package/sql/20260815120000_automatic_subscription_renewal.sql +1083 -0
  351. package/sql/20260817180000_promotion_claim_lifecycle.sql +591 -0
  352. package/sql/20260817180100_promotion_claim_lifecycle_validation.sql +23 -0
  353. package/sql/20260827220000_catalog_document_revision_foundation.sql +226 -0
  354. package/sql/20260827220001_catalog_document_revision_foundation_validate.sql +10 -0
  355. package/sql/20260908100000_catalog_draft_foundation.sql +217 -0
  356. package/src/catalog/activeSkuSelection.ts +20 -0
  357. package/src/catalog/adminBindings.ts +59 -0
  358. package/src/catalog/adminBundleHandler.ts +165 -0
  359. package/src/catalog/adminBundleReadHandler.ts +263 -0
  360. package/src/catalog/adminBundleWritePort.ts +102 -0
  361. package/src/catalog/bundleAvailability.ts +129 -0
  362. package/src/catalog/bundleAvailabilityService.ts +171 -0
  363. package/src/catalog/bundleBindings.ts +29 -0
  364. package/src/catalog/bundleCatalogQueries.ts +98 -0
  365. package/src/catalog/bundleCatalogReadPort.ts +181 -0
  366. package/src/catalog/bundleCatalogStore.ts +274 -0
  367. package/src/catalog/bundlePriceSelection.ts +25 -0
  368. package/src/catalog/bundleRuleCodes.ts +134 -0
  369. package/src/catalog/bundleRules.ts +153 -0
  370. package/src/catalog/catalogDraftStore.ts +71 -0
  371. package/src/catalog/catalogSkuEnvelope.ts +214 -0
  372. package/src/catalog/catalogSkuEnvelopeMapper.ts +160 -0
  373. package/src/catalog/contracts/bundleAdmin.ts +20 -0
  374. package/src/catalog/contracts/bundleAvailability.ts +5 -0
  375. package/src/catalog/contracts/bundleRead.ts +13 -0
  376. package/src/catalog/contracts/draft.ts +69 -0
  377. package/src/catalog/contracts/foundation.ts +104 -0
  378. package/src/catalog/contracts/index.ts +14 -0
  379. package/src/catalog/contracts/lifecycle.ts +74 -0
  380. package/src/catalog/contracts/productMediaContracts.ts +170 -0
  381. package/src/catalog/contracts/productTypes.ts +35 -0
  382. package/src/catalog/contracts/resolveProductMedia.ts +85 -0
  383. package/src/catalog/contracts/sellable.ts +12 -0
  384. package/src/catalog/draftHandler.ts +105 -0
  385. package/src/catalog/draftPorts.ts +18 -0
  386. package/src/catalog/draftSchemas.ts +88 -0
  387. package/src/catalog/draftValidation.ts +100 -0
  388. package/src/catalog/effectiveBundleCatalogQueries.ts +80 -0
  389. package/src/catalog/effectiveBundleCatalogStore.ts +208 -0
  390. package/src/catalog/foundationSchemas.ts +123 -0
  391. package/src/catalog/index.ts +44 -0
  392. package/src/catalog/mediaSourcePack.ts +219 -0
  393. package/src/catalog/productTypeSchemas.ts +70 -0
  394. package/src/catalog/publicationPorts.ts +44 -0
  395. package/src/catalog/readPorts.ts +61 -0
  396. package/src/catalog/referenceCatalogHandler.ts +39 -0
  397. package/src/catalog/resolvedSkuProjection.ts +76 -0
  398. package/src/catalog/sellableBundleHandler.ts +133 -0
  399. package/src/catalog/sellableSchemas.ts +33 -0
  400. package/src/catalog/sql.ts +14 -0
  401. package/src/catalog/validation.ts +3 -0
  402. package/src/communications/contracts/index.ts +1 -0
  403. package/src/communications/contracts/marketingSync.ts +109 -0
  404. package/src/communications/index.ts +1 -0
  405. package/src/communications/klaviyo/consentPush.ts +19 -0
  406. package/src/communications/klaviyo/index.ts +649 -0
  407. package/src/communications/resend/index.ts +350 -0
  408. package/src/composition/catalogContribution.ts +32 -0
  409. package/src/composition/index.ts +3 -0
  410. package/src/composition/inventoryContribution.ts +24 -0
  411. package/src/composition/shippingContribution.ts +24 -0
  412. package/src/inventory/adminInventory.ts +251 -0
  413. package/src/inventory/bindings.ts +61 -0
  414. package/src/inventory/contracts/forecast.ts +96 -0
  415. package/src/inventory/contracts/index.ts +18 -0
  416. package/src/inventory/contracts/inventory.ts +14 -0
  417. package/src/inventory/contracts/newPurchaseStock.ts +23 -0
  418. package/src/inventory/contracts/reservation.ts +20 -0
  419. package/src/inventory/forecast.ts +239 -0
  420. package/src/inventory/index.ts +22 -0
  421. package/src/inventory/inventoryHandlers.ts +215 -0
  422. package/src/inventory/newPurchaseStockAvailability.ts +120 -0
  423. package/src/inventory/ports.ts +46 -0
  424. package/src/inventory/renewalReservation.ts +88 -0
  425. package/src/inventory/reservationPolicy.ts +27 -0
  426. package/src/inventory/schemas.ts +128 -0
  427. package/src/inventory/sql.ts +5 -0
  428. package/src/inventory/subscriptionForecastCalculation.ts +157 -0
  429. package/src/pricing/acquisitionFinancialPolicy.ts +26 -0
  430. package/src/pricing/acquisitionOfferPricing.ts +38 -0
  431. package/src/pricing/acquisitionOfferPricingHandler.ts +157 -0
  432. package/src/pricing/acquisitionOfferPricingSchemas.ts +35 -0
  433. package/src/pricing/acquisitionQuoteBinding.ts +12 -0
  434. package/src/pricing/acquisitionReadiness.ts +148 -0
  435. package/src/pricing/acquisitionTerms.ts +125 -0
  436. package/src/pricing/acquisitionTermsSchemas.ts +52 -0
  437. package/src/pricing/catalogFactsProvenance.ts +102 -0
  438. package/src/pricing/catalogFactsSchemas.ts +69 -0
  439. package/src/pricing/commerceOfferPricing.ts +224 -0
  440. package/src/pricing/commerceOfferPricingCache.ts +53 -0
  441. package/src/pricing/commerceOfferPricingHandler.ts +137 -0
  442. package/src/pricing/commercePriceAuthority.ts +169 -0
  443. package/src/pricing/commercePricingPolicyErrors.ts +88 -0
  444. package/src/pricing/commerceQuoteBatchHandler.ts +129 -0
  445. package/src/pricing/commerceQuoteCatalogReadPort.ts +42 -0
  446. package/src/pricing/commerceQuoteHandler.ts +199 -0
  447. package/src/pricing/commerceQuoteSnapshotExpect.ts +48 -0
  448. package/src/pricing/contracts/acquisitionTerms.ts +26 -0
  449. package/src/pricing/contracts/authority.ts +44 -0
  450. package/src/pricing/contracts/evidence.ts +25 -0
  451. package/src/pricing/contracts/index.ts +17 -0
  452. package/src/pricing/contracts/money.ts +32 -0
  453. package/src/pricing/contracts/offer.ts +42 -0
  454. package/src/pricing/contracts/offerPolicy.ts +17 -0
  455. package/src/pricing/contracts/policy.ts +26 -0
  456. package/src/pricing/contracts/provenance.ts +19 -0
  457. package/src/pricing/contracts/quote.ts +106 -0
  458. package/src/pricing/dbBackedCommerceQuoteHelpers.ts +256 -0
  459. package/src/pricing/dbBackedCommerceQuotePort.ts +280 -0
  460. package/src/pricing/dbBackedCommerceQuoteSnapshotVerifier.ts +49 -0
  461. package/src/pricing/index.ts +53 -0
  462. package/src/pricing/offerPolicyReadiness.ts +110 -0
  463. package/src/pricing/offerPolicySchemas.ts +23 -0
  464. package/src/pricing/offerPricingSchemas.ts +92 -0
  465. package/src/pricing/ports.ts +17 -0
  466. package/src/pricing/pricingPolicyToken.ts +119 -0
  467. package/src/pricing/quoteAcquisitionTerms.ts +10 -0
  468. package/src/pricing/quoteCodeRejectionDetails.ts +23 -0
  469. package/src/pricing/quotePorts.ts +58 -0
  470. package/src/pricing/quotePrimitives.ts +21 -0
  471. package/src/pricing/quotePromotionEvidence.ts +82 -0
  472. package/src/pricing/quoteSchemas.ts +230 -0
  473. package/src/pricing/quoteShippingPromotions.ts +21 -0
  474. package/src/pricing/subscriptionPricePolicy.ts +113 -0
  475. package/src/pricing/taxProfileGuard.ts +33 -0
  476. package/src/pricing/validation.ts +3 -0
  477. package/src/promotions/adminBindings.ts +53 -0
  478. package/src/promotions/adminPromotionCodesContracts.ts +244 -0
  479. package/src/promotions/adminPromotionCodesDataPort.ts +56 -0
  480. package/src/promotions/adminPromotionCodesHandler.ts +254 -0
  481. package/src/promotions/adminPromotionsContracts.ts +121 -0
  482. package/src/promotions/adminPromotionsDataPort.ts +48 -0
  483. package/src/promotions/adminPromotionsHandler.ts +72 -0
  484. package/src/promotions/commerceQuoteReadPreparation.ts +29 -0
  485. package/src/promotions/contracts/index.ts +387 -0
  486. package/src/promotions/index.ts +25 -0
  487. package/src/promotions/promoDataPort.ts +65 -0
  488. package/src/promotions/promotionAcceptanceTelemetry.ts +73 -0
  489. package/src/promotions/promotionBenefitDefinition.ts +48 -0
  490. package/src/promotions/promotionClaimSweepPort.ts +23 -0
  491. package/src/promotions/promotionClaims.ts +70 -0
  492. package/src/promotions/promotionCodePreview.ts +183 -0
  493. package/src/promotions/promotionCodeQuotePort.ts +35 -0
  494. package/src/promotions/promotionQuoteAcceptance.ts +291 -0
  495. package/src/promotions/promotionQuoteBinding.ts +163 -0
  496. package/src/promotions/quoteAcquisitionBinding.ts +7 -0
  497. package/src/promotions/quoteAutomaticPromotions.ts +84 -0
  498. package/src/promotions/quotePromoDiscounts.ts +194 -0
  499. package/src/promotions/quotePromotionCodeAdjustments.ts +277 -0
  500. package/src/promotions/quotePromotionV2.ts +239 -0
  501. package/src/promotions/quoteShippingPolicy.ts +61 -0
  502. package/src/promotions/sql.ts +5 -0
  503. package/src/promotions/validation.ts +3 -0
  504. package/src/shipping/addressCanonLookupHandler.ts +114 -0
  505. package/src/shipping/addressCanonical.ts +31 -0
  506. package/src/shipping/addressPorts.ts +48 -0
  507. package/src/shipping/addressSchemas.ts +28 -0
  508. package/src/shipping/adminShippingRateHandler.ts +72 -0
  509. package/src/shipping/bindings.ts +74 -0
  510. package/src/shipping/contracts/address.ts +71 -0
  511. package/src/shipping/contracts/delivery.ts +114 -0
  512. package/src/shipping/contracts/index.ts +19 -0
  513. package/src/shipping/contracts/shippingRate.ts +7 -0
  514. package/src/shipping/contracts/threshold.ts +22 -0
  515. package/src/shipping/customerAddressesHandler.ts +100 -0
  516. package/src/shipping/customerSelfServiceAddressModels.ts +124 -0
  517. package/src/shipping/deliverySelectionHandler.ts +94 -0
  518. package/src/shipping/deliverySelectionPort.ts +10 -0
  519. package/src/shipping/deliverySelectionSummary.ts +56 -0
  520. package/src/shipping/handlerSchemas.ts +19 -0
  521. package/src/shipping/index.ts +26 -0
  522. package/src/shipping/pickupPointSearchPort.ts +8 -0
  523. package/src/shipping/readDeliverySelectionEvidence.ts +51 -0
  524. package/src/shipping/shippingRatePorts.ts +36 -0
  525. package/src/shipping/thresholdPolicy.ts +65 -0
@@ -0,0 +1,860 @@
1
+ import { consentCommand } from "./consentPush.js";
2
+ const quarantineCauses = new WeakMap();
3
+ const providerCooling = new WeakMap();
4
+ const REVISION = "2026-07-15";
5
+ const ORIGIN = "https://a.klaviyo.com";
6
+ const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
7
+ const text = (value) => typeof value === "string" && value.trim().length > 0 && value.length <= 512 && ![...value].some(c => c.charCodeAt(0) < 32);
8
+ const id = (value) => typeof value === "string" && /^[A-Za-z0-9_-]{1,200}$/.test(value);
9
+ const positive = (value) => Number.isSafeInteger(value) && value > 0;
10
+ const bytes = (value) => new TextEncoder().encode(JSON.stringify(value)).byteLength;
11
+ function canonical(value) {
12
+ if (Array.isArray(value))
13
+ return "[" + value.map(canonical).join(",") + "]";
14
+ if (value && typeof value === "object")
15
+ return "{" + Object.keys(value).filter(k => value[k] !== undefined).sort().map(k => JSON.stringify(k) + ":" + canonical(value[k])).join(",") + "}";
16
+ return JSON.stringify(value);
17
+ }
18
+ const equal = (a, b) => canonical(a) === canonical(b);
19
+ function freeze(value) {
20
+ if (value && typeof value === "object") {
21
+ for (const child of Object.values(value))
22
+ freeze(child);
23
+ Object.freeze(value);
24
+ }
25
+ return value;
26
+ }
27
+ const copy = (value) => freeze(structuredClone(value));
28
+ const exact = (value, keys) => Object.keys(object(value)).sort().join() === [...keys].sort().join();
29
+ const validBinding = (b) => exact(b, ["account", "environment", "namespace", "mappingVersion"]) && Object.values(object(b)).every(text);
30
+ const validAudience = (a) => object(a).kind === "account-wide" ? exact(a, ["kind"]) : object(a).kind === "audience" && exact(a, ["kind", "reference"]) && text(object(a).reference);
31
+ const overlaps = (a, b) => a.kind === "account-wide" || b.kind === "account-wide" || equal(a, b);
32
+ const keyOf = (r) => canonical([r.binding, r.operation, r.subjectReference, r.requestId]);
33
+ const subjectKey = (r) => canonical([r.binding, r.subjectReference]);
34
+ const semantic = (r) => canonical([r.audience, r.payload]);
35
+ const echo = (r) => ({ binding: r.binding, operation: r.operation, subjectReference: r.subjectReference, requestId: r.requestId, generation: r.generation });
36
+ function result(r, value) { return copy({ ...value, ...echo(r) }); }
37
+ const refused = (r, reason) => result(r, { kind: "refused", reason });
38
+ const retry = (r, reason, afterSeconds = 1) => result(r, { kind: "retryable", reason, afterSeconds });
39
+ const unknown = (r, reason, receipt) => result(r, { kind: "unknown", reason, ...(receipt ? { receipt } : {}) });
40
+ function validRequest(r, max) {
41
+ if (!exact(r, ["binding", "audience", "subjectReference", "requestId", "generation", "deadlineMs", "authorizationReference", "operation", "payload"]) ||
42
+ !validBinding(r.binding) || !validAudience(r.audience) || !text(r.subjectReference) || !text(r.requestId) || !text(r.authorizationReference) ||
43
+ !Number.isSafeInteger(r.generation) || r.generation < 0 || !Number.isFinite(r.deadlineMs) || bytes(r) > max)
44
+ return false;
45
+ const p = r.payload;
46
+ switch (r.operation) {
47
+ case "profile.sync": {
48
+ const v = p;
49
+ if (!exact(p, ["projection", "consentEvidenceReference"]) || !text(v.consentEvidenceReference) || !exact(v.projection, ["email", "externalReference", "fields", "properties"]))
50
+ return false;
51
+ const q = v.projection;
52
+ return text(q.email) && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(q.email) && text(q.externalReference) && q.fields !== null && !Array.isArray(q.fields) && typeof q.fields === "object" &&
53
+ Object.entries(q.fields).every(([k, value]) => ["givenName", "familyName", "locale"].includes(k) && text(value)) &&
54
+ q.properties !== null && typeof q.properties === "object" && !Array.isArray(q.properties) && Object.entries(q.properties).every(([k, value]) => text(k) &&
55
+ (typeof value === "string" && value.length <= 512 || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value)));
56
+ }
57
+ case "consent.read": return exact(p, []);
58
+ case "erasure.request": return exact(p, ["caseReference", "authorityReference"]) && Object.values(p).every(text);
59
+ case "erasure.read": return exact(p, ["receipt"]) && object(object(p).receipt).operation === "erasure.request";
60
+ case "consent.push": return exact(p, ["action", "consentEvidenceReference"]) && text(object(p).consentEvidenceReference) &&
61
+ (r.payload.action === "grant" && r.audience.kind === "audience" || r.payload.action === "withdraw");
62
+ default: return false;
63
+ }
64
+ }
65
+ function validEntry(entry) {
66
+ if (!validRequest(entry.request, Number.MAX_SAFE_INTEGER) || entry.semantic !== semantic(entry.request) ||
67
+ !["admitted", "dispatched", "settled"].includes(entry.phase) || !equal(entry.target.binding, entry.request.binding) ||
68
+ entry.target.subjectReference !== entry.request.subjectReference)
69
+ return false;
70
+ if (!entry.result)
71
+ return entry.phase !== "settled";
72
+ const value = entry.result;
73
+ if (!equal(echo(value), echo(entry.request)))
74
+ return false;
75
+ const receipt = "receipt" in value ? value.receipt : undefined;
76
+ if (receipt && (!["profile.sync", "erasure.request", "consent.push"].includes(entry.request.operation) || !equal(receipt.request, entry.request) || receipt.operation !== entry.request.operation))
77
+ return false;
78
+ switch (value.kind) {
79
+ case "refused": return ["invalid_request", "binding", "provenance", "stale", "deadline", "unsupported", "verification_pending", "restricted", "identity_conflict", "provider_refusal"].includes(value.reason);
80
+ case "retryable": return ["cooling", "host", "provider"].includes(value.reason) && Number.isFinite(value.afterSeconds) && value.afterSeconds > 0;
81
+ case "unknown": return ["dispatch_unresolved", "acceptance_ambiguous", "record_failed"].includes(value.reason);
82
+ case "accepted":
83
+ case "acknowledged": return !!receipt && ["profile.sync", "erasure.request", "consent.push"].includes(entry.request.operation) &&
84
+ ["confirmed", "unknown"].includes(value.journal) && (entry.request.operation !== "consent.push" || "reconcileRequired" in value && typeof value.reconcileRequired === "boolean");
85
+ case "completed": {
86
+ if (value.operation !== "consent.read" || !["confirmed", "unknown"].includes(value.journal))
87
+ return false;
88
+ const v = value.value;
89
+ return equal(v.audience, entry.request.audience) && Number.isFinite(v.observedAtMs) && ["complete", "partial", "unsupported"].includes(v.coverage) &&
90
+ ["subscribed", "unsubscribed", "pending", "never_subscribed", "unknown"].includes(v.remoteConsent) && Array.isArray(v.feedback) && v.feedback.every(f => validAudience(f.audience) && text(f.sourceReference) && (f.occurredAtMs === null || Number.isFinite(f.occurredAtMs)) &&
91
+ (["hard_bounce", "invalid_address"].includes(f.kind) ? f.outcome === "deny_delivery" : ["unsubscribe", "complaint", "manual_block"].includes(f.kind) && f.outcome === "deny_permission"));
92
+ }
93
+ default: return false;
94
+ }
95
+ }
96
+ function emptyLedger() { return { version: 1, coolingUntil: 0, nextAt: 0, slot: false, quarantined: false, entries: {}, mappings: {}, tombstones: [], withdrawals: {} }; }
97
+ function parseLedger(raw) {
98
+ if (raw === null)
99
+ return emptyLedger();
100
+ const v = JSON.parse(raw);
101
+ if (v.version !== 1 || !Number.isFinite(v.coolingUntil) || !Number.isFinite(v.nextAt) || typeof v.quarantined !== "boolean" ||
102
+ !v.entries || Array.isArray(v.entries) || !v.mappings || !Array.isArray(v.tombstones) || !v.withdrawals || typeof v.slot !== "boolean" || !Object.values(v.entries).every(validEntry))
103
+ throw new Error("journal");
104
+ return v;
105
+ }
106
+ function profileAttributes(p, creating) {
107
+ const names = { givenName: "first_name", familyName: "last_name", locale: "locale" };
108
+ return { ...(creating ? { email: p.email, external_id: p.externalReference } : {}),
109
+ ...Object.fromEntries(Object.entries(p.fields).map(([k, v]) => [names[k], v])), properties: p.properties };
110
+ }
111
+ function profileData(body, target, projection) {
112
+ const d = object(object(body).data), a = object(d.attributes);
113
+ if (d.type !== "profile" || !id(d.id) || (target.id && d.id !== target.id) || a.email !== target.email || a.external_id !== target.externalReference)
114
+ return null;
115
+ if (projection) {
116
+ const wanted = profileAttributes(projection, false);
117
+ for (const [k, v] of Object.entries(wanted)) {
118
+ if (k === "properties") {
119
+ for (const [name, value] of Object.entries(object(v)))
120
+ if (!equal(object(a.properties)[name], value))
121
+ return null;
122
+ }
123
+ else if (!equal(a[k], v))
124
+ return null;
125
+ }
126
+ }
127
+ return d.id;
128
+ }
129
+ function consentValue(body, target, audience, verification, now, max) {
130
+ if (!profileData(body, target))
131
+ return null;
132
+ const m = object(object(object(object(object(object(body).data).attributes).subscriptions).email).marketing);
133
+ const states = { SUBSCRIBED: "subscribed", UNSUBSCRIBED: "unsubscribed", NEVER_SUBSCRIBED: "never_subscribed", PENDING: "pending" };
134
+ let complete = verification.negativeSourcesComplete && Array.isArray(m.suppression) && Array.isArray(m.list_suppressions) && typeof m.can_receive_email_marketing === "boolean" && typeof m.consent === "string" && m.consent in states;
135
+ const feedback = [];
136
+ const kinds = { UNSUBSCRIBE: "unsubscribe", SPAM_REPORT: "complaint", SPAM_COMPLAINT: "complaint", USER_SUPPRESSED: "manual_block", HARD_BOUNCE: "hard_bounce", INVALID_EMAIL: "invalid_address" };
137
+ function add(reason, timestamp, scope, source) {
138
+ if (feedback.length >= max) {
139
+ complete = false;
140
+ return;
141
+ }
142
+ const kind = kinds[reason];
143
+ feedback.push(copy({ kind, outcome: kind === "hard_bounce" || kind === "invalid_address" ? "deny_delivery" : "deny_permission",
144
+ sourceReference: source, occurredAtMs: typeof timestamp === "string" && Number.isFinite(Date.parse(timestamp)) ? Date.parse(timestamp) : null, audience: scope }));
145
+ }
146
+ if (m.consent === "UNSUBSCRIBED")
147
+ add("UNSUBSCRIBE", m.consent_timestamp, { kind: "account-wide" }, `negative:${target.id}:consent`);
148
+ for (const [source, values] of [["global", m.suppression], ["list", m.list_suppressions]]) {
149
+ if (!Array.isArray(values))
150
+ continue;
151
+ // Current-state arrays are bounded by the response rail; no history, metrics discovery or pagination.
152
+ for (const [n, value] of values.entries()) {
153
+ const item = object(value);
154
+ if (typeof item.reason !== "string" || !Object.hasOwn(kinds, item.reason)) {
155
+ complete = false;
156
+ continue;
157
+ }
158
+ const scope = source === "global" ? { kind: "account-wide" } : typeof item.list_id === "string" ? verification.listMappings[item.list_id] : undefined;
159
+ if (!scope || !validAudience(scope)) {
160
+ complete = false;
161
+ continue;
162
+ }
163
+ add(item.reason, item.timestamp, scope, `negative:${target.id}:${source}:${n}`);
164
+ }
165
+ }
166
+ if (m.can_receive_email_marketing === false && feedback.length === 0)
167
+ complete = false;
168
+ return copy({ audience, observedAtMs: now, remoteConsent: audience.kind === "account-wide" ? states[String(m.consent)] ?? "unknown" : "unknown",
169
+ feedback, coverage: complete ? "complete" : "partial" });
170
+ }
171
+ /** Opt-in Node adapter; owner TEST evidence is required before any bound operation is enabled. */
172
+ export function createKlaviyoMarketingAdapter(input) {
173
+ const binding = copy(input.binding), audience = copy(input.audience), now = input.now ?? Date.now;
174
+ const maxRequestBytes = input.maxRequestBytes ?? 65536, maxResponseBytes = input.maxResponseBytes ?? 1048576;
175
+ const maxFeedbackItems = input.maxFeedbackItems ?? 64, timeoutMs = input.timeoutMs ?? 10000, rateIntervalMs = input.rateIntervalMs ?? 100;
176
+ if (!validBinding(binding) || !validAudience(audience) || ![maxRequestBytes, maxResponseBytes, maxFeedbackItems, timeoutMs, rateIntervalMs].every(positive))
177
+ throw new Error("Invalid adapter configuration");
178
+ const verification = input.verification ? copy(input.verification) : undefined;
179
+ if (verification && (!equal(verification.binding, binding) || !equal(verification.audience, audience) || verification.revision !== REVISION || !text(verification.evidenceReference)))
180
+ throw new Error("Invalid verification binding");
181
+ const operationScopes = {
182
+ "profile.sync": ["profiles:read", "profiles:write"], "consent.read": ["profiles:read"], "erasure.request": ["data-privacy:write"],
183
+ "consent.push": ["profiles:read", "subscriptions:write", "lists:write"],
184
+ };
185
+ const capability = (operation) => verification?.operations.includes(operation) && operationScopes[operation].every(s => verification.scopes.includes(s))
186
+ ? { state: "supported", evidenceReference: verification.evidenceReference, maxRequestBytes, maxFeedbackItems }
187
+ : { state: "verification_pending" };
188
+ const capabilities = copy({ binding, audience, operations: {
189
+ "profile.sync": capability("profile.sync"), "consent.read": capability("consent.read"),
190
+ "erasure.request": audience.kind === "account-wide" ? capability("erasure.request") : { state: "unsupported" },
191
+ "erasure.read": { state: "unsupported" }, "consent.push": { ...capability("consent.push"), grantEffect: "account-wide" },
192
+ } });
193
+ const accountKey = canonical(["communications-adapter", binding.account, binding.environment]);
194
+ const availability = input.storage.availability;
195
+ if (!availability || availability.account !== binding.account || availability.environment !== binding.environment ||
196
+ !["busy", "quarantined"].every(k => { const d = Object.getOwnPropertyDescriptor(availability, k); return d?.writable && typeof d.value === "boolean"; }))
197
+ throw new Error("Invalid account availability record");
198
+ if (!availability.quarantined)
199
+ quarantineCauses.delete(availability);
200
+ let quarantine = false;
201
+ const closeAccount = (grantCause) => {
202
+ const previous = quarantineCauses.get(availability);
203
+ if (!grantCause || previous?.kind === "non-grant" ||
204
+ (quarantine || availability.quarantined) && !previous)
205
+ quarantineCauses.set(availability, { kind: "non-grant" });
206
+ else {
207
+ const grants = previous?.kind === "grants" ? previous.grants : new Map();
208
+ grants.set(keyOf(grantCause), subjectKey(grantCause));
209
+ quarantineCauses.set(availability, { kind: "grants", grants });
210
+ }
211
+ quarantine = true;
212
+ availability.quarantined = true;
213
+ };
214
+ const isWithdrawal = (r) => r.operation === "consent.push" && r.payload.action === "withdraw";
215
+ function availabilityQuarantined(r) {
216
+ // Exempt only unresolved grant fences; every non-grant admission predicate remains.
217
+ return (quarantine || availability.quarantined) &&
218
+ !(isWithdrawal(r) && quarantineCauses.get(availability)?.kind === "grants");
219
+ }
220
+ const load = async () => {
221
+ let raw;
222
+ try {
223
+ raw = await input.storage.get(accountKey);
224
+ }
225
+ catch {
226
+ closeAccount();
227
+ throw new Error("storage");
228
+ }
229
+ try {
230
+ const ledger = parseLedger(raw);
231
+ ledger.coolingUntil = Math.max(ledger.coolingUntil, providerCooling.get(availability) ?? 0);
232
+ return { raw, ledger };
233
+ }
234
+ catch {
235
+ closeAccount();
236
+ throw new Error("journal");
237
+ }
238
+ };
239
+ async function quarantineAccount(affectedKey, grantCause) {
240
+ closeAccount(grantCause);
241
+ // An error reply may follow a committed CAS. Write the existing quarantine bit
242
+ // against the actual latest bytes, rather than trusting the failed writer's copy.
243
+ try {
244
+ const raw = await input.storage.get(accountKey), ledger = parseLedger(raw);
245
+ ledger.quarantined ||= !grantCause;
246
+ const saved = affectedKey ? ledger.entries[affectedKey]?.result : undefined;
247
+ if (affectedKey && saved && (saved.kind === "accepted" || saved.kind === "acknowledged" || saved.kind === "completed"))
248
+ ledger.entries[affectedKey].result = result(ledger.entries[affectedKey].request, { ...saved, journal: "unknown" });
249
+ await input.storage.compareAndSwap(accountKey, raw, canonical(ledger));
250
+ }
251
+ catch {
252
+ closeAccount(); /* Recovery storage faults are non-grant causes. */
253
+ }
254
+ }
255
+ async function persist(raw, ledger, affectedKey, grantCause) {
256
+ try {
257
+ const next = canonical(ledger);
258
+ if (!await input.storage.compareAndSwap(accountKey, raw, next))
259
+ return "conflict";
260
+ if (await input.storage.get(accountKey) !== next) {
261
+ await quarantineAccount(affectedKey, grantCause);
262
+ return "uncertain";
263
+ }
264
+ return "saved";
265
+ }
266
+ catch {
267
+ await quarantineAccount(affectedKey, grantCause);
268
+ return "uncertain";
269
+ }
270
+ }
271
+ async function releaseSlot(r) {
272
+ try {
273
+ const release = await load();
274
+ if (r ? availabilityQuarantined(r) : quarantine || availability.quarantined)
275
+ return false;
276
+ release.ledger.slot = false;
277
+ if (await persist(release.raw, release.ledger) === "saved")
278
+ return true;
279
+ }
280
+ catch { /* The durable slot stays held until host repair. */ }
281
+ await quarantineAccount();
282
+ return false;
283
+ }
284
+ function current(r, snapshot) {
285
+ const s = input.subjectMapper(r.subjectReference);
286
+ if (!s || s.subjectReference !== r.subjectReference || !equal(s.binding, binding))
287
+ return "identity_conflict";
288
+ if (s.generation !== r.generation)
289
+ return "stale";
290
+ if (r.deadlineMs <= now())
291
+ return "deadline";
292
+ if (s.erased && (r.operation === "profile.sync" || r.operation === "consent.push" && r.payload.action === "grant"))
293
+ return "restricted";
294
+ if (snapshot && !equal(s, snapshot))
295
+ return "provenance";
296
+ return copy(s);
297
+ }
298
+ function authority(r, target) {
299
+ const auth = input.authorizations.get(r.authorizationReference);
300
+ if (!auth || !equal(auth.binding, binding) || auth.subjectReference !== r.subjectReference || !equal(auth.audience, r.audience) || auth.operation !== r.operation)
301
+ return "provenance";
302
+ if (auth.expiresAtMs <= now())
303
+ return "deadline";
304
+ if (r.operation === "consent.read" || r.operation === "erasure.read")
305
+ return;
306
+ const ref = r.operation === "erasure.request" ? r.payload.authorityReference : r.payload.consentEvidenceReference;
307
+ const local = input.localEvidence.get(ref);
308
+ if (!local || !equal(local.binding, binding) || local.subjectReference !== r.subjectReference || !equal(local.audience, r.audience) || !text(local.purpose))
309
+ return "provenance";
310
+ const action = r.operation === "profile.sync" ? "profile" : r.operation === "erasure.request" ? "erase" : r.payload.action;
311
+ if (!local.current || local.action !== action)
312
+ return "restricted";
313
+ if (r.operation === "profile.sync") {
314
+ if (!equal(local.projection, r.payload.projection))
315
+ return "provenance";
316
+ if (r.payload.projection.email !== target.email || r.payload.projection.externalReference !== target.externalReference ||
317
+ Object.keys(r.payload.projection.fields).some(k => !target.ownedFields.includes(k)) ||
318
+ Object.keys(r.payload.projection.properties).some(k => !target.ownedProperties.includes(k)))
319
+ return "identity_conflict";
320
+ if (!verification?.noTrigger.profile)
321
+ return "restricted";
322
+ }
323
+ if (r.operation === "erasure.request" && (local.caseReference !== r.payload.caseReference || local.authorityReference !== r.payload.authorityReference))
324
+ return "provenance";
325
+ if (r.operation === "consent.push") {
326
+ if (r.payload.action === "withdraw" && r.audience.kind === "account-wide" && !local.allPurposes)
327
+ return "restricted";
328
+ if (r.payload.action === "grant" && (!verification?.noTrigger.accountWideGrant || !verification.noTrigger.doi || !verification.suppressionSafeAtEffect))
329
+ return "restricted";
330
+ if (r.payload.action === "withdraw" && r.audience.kind === "audience" && !verification?.safeListRemoval)
331
+ return "restricted";
332
+ }
333
+ }
334
+ async function http(r, method, path, body, guard) {
335
+ if (body !== undefined && bytes(body) > maxRequestBytes)
336
+ return { status: 0, failure: refused(r, "invalid_request") };
337
+ const mutation = method !== "GET";
338
+ const ambiguous = () => mutation ? unknown(r, "acceptance_ambiguous", { operation: r.operation, request: r }) : retry(r, "provider");
339
+ let loaded;
340
+ try {
341
+ loaded = await load();
342
+ }
343
+ catch {
344
+ closeAccount();
345
+ return { status: 0, failure: retry(r, "host") };
346
+ }
347
+ let { raw, ledger } = loaded;
348
+ // Pace the remaining HTTP steps of this operation without retrying a mutation.
349
+ // Provider cooldown and occupied/quarantined slots still return immediately.
350
+ const delay = ledger.nextAt - now();
351
+ if (!availabilityQuarantined(r) && !ledger.quarantined && !ledger.slot && ledger.coolingUntil <= now() && delay > 0 && delay < Math.min(timeoutMs, r.deadlineMs - now())) {
352
+ await new Promise(resolve => setTimeout(resolve, delay));
353
+ try {
354
+ ({ raw, ledger } = await load());
355
+ }
356
+ catch {
357
+ closeAccount();
358
+ return { status: 0, failure: retry(r, "host") };
359
+ }
360
+ }
361
+ if ((availabilityQuarantined(r) || ledger.quarantined || ledger.slot) || ledger.coolingUntil > now() || ledger.nextAt > now())
362
+ return { status: 0, failure: retry(r, "cooling", Math.max(1, Math.ceil((Math.max(ledger.coolingUntil, ledger.nextAt) - now()) / 1000))) };
363
+ if (mutation && (r.operation === "profile.sync" || r.operation === "consent.push" && r.payload.action === "grant") && ledger.tombstones.includes(subjectKey(r)))
364
+ return { status: 0, failure: refused(r, "restricted") };
365
+ if (mutation && r.operation === "consent.push" && r.payload.action === "grant" && Object.values(ledger.withdrawals).some(w => w.owner === subjectKey(r) && !w.resolved && overlaps(w.audience, r.audience)))
366
+ return { status: 0, failure: refused(r, "restricted") };
367
+ const before = guard();
368
+ if (before)
369
+ return { status: 0, failure: refused(r, before) };
370
+ ledger.nextAt = now() + rateIntervalMs;
371
+ ledger.slot = true;
372
+ const saved = await persist(raw, ledger);
373
+ if (saved !== "saved")
374
+ return { status: 0, failure: saved === "conflict" ? retry(r, "host") : unknown(r, "dispatch_unresolved") };
375
+ async function transfer() {
376
+ const controller = new AbortController();
377
+ const remaining = Math.min(timeoutMs, r.deadlineMs - now());
378
+ let timeout;
379
+ const abort = new Promise((_resolve, reject) => { timeout = setTimeout(() => { controller.abort(); reject(new Error("deadline")); }, remaining); });
380
+ try {
381
+ if (remaining <= 0)
382
+ return { status: 0, failure: refused(r, "deadline") };
383
+ const recheck = guard();
384
+ if (recheck)
385
+ return { status: 0, failure: refused(r, recheck) };
386
+ if (availabilityQuarantined(r) || !availability.busy)
387
+ return { status: 0, failure: retry(r, "cooling") };
388
+ let credential;
389
+ try {
390
+ credential = input.resolveCredential();
391
+ }
392
+ catch {
393
+ return { status: 0, failure: retry(r, "host") };
394
+ }
395
+ if (!credential || !equal(credential.binding, binding) || !text(credential.key) || /\s/.test(credential.key))
396
+ return { status: 0, failure: refused(r, "binding") };
397
+ const response = await Promise.race([(input.fetch ?? fetch)(ORIGIN + path, {
398
+ method, ...(body === undefined ? {} : { body: JSON.stringify(body) }), signal: controller.signal, redirect: "manual",
399
+ headers: { Authorization: `Klaviyo-API-Key ${credential.key}`, revision: REVISION, Accept: "application/vnd.api+json",
400
+ ...(body === undefined ? {} : { "Content-Type": "application/vnd.api+json" }) },
401
+ }), abort]);
402
+ if (response.status === 429) {
403
+ const value = response.headers.get("retry-after");
404
+ const parsed = value && /^\d+(?:\.\d+)?$/.test(value) ? Number(value) : value ? (Date.parse(value) - now()) / 1000 : NaN;
405
+ const afterSeconds = Number.isFinite(parsed) && parsed >= 0 && Number.isFinite(now() + Math.ceil(parsed) * 1000) ? Math.max(1, Math.ceil(parsed)) : 60;
406
+ // A lost cooldown write must never turn the withdrawal exemption into
407
+ // permission to ignore an observed account-level provider rate limit.
408
+ providerCooling.set(availability, Math.max(providerCooling.get(availability) ?? 0, now() + afterSeconds * 1000));
409
+ try {
410
+ const next = await load();
411
+ next.ledger.coolingUntil = Math.max(next.ledger.coolingUntil, now() + afterSeconds * 1000);
412
+ if (await persist(next.raw, next.ledger) !== "saved")
413
+ closeAccount();
414
+ }
415
+ catch {
416
+ closeAccount();
417
+ }
418
+ await response.body?.cancel().catch(() => { });
419
+ return { status: 429, failure: retry(r, "cooling", afterSeconds) };
420
+ }
421
+ if (response.status >= 400 && response.status < 500) {
422
+ await response.body?.cancel().catch(() => { });
423
+ if (response.status === 404 && !mutation)
424
+ return { status: 404 };
425
+ return { status: response.status, failure: refused(r, response.status === 409 ? "identity_conflict" : "provider_refusal") };
426
+ }
427
+ if (response.status >= 300) {
428
+ await response.body?.cancel().catch(() => { });
429
+ return { status: response.status, failure: ambiguous() };
430
+ }
431
+ if (response.status === 202 || response.status === 204) {
432
+ await response.body?.cancel().catch(() => { });
433
+ return { status: response.status };
434
+ }
435
+ if (!/^application\/(?:vnd\.api\+json|json)(?:\s*;|$)/i.test(response.headers.get("content-type") ?? "")) {
436
+ await response.body?.cancel().catch(() => { });
437
+ return { status: response.status, failure: ambiguous() };
438
+ }
439
+ const reader = response.body?.getReader();
440
+ if (!reader)
441
+ return { status: response.status, failure: ambiguous() };
442
+ const chunks = [];
443
+ let size = 0;
444
+ for (;;) {
445
+ const chunk = await Promise.race([reader.read(), abort]);
446
+ if (chunk.done)
447
+ break;
448
+ size += chunk.value.byteLength;
449
+ if (size > maxResponseBytes) {
450
+ await reader.cancel();
451
+ return { status: response.status, failure: ambiguous() };
452
+ }
453
+ chunks.push(chunk.value);
454
+ }
455
+ const combined = new Uint8Array(size);
456
+ let offset = 0;
457
+ for (const chunk of chunks) {
458
+ combined.set(chunk, offset);
459
+ offset += chunk.byteLength;
460
+ }
461
+ return { status: response.status, body: JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(combined)) };
462
+ }
463
+ catch {
464
+ return { status: 0, failure: ambiguous() };
465
+ }
466
+ finally {
467
+ clearTimeout(timeout);
468
+ if (r.operation !== "consent.read")
469
+ await releaseSlot(r);
470
+ }
471
+ }
472
+ const answer = await transfer();
473
+ return { ...answer, ...(r.operation === "consent.read" ? { slotHeld: true } : {}) };
474
+ }
475
+ async function execute(original, ownsAvailability) {
476
+ // Snapshot validation precedes every await, credential lookup and provider call.
477
+ let r;
478
+ try {
479
+ r = copy(original);
480
+ if (!validRequest(r, maxRequestBytes))
481
+ return refused(r, "invalid_request");
482
+ }
483
+ catch {
484
+ return refused(original, "invalid_request");
485
+ }
486
+ if (!equal(r.binding, binding))
487
+ return refused(r, "binding");
488
+ const initial = current(r);
489
+ if (typeof initial === "string")
490
+ return refused(r, initial);
491
+ const key = keyOf(r), owner = subjectKey(r);
492
+ let loaded;
493
+ try {
494
+ loaded = await load();
495
+ }
496
+ catch (error) {
497
+ closeAccount();
498
+ return error instanceof Error && error.message === "journal" ? unknown(r, "record_failed") : retry(r, "host");
499
+ }
500
+ let { raw, ledger } = loaded;
501
+ // Receipt provenance precedes capability refusal, including deserialized receipts.
502
+ if (r.operation === "erasure.read") {
503
+ const receipt = r.payload.receipt;
504
+ if (!receipt?.request || !validRequest(receipt.request, maxRequestBytes))
505
+ return refused(r, "provenance");
506
+ const prior = ledger.entries[keyOf(receipt.request)];
507
+ if (receipt.operation !== "erasure.request" || !prior || !equal(prior.request, receipt.request) || prior.request.operation !== "erasure.request" ||
508
+ !equal(prior.request.binding, r.binding) || prior.request.subjectReference !== r.subjectReference || !equal(prior.request.audience, r.audience) ||
509
+ !prior.result || !("receipt" in prior.result) || !equal(prior.result.receipt, receipt))
510
+ return refused(r, "provenance");
511
+ }
512
+ const latest = current(r, initial);
513
+ if (typeof latest === "string")
514
+ return refused(r, latest);
515
+ if (ledger.entries[key] && ledger.entries[key].semantic !== semantic(r))
516
+ return refused(r, "invalid_request");
517
+ if (r.operation !== "consent.push" && !equal(r.audience, audience) || r.operation === "consent.push" && r.audience.kind === "audience" && !equal(r.audience, audience))
518
+ return refused(r, "unsupported");
519
+ const cap = capabilities.operations[r.operation];
520
+ if (cap.state !== "supported")
521
+ return refused(r, cap.state);
522
+ const previous = ledger.entries[key];
523
+ if (previous) {
524
+ if (previous.semantic !== semantic(r))
525
+ return refused(r, "invalid_request");
526
+ if (!equal(previous.request.binding, r.binding) || previous.request.subjectReference !== r.subjectReference || previous.request.operation !== r.operation) {
527
+ closeAccount();
528
+ return unknown(r, "record_failed");
529
+ }
530
+ if (previous.result) {
531
+ const value = { ...previous.result };
532
+ if (r.operation === "consent.push" && (value.kind === "accepted" || value.kind === "acknowledged") && "reconcileRequired" in value) {
533
+ value.reconcileRequired ||= Object.values(ledger.withdrawals).some(w => !w.resolved && w.owner === owner && overlaps(w.audience, r.audience) && ledger.entries[key]?.request.subjectReference === r.subjectReference);
534
+ }
535
+ return result(r, value);
536
+ }
537
+ return previous.phase === "admitted" ? result(r, { kind: "accepted", receipt: { operation: r.operation, request: previous.request }, journal: "confirmed", ...(r.operation === "consent.push" ? { reconcileRequired: true } : {}) }) : unknown(r, "dispatch_unresolved");
538
+ }
539
+ if (r.operation === "consent.push" && r.payload.action === "grant" && Object.values(ledger.withdrawals).some(w => !w.resolved && w.owner === owner && overlaps(w.audience, r.audience)))
540
+ return refused(r, "restricted");
541
+ // Withdrawals omit only this subject's unresolved-grant fence.
542
+ const pending = Object.values(ledger.entries).filter(e => subjectKey(e.request) === owner && e.phase !== "settled");
543
+ const withdrawing = r.operation === "consent.push" && r.payload.action === "withdraw";
544
+ if (r.operation !== "consent.read" && pending.some(e => !withdrawing || e.request.operation !== "consent.push" || e.request.payload.action !== "grant"))
545
+ return unknown(r, "dispatch_unresolved");
546
+ if ((availabilityQuarantined(r) || ledger.quarantined || ledger.slot) || ledger.coolingUntil > now())
547
+ return retry(r, "cooling", Math.max(1, Math.ceil((ledger.coolingUntil - now()) / 1000)));
548
+ const proof = authority(r, latest);
549
+ if (proof)
550
+ return refused(r, proof);
551
+ if (!ownsAvailability)
552
+ return retry(r, "cooling");
553
+ if (ledger.tombstones.includes(owner) && (r.operation === "profile.sync" || r.operation === "consent.push" && r.payload.action === "grant"))
554
+ return refused(r, "restricted");
555
+ const localRef = r.operation === "profile.sync" || r.operation === "consent.push" ? r.payload.consentEvidenceReference : r.operation === "erasure.request" ? r.payload.authorityReference : undefined;
556
+ const localSnapshot = localRef ? copy(input.localEvidence.get(localRef)) : undefined;
557
+ const authSnapshot = copy(input.authorizations.get(r.authorizationReference));
558
+ const guard = () => {
559
+ const s = current(r, initial);
560
+ if (typeof s === "string")
561
+ return s;
562
+ if (!equal(authSnapshot, input.authorizations.get(r.authorizationReference)) || localRef && !equal(localSnapshot, input.localEvidence.get(localRef)))
563
+ return "provenance";
564
+ return authority(r, s);
565
+ };
566
+ const listId = r.audience.kind === "audience" ? Object.entries(verification.listMappings).filter(([, scope]) => equal(scope, r.audience)).map(([k]) => k) : [];
567
+ let target = copy({ ...latest, ...(ledger.mappings[owner] ? { id: ledger.mappings[owner] } : {}) });
568
+ if (target.id && !id(target.id) || target.id && latest.id && target.id !== latest.id)
569
+ return refused(r, "identity_conflict");
570
+ const readProfile = async (selected) => http(r, "GET", `/api/profiles/${selected.id}?additional-fields[profile]=subscriptions`, undefined, guard);
571
+ if (!target.id) {
572
+ if (r.operation !== "profile.sync")
573
+ return refused(r, "identity_conflict");
574
+ const query = new URLSearchParams({ filter: `and(equals(email,${JSON.stringify(target.email)}),equals(external_id,${JSON.stringify(target.externalReference)}))`, "page[size]": "2" });
575
+ const lookup = await http(r, "GET", "/api/profiles?" + query, undefined, guard);
576
+ if (lookup.failure)
577
+ return lookup.failure;
578
+ const root = object(lookup.body), data = root.data;
579
+ if (lookup.status !== 200 || !Array.isArray(data) || data.length > 1 || object(root.links).next !== null)
580
+ return refused(r, "identity_conflict");
581
+ if (data.length === 1) {
582
+ const found = profileData({ data: data[0] }, target);
583
+ if (!found)
584
+ return refused(r, "identity_conflict");
585
+ target = copy({ ...target, id: found });
586
+ }
587
+ else if (!verification.safeCreate || !verification.noTrigger.profile)
588
+ return refused(r, "restricted");
589
+ }
590
+ if (target.id && (r.operation === "profile.sync" || r.operation === "consent.push")) {
591
+ const read = await readProfile(target);
592
+ if (read.failure)
593
+ return read.failure;
594
+ if (read.status !== 200 || !profileData(read.body, target))
595
+ return refused(r, "identity_conflict");
596
+ if (r.operation === "consent.push" && r.payload.action === "grant") {
597
+ const value = consentValue(read.body, target, r.audience, verification, now(), maxFeedbackItems);
598
+ if (!value || value.coverage !== "complete" || value.feedback.length > 0)
599
+ return refused(r, "restricted");
600
+ }
601
+ }
602
+ if (r.operation === "consent.read") {
603
+ const readStartedAt = now();
604
+ const settlements = copy(Object.fromEntries(input.grantSettlements ?? []));
605
+ const read = await readProfile(target);
606
+ if (read.failure) {
607
+ if (read.slotHeld)
608
+ await releaseSlot();
609
+ const changed = guard();
610
+ return changed ? refused(r, changed) : read.failure;
611
+ }
612
+ const changed = guard();
613
+ if (changed) {
614
+ if (read.slotHeld)
615
+ await releaseSlot();
616
+ return refused(r, changed);
617
+ }
618
+ let value = read.status === 404 ? copy({ audience: r.audience, observedAtMs: now(), remoteConsent: "unknown", coverage: "partial", feedback: [] }) : consentValue(read.body, target, r.audience, verification, now(), maxFeedbackItems);
619
+ if (!value) {
620
+ if (read.slotHeld)
621
+ await releaseSlot();
622
+ const changed = guard();
623
+ return changed ? refused(r, changed) : retry(r, "provider");
624
+ }
625
+ const unsettled = (w) => w.earlier.some(k => {
626
+ const grant = ledger.entries[k];
627
+ const proof = grant && settlements[grant.request.requestId];
628
+ return !grant || !proof || proof.source !== "provider-read" || !text(proof.evidenceReference) ||
629
+ !equal(proof.binding, r.binding) || proof.subjectReference !== r.subjectReference ||
630
+ proof.requestId !== grant.request.requestId || !Number.isFinite(proof.settledAtMs) || proof.settledAtMs > readStartedAt;
631
+ });
632
+ const affected = Object.values(ledger.withdrawals).filter(w => w.owner === owner && !w.resolved && overlaps(w.audience, r.audience));
633
+ const uncertain = affected.some(w => unsettled(w)) || affected.length > 0 && (value.coverage !== "complete" || !["subscribed", "unsubscribed"].includes(value.remoteConsent));
634
+ if (uncertain)
635
+ value = copy({ ...value, remoteConsent: "unknown" });
636
+ let journal = "unknown";
637
+ try {
638
+ const next = await load();
639
+ if (!uncertain && value.coverage === "complete") {
640
+ for (const [k, w] of Object.entries(next.ledger.withdrawals)) {
641
+ if (w.owner === owner && overlaps(w.audience, r.audience) && !unsettled(w)) {
642
+ if (value.remoteConsent === "unsubscribed")
643
+ next.ledger.withdrawals[k].resolved = true;
644
+ for (const earlier of w.earlier) {
645
+ next.ledger.entries[earlier].grantSettled = true;
646
+ next.ledger.entries[earlier].phase = "settled";
647
+ }
648
+ }
649
+ }
650
+ }
651
+ const alreadyQuarantined = next.ledger.quarantined;
652
+ next.ledger.entries[key] = { request: r, semantic: semantic(r), target, result: result(r, { kind: "completed", value, journal: "unknown" }), phase: "settled" };
653
+ next.ledger.quarantined = true;
654
+ if (await persist(next.raw, next.ledger, key) === "saved" && !alreadyQuarantined) {
655
+ const recorded = canonical(next.ledger);
656
+ next.ledger.entries[key].result = result(r, { kind: "completed", value, journal: "confirmed" });
657
+ next.ledger.quarantined = false;
658
+ next.ledger.slot = false;
659
+ if (await persist(recorded, next.ledger, key) === "saved")
660
+ journal = "confirmed";
661
+ else
662
+ await quarantineAccount(key);
663
+ }
664
+ else
665
+ await quarantineAccount(key);
666
+ }
667
+ catch {
668
+ await quarantineAccount(key);
669
+ }
670
+ const afterPersistence = guard();
671
+ if (afterPersistence)
672
+ return refused(r, afterPersistence);
673
+ return result(r, { kind: "completed", value, journal });
674
+ }
675
+ if (r.operation === "erasure.read")
676
+ return refused(r, "unsupported");
677
+ if (r.operation === "consent.push" && r.audience.kind === "audience" && (listId.length !== 1 || !id(listId[0])))
678
+ return refused(r, "restricted");
679
+ const changed = guard();
680
+ if (changed)
681
+ return refused(r, changed);
682
+ // Refresh before CAS: all namespaces/instances sharing account storage arbitrate here.
683
+ ({ raw, ledger } = await load());
684
+ if (ledger.entries[key])
685
+ return unknown(r, "dispatch_unresolved");
686
+ if (Object.values(ledger.entries).some(e => subjectKey(e.request) === owner && e.phase !== "settled" && (!withdrawing || e.request.operation !== "consent.push" || e.request.payload.action !== "grant")))
687
+ return unknown(r, "dispatch_unresolved");
688
+ if (r.operation === "consent.push" && r.payload.action === "grant" && Object.values(ledger.withdrawals).some(w => !w.resolved && w.owner === owner && overlaps(w.audience, r.audience)))
689
+ return refused(r, "restricted");
690
+ const receipt = copy({ operation: r.operation, request: r });
691
+ ledger.entries[key] = { request: r, semantic: semantic(r), target, phase: "dispatched" };
692
+ if (r.operation === "erasure.request" && !ledger.tombstones.includes(owner))
693
+ ledger.tombstones.push(owner);
694
+ let reconcileRequired = false;
695
+ if (withdrawing) {
696
+ const earlier = Object.entries(ledger.entries).filter(([, e]) => subjectKey(e.request) === owner && e.request.operation === "consent.push" && e.request.payload.action === "grant" && !e.grantSettled).map(([k]) => k);
697
+ const causes = quarantineCauses.get(availability);
698
+ for (const [grant, subject] of causes?.kind === "grants" ? causes.grants : [])
699
+ if (subject === owner && !earlier.includes(grant))
700
+ earlier.push(grant);
701
+ reconcileRequired = earlier.length > 0;
702
+ ledger.withdrawals[key] = { owner, audience: r.audience, earlier, resolved: false };
703
+ }
704
+ const grantCause = r.operation === "consent.push" && r.payload.action === "grant" ? r : undefined;
705
+ const admitted = await persist(raw, ledger, undefined, grantCause);
706
+ if (admitted !== "saved")
707
+ return admitted === "conflict" ? retry(r, "host") : unknown(r, "dispatch_unresolved");
708
+ const command = r.operation === "consent.push" ? consentCommand(r, { id: target.id, email: target.email }, listId[0]) : r.operation === "erasure.request" ? {
709
+ method: "POST", path: "/api/data-privacy-deletion-jobs", expected: [202], body: { data: { type: "data-privacy-deletion-job", attributes: { profile: { data: { type: "profile", id: target.id, attributes: {} } } } } },
710
+ } : {
711
+ method: target.id ? "PATCH" : "POST", path: target.id ? `/api/profiles/${target.id}` : "/api/profiles", expected: target.id ? [200, 202, 204] : [201, 202, 204],
712
+ body: { data: { type: "profile", ...(target.id ? { id: target.id } : {}), attributes: profileAttributes(r.payload.projection, !target.id) } },
713
+ };
714
+ const wire = await http(r, command.method, command.path, command.body, guard);
715
+ let settled, remoteId;
716
+ if (wire.failure)
717
+ settled = wire.failure;
718
+ else if (!command.expected.includes(wire.status))
719
+ settled = unknown(r, "acceptance_ambiguous", receipt);
720
+ else {
721
+ settled = result(r, { kind: "acknowledged", receipt, journal: quarantine || availability.quarantined || ledger.quarantined ? "unknown" : "confirmed", ...(r.operation === "consent.push" ? { reconcileRequired } : {}) });
722
+ if (r.operation === "profile.sync") {
723
+ const found = wire.body === undefined ? target.id : profileData(wire.body, target, r.payload.projection);
724
+ if (wire.body !== undefined && !found)
725
+ settled = unknown(r, "acceptance_ambiguous", receipt);
726
+ else if (!found)
727
+ settled = result(r, { kind: "acknowledged", receipt, journal: "unknown" });
728
+ else {
729
+ const verified = await readProfile({ ...target, id: found });
730
+ if (verified.failure || verified.status !== 200 || !profileData(verified.body, { ...target, id: found }, r.payload.projection) || guard())
731
+ settled = result(r, { kind: "acknowledged", receipt, journal: "unknown" });
732
+ else
733
+ remoteId = found;
734
+ }
735
+ }
736
+ }
737
+ try {
738
+ const final = await load(), entry = final.ledger.entries[key];
739
+ if (!entry || !equal(entry.request, r) || !equal(entry.target, target)) {
740
+ closeAccount();
741
+ return settled.kind === "acknowledged" ? result(r, { ...settled, journal: "unknown" }) : unknown(r, "record_failed", receipt);
742
+ }
743
+ entry.result = settled;
744
+ // Unknown acceptance retains the per-subject dispatch fence. Known acceptance with a
745
+ // failed projection/mapping check quarantines the account; it is never retried as a write.
746
+ entry.phase = settled.kind === "unknown" ? "dispatched" : "settled";
747
+ if (r.operation === "consent.push" && r.payload.action === "grant" && (settled.kind === "refused" || settled.kind === "retryable"))
748
+ entry.grantSettled = true;
749
+ if (r.operation === "profile.sync" && settled.kind === "acknowledged" && settled.journal === "unknown")
750
+ final.ledger.quarantined = true;
751
+ if (remoteId) {
752
+ entry.remoteId = remoteId;
753
+ final.ledger.mappings[owner] = remoteId;
754
+ }
755
+ const releaseQuarantine = !final.ledger.quarantined && !availabilityQuarantined(r);
756
+ // A grant already has its durable per-subject dispatch fence. Only other
757
+ // operations need the non-grant quarantine bit during journal settlement.
758
+ final.ledger.quarantined ||= !grantCause;
759
+ if (await persist(final.raw, final.ledger, key, grantCause) !== "saved") {
760
+ await quarantineAccount(key, grantCause);
761
+ return settled.kind === "acknowledged" ? result(r, { ...settled, journal: "unknown" }) : settled.kind === "refused" || settled.kind === "retryable" ? settled : unknown(r, "record_failed", receipt);
762
+ }
763
+ if (releaseQuarantine) {
764
+ const recorded = canonical(final.ledger);
765
+ final.ledger.quarantined = false;
766
+ if (await persist(recorded, final.ledger, key, grantCause) !== "saved") {
767
+ await quarantineAccount(key, grantCause);
768
+ return settled.kind === "acknowledged" ? result(r, { ...settled, journal: "unknown" }) : settled;
769
+ }
770
+ }
771
+ return settled;
772
+ }
773
+ catch {
774
+ await quarantineAccount(key);
775
+ return settled.kind === "acknowledged" ? result(r, { ...settled, journal: "unknown" }) : settled.kind === "refused" || settled.kind === "retryable" ? settled : unknown(r, "record_failed", receipt);
776
+ }
777
+ }
778
+ async function run(r, operation) {
779
+ const fallback = { binding, operation, subjectReference: "", requestId: "", generation: 0 };
780
+ try {
781
+ if (!validRequest(r, maxRequestBytes))
782
+ return result(fallback, { kind: "refused", reason: "invalid_request" });
783
+ }
784
+ catch {
785
+ return result(fallback, { kind: "refused", reason: "invalid_request" });
786
+ }
787
+ if (r.operation !== operation)
788
+ return result(r, { kind: "refused", reason: "binding" });
789
+ const ownsAvailability = !availability.busy;
790
+ if (ownsAvailability)
791
+ availability.busy = true;
792
+ try {
793
+ return await execute(r, ownsAvailability);
794
+ }
795
+ catch {
796
+ closeAccount();
797
+ return result({ ...fallback, subjectReference: r.subjectReference, requestId: r.requestId, generation: r.generation }, { kind: "unknown", reason: "record_failed" });
798
+ }
799
+ finally {
800
+ if (ownsAvailability)
801
+ availability.busy = false;
802
+ }
803
+ }
804
+ const scope = copy(input.legacy?.scope ?? { bindingId: binding.namespace, accountReference: binding.account, audienceReference: audience.kind === "audience" ? audience.reference : "account-wide" });
805
+ function legacyRequest(claim) {
806
+ const saved = input.legacy?.claims.get(claim.effectId);
807
+ return saved && equal(saved.claim, claim) && equal(claim.scope, scope) && claim.subjectReference === saved.request.subjectReference && claim.generation === saved.request.generation &&
808
+ claim.deadlineMs === saved.request.deadlineMs && claim.idempotencyKey === saved.request.requestId ? saved.request : undefined;
809
+ }
810
+ const observation = (claim, value, remoteId) => {
811
+ const base = { effectId: claim.effectId, generation: claim.generation, observedAtMs: now() };
812
+ if (value.kind === "acknowledged" && value.journal === "confirmed" && remoteId)
813
+ return { ...base, status: "accepted", remoteReference: remoteId };
814
+ if (value.kind === "retryable")
815
+ return { ...base, status: "throttled", retryAfterMs: value.afterSeconds * 1000 };
816
+ if (value.kind === "refused")
817
+ return { ...base, status: "refused", reason: value.reason === "deadline" ? "deadline" : ["binding", "provenance", "stale", "identity_conflict"].includes(value.reason) ? "scope" : ["unsupported", "verification_pending"].includes(value.reason) ? "unsupported" : "restricted" };
818
+ return { ...base, status: "ambiguous", retryAfterMs: 1000 };
819
+ };
820
+ return {
821
+ capabilities, profiles: { sync: r => run(r, "profile.sync") },
822
+ consent: { read: r => run(r, "consent.read") },
823
+ erasure: { request: r => run(r, "erasure.request"), read: r => run(r, "erasure.read") },
824
+ consentPush: { capabilities, push: r => run(r, "consent.push") },
825
+ newsletter: { providerKind: "klaviyo", capabilities: { subscribe: false, unsubscribe: false, suppress: false, unsuppress: false, doubleOptIn: false, emailChange: false, listMembership: false, webhookIdempotency: false, retryAfter: false },
826
+ async sync() { return { status: "skipped", reason: "unsupported" }; }, async reconcile() { return { status: "skipped", reason: "unsupported" }; } },
827
+ scope,
828
+ async admit(claim) {
829
+ try {
830
+ const r = legacyRequest(claim);
831
+ if (!r || !validRequest(r, maxRequestBytes) || !equal(r.binding, binding))
832
+ return "refused";
833
+ const s = current(r);
834
+ return typeof s === "string" || authority(r, s) || capabilities.operations[r.operation].state !== "supported" ? "refused" : "admitted";
835
+ }
836
+ catch {
837
+ return "refused";
838
+ }
839
+ },
840
+ async apply(claim) {
841
+ const r = legacyRequest(claim);
842
+ if (!r)
843
+ return { effectId: claim.effectId, generation: claim.generation, observedAtMs: now(), status: "refused", reason: "scope" };
844
+ const value = await run(r, r.operation), saved = await load().catch(() => null);
845
+ return observation(claim, value, saved?.ledger.entries[keyOf(r)]?.remoteId);
846
+ },
847
+ async observe(claim) {
848
+ const r = legacyRequest(claim);
849
+ if (!r)
850
+ return { effectId: claim.effectId, generation: claim.generation, observedAtMs: now(), status: "refused", reason: "scope" };
851
+ try {
852
+ const saved = (await load()).ledger.entries[keyOf(r)];
853
+ return saved?.result ? observation(claim, saved.result, saved.remoteId) : null;
854
+ }
855
+ catch {
856
+ return observation(claim, unknown(r, "record_failed"));
857
+ }
858
+ },
859
+ };
860
+ }