@volter/twin-stripe 2.0.38 → 3.0.1

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 (402) hide show
  1. package/README.md +112 -207
  2. package/dist/generated/pack-facts.json +94 -0
  3. package/dist/journeys/coverage.json +596 -0
  4. package/dist/journeys/customer-life.json +6108 -0
  5. package/dist/journeys/customer-life.outline.md +371 -0
  6. package/dist/journeys/decisions.json +677 -0
  7. package/dist/journeys/demand.json +706 -0
  8. package/dist/journeys/score.json +853 -0
  9. package/dist/journeys/unreachable.json +102 -0
  10. package/dist/journeys/vendor-examples.json +8010 -0
  11. package/dist/spec/SOURCE.md +18 -0
  12. package/dist/spec/doc-examples.json +445 -0
  13. package/dist/spec/fixtures3.json.gz +0 -0
  14. package/dist/spec/openapi.json.gz +0 -0
  15. package/dist/spec/patches.json +34 -0
  16. package/dist/spec/sources.json +306 -0
  17. package/dist/src/cli.d.ts +0 -1
  18. package/dist/src/cli.js +11 -31
  19. package/dist/src/engine/after-payment.d.ts +8 -0
  20. package/dist/src/engine/after-payment.js +27 -0
  21. package/dist/src/engine/apps-secrets.d.ts +1 -0
  22. package/dist/src/engine/apps-secrets.js +3 -0
  23. package/dist/src/engine/balance.d.ts +5 -0
  24. package/dist/src/engine/balance.js +15 -0
  25. package/dist/src/engine/checkout.d.ts +24 -0
  26. package/dist/src/engine/checkout.js +30 -0
  27. package/dist/src/engine/common.d.ts +19 -0
  28. package/dist/src/engine/common.js +34 -0
  29. package/dist/src/engine/connect.d.ts +45 -0
  30. package/dist/src/engine/connect.js +108 -0
  31. package/dist/src/engine/customers.d.ts +4 -0
  32. package/dist/src/engine/customers.js +8 -0
  33. package/dist/src/engine/display.d.ts +11 -0
  34. package/dist/src/engine/display.js +13 -0
  35. package/dist/src/engine/files.d.ts +11 -0
  36. package/dist/src/engine/files.js +23 -0
  37. package/dist/src/engine/invoices.d.ts +21 -0
  38. package/dist/src/engine/invoices.js +84 -0
  39. package/dist/src/engine/issuing.d.ts +33 -0
  40. package/dist/src/engine/issuing.js +54 -0
  41. package/dist/src/engine/ledger.d.ts +8 -0
  42. package/dist/src/engine/ledger.js +8 -0
  43. package/dist/src/engine/oauth.d.ts +4 -0
  44. package/dist/src/engine/oauth.js +24 -0
  45. package/dist/src/engine/onboarding.d.ts +13 -0
  46. package/dist/src/engine/onboarding.js +42 -0
  47. package/dist/src/{stripe-params.d.ts → engine/params.d.ts} +20 -3
  48. package/dist/src/{stripe-params.js → engine/params.js} +7 -60
  49. package/dist/src/engine/payment-intents.d.ts +10 -0
  50. package/dist/src/engine/payment-intents.js +27 -0
  51. package/dist/src/engine/payment-methods.d.ts +16 -0
  52. package/dist/src/engine/payment-methods.js +22 -0
  53. package/dist/src/engine/radar.d.ts +2 -0
  54. package/dist/src/engine/radar.js +5 -0
  55. package/dist/src/engine/refunds.d.ts +1 -0
  56. package/dist/src/engine/refunds.js +3 -0
  57. package/dist/src/engine/renewals.d.ts +36 -0
  58. package/dist/src/engine/renewals.js +65 -0
  59. package/dist/src/{stripe-twin.d.ts → engine/stripe.d.ts} +12 -45
  60. package/dist/src/{stripe-twin.js → engine/stripe.js} +53 -387
  61. package/dist/src/engine/subscriptions.d.ts +11 -0
  62. package/dist/src/engine/subscriptions.js +30 -0
  63. package/dist/src/engine/transfers.d.ts +2 -0
  64. package/dist/src/engine/transfers.js +1 -0
  65. package/dist/src/{stripe-version.d.ts → engine/version.d.ts} +0 -2
  66. package/dist/src/{stripe-version.js → engine/version.js} +21 -9
  67. package/dist/src/engine/webhook-endpoints.d.ts +1 -0
  68. package/dist/src/engine/webhook-endpoints.js +12 -0
  69. package/dist/src/engine/webhooks.d.ts +31 -0
  70. package/dist/src/engine/webhooks.js +68 -0
  71. package/dist/src/fetch.d.ts +2 -0
  72. package/dist/src/fetch.js +26 -0
  73. package/dist/src/generated/surface.gen.json +1 -1
  74. package/dist/src/generated/ui.gen.json +1 -1
  75. package/dist/src/index.d.ts +5 -14
  76. package/dist/src/index.js +10 -75
  77. package/dist/src/manifest.js +478 -876
  78. package/dist/src/screens/billing-portal.d.ts +3 -0
  79. package/dist/src/screens/{portal.js → billing-portal.js} +32 -58
  80. package/dist/src/screens/checkout-js.d.ts +2 -0
  81. package/dist/src/screens/checkout-js.js +84 -0
  82. package/dist/src/screens/checkout.d.ts +2 -31
  83. package/dist/src/screens/checkout.js +32 -121
  84. package/dist/src/screens/connect-onboarding.d.ts +3 -0
  85. package/dist/src/screens/{onboarding.js → connect-onboarding.js} +38 -104
  86. package/dist/src/screens/dashboard.d.ts +3 -0
  87. package/dist/src/screens/dashboard.js +116 -0
  88. package/dist/src/screens/shared.d.ts +27 -0
  89. package/dist/src/screens/shared.js +87 -0
  90. package/dist/src/screens/stripe-js.d.ts +3 -0
  91. package/dist/src/screens/stripe-js.js +95 -0
  92. package/dist/src/semantics/account.d.ts +2 -0
  93. package/dist/src/semantics/account.js +8 -0
  94. package/dist/src/semantics/account_links.d.ts +2 -0
  95. package/dist/src/semantics/account_links.js +22 -0
  96. package/dist/src/semantics/accounts.d.ts +4 -0
  97. package/dist/src/semantics/accounts.js +40 -0
  98. package/dist/src/semantics/apps.d.ts +4 -0
  99. package/dist/src/semantics/{apps-secrets.js → apps.js} +14 -26
  100. package/dist/src/semantics/around.d.ts +2 -0
  101. package/dist/src/semantics/around.js +13 -0
  102. package/dist/src/semantics/balance.d.ts +2 -11
  103. package/dist/src/semantics/balance.js +3 -194
  104. package/dist/src/semantics/balance_transactions.d.ts +2 -0
  105. package/dist/src/semantics/balance_transactions.js +15 -0
  106. package/dist/src/semantics/billing_portal.d.ts +4 -0
  107. package/dist/src/semantics/billing_portal.js +36 -0
  108. package/dist/src/semantics/charges.d.ts +3 -28
  109. package/dist/src/semantics/charges.js +5 -202
  110. package/dist/src/semantics/checkout.d.ts +3 -20
  111. package/dist/src/semantics/checkout.js +21 -271
  112. package/dist/src/semantics/clock.d.ts +2 -0
  113. package/dist/src/semantics/clock.js +15 -0
  114. package/dist/src/semantics/coupons.d.ts +2 -6
  115. package/dist/src/semantics/coupons.js +2 -60
  116. package/dist/src/semantics/customers.d.ts +5 -6
  117. package/dist/src/semantics/customers.js +47 -413
  118. package/dist/src/semantics/doors.d.ts +23 -0
  119. package/dist/src/semantics/doors.js +73 -0
  120. package/dist/src/semantics/entitlements.d.ts +0 -2
  121. package/dist/src/semantics/entitlements.js +4 -95
  122. package/dist/src/semantics/events.d.ts +11 -0
  123. package/dist/src/semantics/events.js +31 -0
  124. package/dist/src/semantics/index.d.ts +2 -0
  125. package/dist/src/semantics/index.js +85 -0
  126. package/dist/src/semantics/invoice_payments.d.ts +2 -0
  127. package/dist/src/semantics/invoice_payments.js +12 -0
  128. package/dist/src/semantics/invoices.d.ts +11 -18
  129. package/dist/src/semantics/invoices.js +121 -440
  130. package/dist/src/semantics/issuing.d.ts +6 -13
  131. package/dist/src/semantics/issuing.js +58 -556
  132. package/dist/src/semantics/payment_intents.d.ts +3 -0
  133. package/dist/src/semantics/payment_intents.js +71 -0
  134. package/dist/src/semantics/payment_methods.d.ts +3 -0
  135. package/dist/src/semantics/payment_methods.js +15 -0
  136. package/dist/src/semantics/payouts.d.ts +3 -0
  137. package/dist/src/semantics/payouts.js +28 -0
  138. package/dist/src/semantics/prices.d.ts +4 -0
  139. package/dist/src/semantics/prices.js +37 -0
  140. package/dist/src/semantics/products.d.ts +4 -2
  141. package/dist/src/semantics/products.js +12 -138
  142. package/dist/src/semantics/promotion_codes.d.ts +3 -0
  143. package/dist/src/semantics/promotion_codes.js +35 -0
  144. package/dist/src/semantics/radar.d.ts +2 -2
  145. package/dist/src/semantics/radar.js +5 -74
  146. package/dist/src/semantics/refunds.d.ts +2 -9
  147. package/dist/src/semantics/refunds.js +3 -193
  148. package/dist/src/semantics/shared.d.ts +558 -55
  149. package/dist/src/semantics/shared.js +2070 -114
  150. package/dist/src/semantics/states.d.ts +462 -0
  151. package/dist/src/semantics/states.js +634 -0
  152. package/dist/src/semantics/subscriptions.d.ts +6 -11
  153. package/dist/src/semantics/subscriptions.js +37 -530
  154. package/dist/src/semantics/test_helpers.d.ts +3 -0
  155. package/dist/src/semantics/test_helpers.js +125 -0
  156. package/dist/src/semantics/transfers.d.ts +2 -2
  157. package/dist/src/semantics/transfers.js +6 -101
  158. package/dist/src/semantics/webhook_endpoints.d.ts +2 -0
  159. package/dist/src/semantics/webhook_endpoints.js +26 -0
  160. package/dist/src/server.d.ts +7 -0
  161. package/dist/src/server.js +7 -0
  162. package/generated/pack-facts.json +94 -0
  163. package/journeys/coverage.json +596 -0
  164. package/journeys/customer-life.json +6108 -0
  165. package/journeys/customer-life.outline.md +371 -0
  166. package/journeys/decisions.json +677 -0
  167. package/journeys/demand.json +706 -0
  168. package/journeys/score.json +853 -0
  169. package/journeys/unreachable.json +102 -0
  170. package/journeys/vendor-examples.json +8010 -0
  171. package/package.json +11 -20
  172. package/spec/SOURCE.md +18 -0
  173. package/spec/doc-examples.json +445 -0
  174. package/spec/fixtures3.json.gz +0 -0
  175. package/spec/openapi.json.gz +0 -0
  176. package/spec/patches.json +34 -0
  177. package/spec/sources.json +306 -0
  178. package/src/cli.ts +11 -28
  179. package/src/engine/after-payment.ts +29 -0
  180. package/src/engine/apps-secrets.ts +4 -0
  181. package/src/engine/balance.ts +32 -0
  182. package/src/engine/checkout.ts +39 -0
  183. package/src/engine/common.ts +49 -0
  184. package/src/engine/connect.ts +106 -0
  185. package/src/engine/customers.ts +10 -0
  186. package/src/engine/display.ts +18 -0
  187. package/src/engine/files.ts +28 -0
  188. package/src/engine/invoices.ts +102 -0
  189. package/src/engine/issuing.ts +66 -0
  190. package/src/engine/ledger.ts +48 -0
  191. package/src/engine/oauth.ts +18 -0
  192. package/src/engine/onboarding.ts +53 -0
  193. package/src/{stripe-params.ts → engine/params.ts} +7 -56
  194. package/src/engine/payment-intents.ts +32 -0
  195. package/src/engine/payment-methods.ts +24 -0
  196. package/src/engine/radar.ts +6 -0
  197. package/src/engine/refunds.ts +3 -0
  198. package/src/engine/renewals.ts +72 -0
  199. package/src/{stripe-twin.ts → engine/stripe.ts} +57 -400
  200. package/src/engine/subscriptions.ts +37 -0
  201. package/src/engine/transfers.ts +22 -0
  202. package/src/{stripe-version.ts → engine/version.ts} +19 -10
  203. package/src/engine/webhook-endpoints.ts +14 -0
  204. package/src/engine/webhooks.ts +70 -0
  205. package/src/fetch.ts +27 -0
  206. package/src/generated/surface.gen.json +1 -1
  207. package/src/generated/ui.gen.json +1 -1
  208. package/src/index.ts +10 -116
  209. package/src/manifest.ts +480 -904
  210. package/src/screens/{portal.tsx → billing-portal.tsx} +35 -42
  211. package/src/screens/checkout-js.ts +80 -0
  212. package/src/screens/checkout.tsx +32 -96
  213. package/src/screens/{onboarding.tsx → connect-onboarding.tsx} +39 -85
  214. package/src/screens/dashboard.tsx +178 -0
  215. package/src/screens/shared.tsx +84 -0
  216. package/src/screens/stripe-js.ts +102 -0
  217. package/src/semantics/account.ts +10 -0
  218. package/src/semantics/account_links.ts +20 -0
  219. package/src/semantics/accounts.ts +44 -0
  220. package/src/semantics/{apps-secrets.ts → apps.ts} +17 -33
  221. package/src/semantics/around.ts +17 -0
  222. package/src/semantics/balance.ts +6 -208
  223. package/src/semantics/balance_transactions.ts +18 -0
  224. package/src/semantics/billing_portal.ts +45 -0
  225. package/src/semantics/charges.ts +8 -212
  226. package/src/semantics/checkout.ts +25 -276
  227. package/src/semantics/clock.ts +19 -0
  228. package/src/semantics/coupons.ts +5 -68
  229. package/src/semantics/customers.ts +48 -414
  230. package/src/semantics/doors.ts +79 -0
  231. package/src/semantics/entitlements.ts +3 -94
  232. package/src/semantics/events.ts +37 -0
  233. package/src/semantics/index.ts +89 -0
  234. package/src/semantics/invoice_payments.ts +16 -0
  235. package/src/semantics/invoices.ts +112 -441
  236. package/src/semantics/issuing.ts +57 -573
  237. package/src/semantics/payment_intents.ts +76 -0
  238. package/src/semantics/payment_methods.ts +20 -0
  239. package/src/semantics/payouts.ts +29 -0
  240. package/src/semantics/prices.ts +41 -0
  241. package/src/semantics/products.ts +12 -146
  242. package/src/semantics/promotion_codes.ts +38 -0
  243. package/src/semantics/radar.ts +8 -79
  244. package/src/semantics/refunds.ts +4 -216
  245. package/src/semantics/shared.ts +2089 -135
  246. package/src/semantics/states.ts +662 -0
  247. package/src/semantics/subscriptions.ts +39 -542
  248. package/src/semantics/test_helpers.ts +119 -0
  249. package/src/semantics/transfers.ts +10 -132
  250. package/src/semantics/webhook_endpoints.ts +27 -0
  251. package/src/server.ts +7 -0
  252. package/client/dashboard-api.ts +0 -286
  253. package/client/stripe-mirror.css +0 -277
  254. package/client/stripe-mirror.tsx +0 -1559
  255. package/dist/client/dashboard-api.d.ts +0 -107
  256. package/dist/client/dashboard-api.js +0 -238
  257. package/dist/client/dashboard-api.ts +0 -286
  258. package/dist/client/stripe-mirror.bundle.js +0 -236
  259. package/dist/client/stripe-mirror.css +0 -277
  260. package/dist/client/stripe-mirror.d.ts +0 -134
  261. package/dist/client/stripe-mirror.js +0 -859
  262. package/dist/client/stripe-mirror.tsx +0 -1559
  263. package/dist/src/screens/connect-oauth.d.ts +0 -27
  264. package/dist/src/screens/connect-oauth.js +0 -414
  265. package/dist/src/screens/connect-settings.d.ts +0 -22
  266. package/dist/src/screens/connect-settings.js +0 -103
  267. package/dist/src/screens/consent-skin.d.ts +0 -4
  268. package/dist/src/screens/consent-skin.js +0 -18
  269. package/dist/src/screens/financial-connections.d.ts +0 -5
  270. package/dist/src/screens/financial-connections.js +0 -90
  271. package/dist/src/screens/identity.d.ts +0 -5
  272. package/dist/src/screens/identity.js +0 -86
  273. package/dist/src/screens/onboarding.d.ts +0 -13
  274. package/dist/src/screens/portal.d.ts +0 -5
  275. package/dist/src/screens/public-details.d.ts +0 -5
  276. package/dist/src/screens/public-details.js +0 -90
  277. package/dist/src/semantics/after-payment.d.ts +0 -22
  278. package/dist/src/semantics/after-payment.js +0 -99
  279. package/dist/src/semantics/apps-secrets.d.ts +0 -2
  280. package/dist/src/semantics/billing.d.ts +0 -2
  281. package/dist/src/semantics/billing.js +0 -220
  282. package/dist/src/semantics/connect.d.ts +0 -5
  283. package/dist/src/semantics/connect.js +0 -493
  284. package/dist/src/semantics/credit-notes.d.ts +0 -2
  285. package/dist/src/semantics/credit-notes.js +0 -172
  286. package/dist/src/semantics/disputes.d.ts +0 -2
  287. package/dist/src/semantics/disputes.js +0 -51
  288. package/dist/src/semantics/ephemeral-keys.d.ts +0 -2
  289. package/dist/src/semantics/ephemeral-keys.js +0 -34
  290. package/dist/src/semantics/files.d.ts +0 -2
  291. package/dist/src/semantics/files.js +0 -125
  292. package/dist/src/semantics/ledger.d.ts +0 -59
  293. package/dist/src/semantics/ledger.js +0 -200
  294. package/dist/src/semantics/payment-intents.d.ts +0 -18
  295. package/dist/src/semantics/payment-intents.js +0 -452
  296. package/dist/src/semantics/payment-links.d.ts +0 -2
  297. package/dist/src/semantics/payment-links.js +0 -133
  298. package/dist/src/semantics/payment-methods.d.ts +0 -20
  299. package/dist/src/semantics/payment-methods.js +0 -140
  300. package/dist/src/semantics/plans.d.ts +0 -5
  301. package/dist/src/semantics/plans.js +0 -121
  302. package/dist/src/semantics/platform.d.ts +0 -9
  303. package/dist/src/semantics/platform.js +0 -206
  304. package/dist/src/semantics/renewals.d.ts +0 -47
  305. package/dist/src/semantics/renewals.js +0 -281
  306. package/dist/src/semantics/setup-intents.d.ts +0 -2
  307. package/dist/src/semantics/setup-intents.js +0 -84
  308. package/dist/src/semantics/subscription-schedules.d.ts +0 -2
  309. package/dist/src/semantics/subscription-schedules.js +0 -119
  310. package/dist/src/semantics/tax.d.ts +0 -2
  311. package/dist/src/semantics/tax.js +0 -197
  312. package/dist/src/semantics/terminal.d.ts +0 -5
  313. package/dist/src/semantics/terminal.js +0 -182
  314. package/dist/src/semantics/test-clocks.d.ts +0 -6
  315. package/dist/src/semantics/test-clocks.js +0 -73
  316. package/dist/src/semantics/tokens.d.ts +0 -4
  317. package/dist/src/semantics/tokens.js +0 -44
  318. package/dist/src/semantics/treasury.d.ts +0 -2
  319. package/dist/src/semantics/treasury.js +0 -377
  320. package/dist/src/semantics/webhook-endpoints.d.ts +0 -3
  321. package/dist/src/semantics/webhook-endpoints.js +0 -85
  322. package/dist/src/stripe-budget.d.ts +0 -55
  323. package/dist/src/stripe-budget.js +0 -155
  324. package/dist/src/stripe-capabilities.d.ts +0 -3
  325. package/dist/src/stripe-capabilities.js +0 -5836
  326. package/dist/src/stripe-conformance.d.ts +0 -43
  327. package/dist/src/stripe-conformance.js +0 -105
  328. package/dist/src/stripe-connector.d.ts +0 -161
  329. package/dist/src/stripe-connector.js +0 -414
  330. package/dist/src/stripe-emit.d.ts +0 -2
  331. package/dist/src/stripe-emit.js +0 -145
  332. package/dist/src/stripe-events.d.ts +0 -93
  333. package/dist/src/stripe-events.js +0 -392
  334. package/dist/src/stripe-js.d.ts +0 -4
  335. package/dist/src/stripe-js.js +0 -70
  336. package/dist/src/stripe-mirror-ui.d.ts +0 -15
  337. package/dist/src/stripe-mirror-ui.js +0 -87
  338. package/dist/src/stripe-perform-harness.d.ts +0 -9
  339. package/dist/src/stripe-perform-harness.js +0 -26
  340. package/dist/src/stripe-server.d.ts +0 -33
  341. package/dist/src/stripe-server.js +0 -429
  342. package/dist/src/stripe-shared.d.ts +0 -109
  343. package/dist/src/stripe-shared.js +0 -276
  344. package/dist/src/stripe-ui-conformance.d.ts +0 -5
  345. package/dist/src/stripe-ui-conformance.js +0 -79
  346. package/dist/src/stripe-ui-structure.d.ts +0 -3
  347. package/dist/src/stripe-ui-structure.js +0 -168
  348. package/dist/test-fixtures/stripe-known-deviations.json +0 -110
  349. package/dist/test-fixtures/stripe-openapi-operations.SOURCE.md +0 -14
  350. package/dist/test-fixtures/stripe-openapi-operations.json +0 -4717
  351. package/dist/test-fixtures/stripe-schemas.SOURCE.md +0 -35
  352. package/dist/test-fixtures/stripe-schemas.json +0 -3813
  353. package/src/screens/connect-oauth.tsx +0 -400
  354. package/src/screens/connect-settings.tsx +0 -121
  355. package/src/screens/consent-skin.ts +0 -20
  356. package/src/screens/financial-connections.tsx +0 -101
  357. package/src/screens/identity.tsx +0 -96
  358. package/src/screens/public-details.tsx +0 -105
  359. package/src/semantics/after-payment.ts +0 -118
  360. package/src/semantics/billing.ts +0 -216
  361. package/src/semantics/connect.ts +0 -487
  362. package/src/semantics/credit-notes.ts +0 -168
  363. package/src/semantics/disputes.ts +0 -62
  364. package/src/semantics/ephemeral-keys.ts +0 -34
  365. package/src/semantics/files.ts +0 -143
  366. package/src/semantics/ledger.ts +0 -253
  367. package/src/semantics/payment-intents.ts +0 -463
  368. package/src/semantics/payment-links.ts +0 -148
  369. package/src/semantics/payment-methods.ts +0 -145
  370. package/src/semantics/plans.ts +0 -131
  371. package/src/semantics/platform.ts +0 -220
  372. package/src/semantics/renewals.ts +0 -279
  373. package/src/semantics/setup-intents.ts +0 -87
  374. package/src/semantics/subscription-schedules.ts +0 -129
  375. package/src/semantics/tax.ts +0 -220
  376. package/src/semantics/terminal.ts +0 -195
  377. package/src/semantics/test-clocks.ts +0 -77
  378. package/src/semantics/tokens.ts +0 -52
  379. package/src/semantics/treasury.ts +0 -383
  380. package/src/semantics/webhook-endpoints.ts +0 -87
  381. package/src/stripe-budget.ts +0 -181
  382. package/src/stripe-capabilities.ts +0 -5651
  383. package/src/stripe-conformance.ts +0 -124
  384. package/src/stripe-connector.ts +0 -462
  385. package/src/stripe-emit.ts +0 -157
  386. package/src/stripe-events.ts +0 -434
  387. package/src/stripe-js.ts +0 -70
  388. package/src/stripe-mirror-ui.ts +0 -90
  389. package/src/stripe-perform-harness.ts +0 -29
  390. package/src/stripe-server.ts +0 -393
  391. package/src/stripe-shared.ts +0 -297
  392. package/src/stripe-ui-conformance.ts +0 -88
  393. package/src/stripe-ui-structure.ts +0 -182
  394. package/test-fixtures/stripe-known-deviations.json +0 -110
  395. package/test-fixtures/stripe-openapi-operations.json +0 -4717
  396. package/test-fixtures/stripe-schemas.json +0 -3813
  397. /package/dist/src/{screens → engine}/industries.d.ts +0 -0
  398. /package/dist/src/{screens → engine}/industries.js +0 -0
  399. /package/dist/src/{semantics → engine}/test-cards.d.ts +0 -0
  400. /package/dist/src/{semantics → engine}/test-cards.js +0 -0
  401. /package/src/{screens → engine}/industries.ts +0 -0
  402. /package/src/{semantics → engine}/test-cards.ts +0 -0
@@ -1,157 +0,0 @@
1
- // The Stripe DELIVER verb — `world-stripe emit` (feature-sweep friction: hand-constructing
2
- // checkout.session.completed / invoice.paid envelopes + HMAC signing to poke an app's webhook
3
- // handler). This is the pack side of the kernel's emit seam (@volter/world-core emit.ts): the
4
- // catalog of emittable event types, endpoint registrations read FROM TWIN STATE AT REST
5
- // (webhook_endpoint rows minted by POST /v1/webhook_endpoints — the CLI runs in its own
6
- // process, so the in-memory delivery registry in stripe-events.ts does not apply), and
7
- // synthesis of the exact envelope real Stripe delivers, signed with the DESTINATION
8
- // endpoint's own whsec_twin_* so `stripe.webhooks.constructEvent(rawBody, header, secret)`
9
- // in the app's unmodified SDK verifies the delivery unchanged.
10
- //
11
- // DELIVER does not transition state: it snapshots the subject AS IT IS in the twin and fires
12
- // the named event about it. Completing a checkout session / paying an invoice is the twin
13
- // API's job; this verb answers "my app's webhook handler needs to SEE the event, now."
14
- import { projectResources, worldNow, type EmitEndpoint, type EmittableEvent, type SynthesizedDelivery, type TwinEmitter } from '@volter/world-core';
15
- import { OBJECT_NAME, PLATFORM_ACCOUNT_ID, TWIN_API_VERSION, view } from './stripe-twin.ts';
16
- import { STRIPE_WEBHOOK_FALLBACK_SECRET, generateTestHeaderString } from './stripe-events.ts';
17
- import { render } from './stripe-version.ts';
18
-
19
- const SERVICE = 'stripe';
20
-
21
- // event type → the twin resource type whose current state becomes `data.object`.
22
- // The catalog is every event type the twin's own write path can produce (eventTypeFor's
23
- // range) — the emit verb can re-fire anything the twin emits organically.
24
- const EMITTABLE: Record<string, string> = {
25
- // payment intents
26
- 'payment_intent.created': 'payment_intent',
27
- 'payment_intent.succeeded': 'payment_intent',
28
- 'payment_intent.processing': 'payment_intent',
29
- 'payment_intent.requires_action': 'payment_intent',
30
- 'payment_intent.amount_capturable_updated': 'payment_intent',
31
- 'payment_intent.payment_failed': 'payment_intent',
32
- 'payment_intent.canceled': 'payment_intent',
33
- // charges + disputes
34
- 'charge.succeeded': 'charge',
35
- 'charge.failed': 'charge',
36
- 'charge.captured': 'charge',
37
- 'charge.refunded': 'charge',
38
- 'charge.dispute.created': 'dispute',
39
- // customers
40
- 'customer.created': 'customer',
41
- 'customer.updated': 'customer',
42
- 'customer.deleted': 'customer',
43
- // checkout — the sweep's exact pain
44
- 'checkout.session.completed': 'checkout_session',
45
- // invoices — the sweep's exact pain
46
- 'invoice.created': 'invoice',
47
- 'invoice.finalized': 'invoice',
48
- 'invoice.paid': 'invoice',
49
- 'invoice.payment_failed': 'invoice',
50
- 'invoice.voided': 'invoice',
51
- 'invoice.marked_uncollectible': 'invoice',
52
- 'invoice.sent': 'invoice',
53
- // subscriptions
54
- 'customer.subscription.created': 'subscription',
55
- 'customer.subscription.updated': 'subscription',
56
- 'customer.subscription.deleted': 'subscription',
57
- 'customer.subscription.paused': 'subscription',
58
- 'customer.subscription.resumed': 'subscription',
59
- // payment methods
60
- 'payment_method.attached': 'payment_method',
61
- 'payment_method.detached': 'payment_method',
62
- // setup intents
63
- 'setup_intent.created': 'setup_intent',
64
- 'setup_intent.succeeded': 'setup_intent',
65
- // payouts, catalog
66
- 'payout.created': 'payout',
67
- 'payout.paid': 'payout',
68
- 'payout.canceled': 'payout',
69
- 'price.created': 'price',
70
- 'product.created': 'product',
71
- // issuing — the settlement consumer's diet (issuing_transaction.created) plus the
72
- // authorization/card/cardholder lifecycle. issuing_authorization.request is emittable
73
- // too: DELIVER re-fires the request event about an authorization AS IT IS in twin state
74
- // (the organic synchronous leg lives in the present-authorization flow; this verb lets
75
- // an app's handler see the event again without re-presenting).
76
- 'issuing_authorization.created': 'issuing_authorization',
77
- 'issuing_authorization.request': 'issuing_authorization',
78
- 'issuing_authorization.updated': 'issuing_authorization',
79
- 'issuing_card.created': 'issuing_card',
80
- 'issuing_card.updated': 'issuing_card',
81
- 'issuing_cardholder.created': 'issuing_cardholder',
82
- 'issuing_cardholder.updated': 'issuing_cardholder',
83
- 'issuing_transaction.created': 'issuing_transaction',
84
- };
85
-
86
- function rows(type: string, root?: string): Array<Record<string, unknown>> {
87
- return projectResources(SERVICE, root).filter((r) => r.type === type);
88
- }
89
-
90
- function liveEndpointRows(root?: string): Array<Record<string, unknown>> {
91
- return rows('webhook_endpoint', root).filter((w) => w.deleted !== true);
92
- }
93
-
94
- let emitSeq = 0;
95
-
96
- export const stripeEmitter: TwinEmitter = {
97
- vendor: 'stripe',
98
-
99
- events(): EmittableEvent[] {
100
- return Object.entries(EMITTABLE).map(([type, subjectType]) => ({ type, subjectType }));
101
- },
102
-
103
- endpoints(root?: string): EmitEndpoint[] {
104
- return liveEndpointRows(root).map((w) => ({
105
- id: String(w.id),
106
- url: String(w.url ?? ''),
107
- ...(Array.isArray(w.enabled_events) ? { enabledEvents: w.enabled_events.map(String) } : {}),
108
- }));
109
- },
110
-
111
- subjects(subjectType: string, root?: string): string[] {
112
- return rows(subjectType, root).filter((r) => r.deleted !== true).map((r) => String(r.id));
113
- },
114
-
115
- synthesize({ type, subjectId, endpoint, root, occurredAt }): SynthesizedDelivery {
116
- const subjectType = EMITTABLE[type];
117
- if (!subjectType) throw new Error(`emit: the stripe pack cannot synthesize "${type}".`);
118
- const row = rows(subjectType, root).find((r) => String(r.id) === subjectId && r.deleted !== true);
119
- if (!row) {
120
- const have = stripeEmitter.subjects(subjectType, root);
121
- throw new Error(
122
- `emit: no ${subjectType} "${subjectId}" in stripe twin state — cannot synthesize ${type}. ` +
123
- (have.length ? `${subjectType} ids in state: ${have.join(', ')}.` : `No ${subjectType} exists in this twin yet (object name: ${OBJECT_NAME[subjectType] ?? subjectType}) — create one through the vendor API first.`),
124
- );
125
- }
126
- // The endpoint's own signing secret, from its state row (real Stripe signs per-endpoint).
127
- const endpointRow = liveEndpointRows(root).find((w) => String(w.id) === endpoint.id || String(w.url) === endpoint.url);
128
- const secret = typeof endpointRow?.secret === 'string' && endpointRow.secret ? endpointRow.secret : STRIPE_WEBHOOK_FALLBACK_SECRET;
129
-
130
- const created = occurredAt ? Math.floor(Date.parse(occurredAt) / 1000) : Math.floor(Date.parse(worldNow()) / 1000);
131
- // a connected account's object is its event's, as the write path scopes it (stripe-twin.ts afterStripeWrite): the
132
- // account itself, or a row kept on its books (`_account`). "Each event for a connected account contains a top-level
133
- // `account` property that identifies the connected account" (docs.stripe.com/connect/webhooks).
134
- const account = typeof row._account === 'string' ? row._account : subjectType === 'account' && String(row.id) !== PLATFORM_ACCOUNT_ID ? String(row.id) : undefined;
135
- // Same envelope persistStripeEvent stores for GET /v1/events; `evt_twin_emit_*` marks it
136
- // operator-fired (never colliding with the write path's organic `evt_twin_<n>` ids).
137
- const event: Record<string, unknown> = {
138
- id: `evt_twin_emit_${created}_${++emitSeq}`,
139
- object: 'event',
140
- api_version: TWIN_API_VERSION,
141
- created,
142
- data: { object: view(subjectType, row) },
143
- livemode: false,
144
- pending_webhooks: 1,
145
- request: { id: null, idempotency_key: null },
146
- type,
147
- ...(account ? { account } : {}),
148
- };
149
- const payload = JSON.stringify(render(event, TWIN_API_VERSION));
150
- const header = generateTestHeaderString({ payload, secret, ...(occurredAt ? { timestamp: created } : {}) });
151
- return {
152
- payload,
153
- headers: { 'content-type': 'application/json', 'stripe-signature': header },
154
- event,
155
- };
156
- },
157
- };
@@ -1,434 +0,0 @@
1
- import { nodeBuiltin, worldNow, deliveryTraceHeaders } from '@volter/world-core';
2
- import { appDestination, appFetch } from '@volter/world-core/app-route';
3
- import { worldEgressRefusal } from '@volter/world-core/network-policy';
4
- import vendorEvents from './generated/events.gen.json' with { type: 'json' };
5
- import { render } from './stripe-version.ts';
6
- // Stripe event/webhook emission (scorecard R17). Real Stripe fires `event`
7
- // objects (and webhooks) on state changes — payment_intent.succeeded,
8
- // customer.subscription.created, invoice.paid, … — so an app's webhook handler
9
- // runs. A faithful twin must too. On a twin write we build the Stripe `event`
10
- // envelope and deliver it to registered endpoints (injected delivery: fake in
11
- // tests, HTTP POST live, failures swallowed like the real vendor).
12
- export type StripeEvent = {
13
- id: string;
14
- object: 'event';
15
- type: string;
16
- created: number;
17
- livemode: false;
18
- /** the connected account a Connect event is from */
19
- account?: string;
20
- data: { object: Record<string, unknown> };
21
- };
22
- // A deliverer MAY return the endpoint's raw HTTP response. Ordinary (asynchronous) event
23
- // emission ignores the return value; the SYNCHRONOUS real-time-authorization leg
24
- // (issuing_authorization.request) reads it to honor the endpoint's approve/decline. A
25
- // fire-and-forget deliverer returning void stays valid for the asynchronous path.
26
- export type StripeWebhookEndpointResponse = { status: number; body: string };
27
- export type StripeEventDelivery = (url: string, event: StripeEvent, secret?: string) => Promise<void | StripeWebhookEndpointResponse> | void | StripeWebhookEndpointResponse;
28
- /** A webhook endpoint a World holds in its tree: where to POST, what to sign with, what it subscribed to. */
29
- export type StripeWebhookTarget = { url: string; secret?: string; enabledEvents?: string[]; /** a Connect endpoint (`connect: true`): events from connected accounts */ connect?: boolean };
30
-
31
- // ── Webhook signature verification (Stripe's scheme) ────────────────────────────
32
- // Real Stripe signs each webhook delivery with an HMAC-SHA256 over `${timestamp}.${payload}`
33
- // keyed by the endpoint's signing secret, and sends it in the `Stripe-Signature` header as
34
- // `t=<unix>,v1=<hex-sig>`. The SDK's `stripe.webhooks.constructEvent(payload, header, secret)`
35
- // recomputes the signature and 400s on a mismatch / stale timestamp; tests build the header
36
- // with `stripe.webhooks.generateTestHeaderString(...)`. We reproduce BOTH, byte-for-byte, so a
37
- // real `stripe` SDK consumer verifies twin-delivered webhooks unchanged. Pure Node crypto —
38
- // no network, fully deterministic. `node:crypto` is required lazily (inside the functions that
39
- // use it) rather than as a top-level static import, so this module can be pulled into the
40
- // browser bundle of the UI mirror without Bun's browser target choking on `node:crypto` — the
41
- // signature helpers are server-only and never reached in the browser path.
42
- type NodeCrypto = typeof import('node:crypto');
43
- function nodeCrypto(): NodeCrypto {
44
- // eslint-disable-next-line @typescript-eslint/no-require-imports
45
- return nodeBuiltin('node:crypto') as NodeCrypto;
46
- }
47
-
48
- /** HMAC-SHA256(secret, `${timestamp}.${payload}`) as lowercase hex — Stripe's v1 signature. */
49
- export function computeStripeSignature(payload: string, secret: string, timestamp: number): string {
50
- return nodeCrypto().createHmac('sha256', secret).update(`${timestamp}.${payload}`, 'utf8').digest('hex');
51
- }
52
-
53
- /** Build a `Stripe-Signature` header value for `payload` (mirrors generateTestHeaderString). */
54
- export function generateTestHeaderString(opts: { payload: string; secret: string; timestamp?: number; scheme?: string }): string {
55
- // signed at the World's time, as its application reads the time (a moved World clock; the app keeps it too)
56
- const timestamp = opts.timestamp ?? Math.floor(Date.parse(worldNow()) / 1000);
57
- const scheme = opts.scheme ?? 'v1';
58
- const signature = computeStripeSignature(opts.payload, opts.secret, timestamp);
59
- return `t=${timestamp},${scheme}=${signature}`;
60
- }
61
-
62
- /** Parse a `Stripe-Signature` header into its timestamp + the list of v1 signatures. */
63
- function parseSignatureHeader(header: string): { timestamp: number; signatures: string[] } {
64
- let timestamp = -1;
65
- const signatures: string[] = [];
66
- for (const part of header.split(',')) {
67
- const eq = part.indexOf('=');
68
- if (eq === -1) continue;
69
- const key = part.slice(0, eq).trim();
70
- const value = part.slice(eq + 1).trim();
71
- if (key === 't') timestamp = Number(value);
72
- else if (key === 'v1') signatures.push(value);
73
- }
74
- return { timestamp, signatures };
75
- }
76
-
77
- export class StripeSignatureVerificationError extends Error {
78
- constructor(message: string) {
79
- super(message);
80
- this.name = 'StripeSignatureVerificationError';
81
- }
82
- }
83
-
84
- /**
85
- * Verify a webhook payload + signature header against the signing secret and return the
86
- * parsed event (mirrors `stripe.webhooks.constructEvent`). Throws a
87
- * StripeSignatureVerificationError on a malformed/missing header, a signature that does
88
- * not match, or (when `tolerance` is given) a timestamp outside the allowed window.
89
- */
90
- export function constructEvent(payload: string, header: string, secret: string, opts: { tolerance?: number; now?: number } = {}): StripeEvent {
91
- if (!header) throw new StripeSignatureVerificationError('No signatures found matching the expected signature for payload.');
92
- const { timestamp, signatures } = parseSignatureHeader(header);
93
- if (timestamp < 0 || signatures.length === 0) {
94
- throw new StripeSignatureVerificationError('Unable to extract timestamp and signatures from header');
95
- }
96
- const expected = computeStripeSignature(payload, secret, timestamp);
97
- const expectedBuf = Buffer.from(expected, 'utf8');
98
- const matched = signatures.some((s) => {
99
- const sBuf = Buffer.from(s, 'utf8');
100
- return sBuf.length === expectedBuf.length && nodeCrypto().timingSafeEqual(sBuf, expectedBuf);
101
- });
102
- if (!matched) {
103
- throw new StripeSignatureVerificationError('No signatures found matching the expected signature for payload.');
104
- }
105
- if (opts.tolerance !== undefined) {
106
- const now = opts.now ?? Math.floor(Date.parse(worldNow()) / 1000);
107
- if (now - timestamp > opts.tolerance) {
108
- throw new StripeSignatureVerificationError('Timestamp outside the tolerance zone');
109
- }
110
- }
111
- return JSON.parse(payload) as StripeEvent;
112
- }
113
-
114
- const registry: string[] = [];
115
- // URLs a HOST registered directly (registerStripeWebhook), with the secret to sign with and the
116
- // enabled_events to filter on. A World's own endpoints are not here: POST /v1/webhook_endpoints
117
- // writes the WebhookEndpoint to the tree, and every emit reads the tree (the caller passes them as
118
- // `endpoints`), so delivery survives a restart and belongs to one World. A URL registered here
119
- // without a secret is signed with the fallback below, so the header is still a real, verifiable
120
- // signature; one registered without a list receives every event, equivalent to ['*'].
121
- const secrets = new Map<string, string>();
122
- const subscriptions = new Map<string, string[]>();
123
- export const STRIPE_WEBHOOK_FALLBACK_SECRET = 'whsec_twin_default';
124
- export function registerStripeWebhook(url: string, secret?: string, enabledEvents?: string[]): void {
125
- if (!registry.includes(url)) registry.push(url);
126
- if (secret) secrets.set(url, secret);
127
- if (enabledEvents) subscriptions.set(url, enabledEvents.map(String));
128
- }
129
- export function unregisterStripeWebhook(url: string): void {
130
- const i = registry.indexOf(url);
131
- if (i !== -1) registry.splice(i, 1);
132
- secrets.delete(url);
133
- subscriptions.delete(url);
134
- }
135
- export function clearStripeWebhooks(): void {
136
- registry.length = 0;
137
- secrets.clear();
138
- subscriptions.clear();
139
- }
140
-
141
- // Does an enabled_events list subscribe an endpoint to `type`? Stripe semantics: '*'
142
- // matches everything, 'issuing_authorization.*' matches the family, else exact. Shared by
143
- // the organic fan-out below and the synchronous issuing leg's enrollment check.
144
- export function stripeEventMatches(enabledEvents: unknown, type: string): boolean {
145
- if (!Array.isArray(enabledEvents)) return false;
146
- return enabledEvents.some((p) => {
147
- const s = String(p);
148
- return s === '*' || (s.endsWith('.*') && type.startsWith(s.slice(0, -1))) || s === type;
149
- });
150
- }
151
- export function listStripeWebhooks(): string[] {
152
- return [...registry];
153
- }
154
-
155
- // twin write operation → Stripe event type (the ones an app's webhooks care about).
156
- // Returns null when an operation has no event (e.g. a plain create/retrieve we
157
- // don't model an event for).
158
- const VENDOR_EVENT_TYPES = new Set((vendorEvents as { types: string[] }).types);
159
-
160
- export function eventTypeFor(operation: string): string | null {
161
- // Several twin write operations ARE already the Stripe event type (we record the
162
- // op as `<resource>.<event>` so it maps 1:1): payment_intent.amount_capturable_updated,
163
- // payment_intent.canceled, payment_intent.succeeded. Those pass through directly.
164
- const PASSTHROUGH = new Set([
165
- 'payment_intent.amount_capturable_updated', 'payment_intent.canceled', 'payment_intent.succeeded',
166
- 'payment_intent.payment_failed', 'payment_intent.created', 'payment_intent.processing',
167
- 'payment_intent.requires_action',
168
- 'charge.succeeded', 'charge.captured', 'charge.refunded', 'charge.dispute.created',
169
- // A refund fires TWO real Stripe events: `refund.created` carrying the Refund, and
170
- // `charge.refunded` carrying the CHARGE with its new totals. The twin records the charge's
171
- // own write as `charge.refunded`, so the event a consumer receives is the charge — which is
172
- // the whole point of that event and the reason a reconciler subscribes to it.
173
- 'refund.created', 'refund.updated',
174
- 'customer.updated', 'customer.deleted', 'invoice.created', 'invoice.finalized',
175
- 'invoice.paid', 'invoice.payment_failed', 'invoice.voided', 'invoice.marked_uncollectible', 'invoice.sent',
176
- 'customer.subscription.updated', 'customer.subscription.paused', 'customer.subscription.resumed',
177
- 'payout.created', 'payout.paid', 'payout.canceled', 'price.created', 'product.created',
178
- 'setup_intent.succeeded', 'setup_intent.created',
179
- // The twin's checkout completion transition records the op AS the real event type:
180
- // real Stripe fires checkout.session.completed (data.object = the completed Session)
181
- // the moment a session reaches status=complete — the event most billing integrations
182
- // fulfill from, so it must pass through, not be swallowed.
183
- 'checkout.session.completed',
184
- // Issuing writes record the op AS the real event type (issuing_authorization.updated on
185
- // approve/decline/update, issuing_card.updated on card updates, …) — all real vendor
186
- // event types (stripe@22.3.0 WebhookEndpoint.EnabledEvent enum).
187
- 'issuing_authorization.updated', 'issuing_card.updated', 'issuing_cardholder.updated',
188
- 'issuing_transaction.updated',
189
- ]);
190
- if (PASSTHROUGH.has(operation)) return operation;
191
- switch (operation) {
192
- case 'customer.create': return 'customer.created';
193
- case 'payment_intent.create': return 'payment_intent.created';
194
- case 'payment_intent.confirm': return 'payment_intent.succeeded';
195
- case 'payment_method.attach': return 'payment_method.attached';
196
- case 'payment_method.detach': return 'payment_method.detached';
197
- case 'setup_intent.confirm': return 'setup_intent.succeeded';
198
- case 'subscription.create': return 'customer.subscription.created';
199
- case 'subscription.update': return 'customer.subscription.updated';
200
- case 'subscription.cancel': return 'customer.subscription.deleted';
201
- case 'invoice.finalize': return 'invoice.finalized';
202
- case 'invoice.pay': return 'invoice.paid';
203
- case 'invoice.void': return 'invoice.voided';
204
- case 'refund.create': return 'refund.created';
205
- case 'dispute.create': return 'charge.dispute.created';
206
- case 'payout.create': return 'payout.created';
207
- case 'product.create': return 'product.created';
208
- case 'price.create': return 'price.created';
209
- // Issuing: real Stripe fires issuing_authorization.created/.updated,
210
- // issuing_card.created/.updated, issuing_cardholder.created/.updated and
211
- // issuing_transaction.created (all present in the vendor SDK's
212
- // WebhookEndpoint.EnabledEvent enum, stripe@22.3.0 WebhookEndpoints.d.ts).
213
- // issuing_authorization.request is deliberately NOT here: it is not a
214
- // state-change event — the twin delivers it SYNCHRONOUSLY from the
215
- // present-authorization flow (requestAuthorizationDecision below).
216
- case 'issuing_authorization.create': return 'issuing_authorization.created';
217
- case 'issuing_card.create': return 'issuing_card.created';
218
- case 'issuing_cardholder.create': return 'issuing_cardholder.created';
219
- case 'issuing_transaction.create': return 'issuing_transaction.created';
220
- default: return vendorEventType(operation);
221
- }
222
- }
223
-
224
- /** Any other write records the event Stripe sends for it, when Stripe sends one: the operation itself, or its
225
- * resource's create/update/delete as `<resource>.created|updated|deleted` (the types Stripe's spec enumerates). */
226
- function vendorEventType(operation: string): string | null {
227
- if (VENDOR_EVENT_TYPES.has(operation)) return operation;
228
- const verb = /^(.+)\.(create|update|delete)$/.exec(operation);
229
- return verb && VENDOR_EVENT_TYPES.has(`${verb[1]}.${verb[2]}d`) ? `${verb[1]}.${verb[2]}d` : null;
230
- }
231
-
232
- /** An event's id, DERIVED from the event itself — the operation, the resource it carries and the instant —
233
- * never a process counter (protocol 2: two identical worlds deliver identically, and a replay of the same
234
- * write raises the same event id rather than one that drifts with process uptime). */
235
- function eventId(operation: string, resource: Record<string, unknown>, occurredAt: string, account?: string): string {
236
- let h = 0x811c9dc5;
237
- const seed = `${operation}|${String((resource as { id?: unknown }).id ?? '')}|${occurredAt}|${account ?? ''}`;
238
- for (let i = 0; i < seed.length; i += 1) { h ^= seed.charCodeAt(i); h = Math.imul(h, 0x01000193) >>> 0; }
239
- return `evt_twin_${h.toString(16).padStart(8, '0')}`;
240
- }
241
- // Live HTTP delivery signs exactly like real Stripe: HMAC-SHA256 over
242
- // `${timestamp}.${payload}` keyed by the ENDPOINT'S OWN signing secret (the
243
- // `whsec_twin_*` minted by POST /v1/webhook_endpoints and registered alongside the
244
- // URL), sent as `Stripe-Signature: t=...,v1=...`. The signed bytes are the exact
245
- // request body, so `stripe.webhooks.constructEvent(rawBody, header, secret)` in the
246
- // consumer's real SDK verifies twin deliveries unchanged (a hardcoded placeholder
247
- // header made every verifying consumer 400 — the delivery was unverifiable).
248
- // Delivery is AT-LEAST-ONCE, like the vendor: real Stripe re-attempts a webhook whose
249
- // delivery fails at the transport layer rather than dropping it, so a consumer that missed
250
- // one event because a socket blipped is a consumer real Stripe would have reached. A single
251
- // swallowed `fetch` rejection here is indistinguishable, downstream, from the consumer
252
- // having a bug — it cost a week of "the settlement webhook is racy" before anyone could see
253
- // that the event had simply never left the twin. Two cheap re-attempts cover a transient
254
- // local failure (ECONNRESET / socket exhaustion under a boot storm); a genuinely unreachable
255
- // endpoint refuses instantly, so the retries cost microseconds. A NON-2xx response is the
256
- // consumer answering, not a transport failure — it is not retried here.
257
- // A delivery that still fails after the re-attempts is DROPPED, and says so: silent loss is
258
- // the one outcome a twin must never have, because it makes every consumer look flaky.
259
- const DELIVERY_ATTEMPTS = 3;
260
- const httpDelivery: StripeEventDelivery = async (url, event, endpointSecret) => {
261
- // the event as its API version renders it (stripe-version.ts)
262
- const payload = JSON.stringify(render(event, (event as { api_version?: string }).api_version));
263
- const secret = endpointSecret ?? secrets.get(url) ?? STRIPE_WEBHOOK_FALLBACK_SECRET;
264
- // the World's egress rule: an endpoint it refuses is not delivered to, and says so; the application's own hostnames
265
- // reach it inside the World
266
- const refusal = appDestination(url) ? null : worldEgressRefusal(url);
267
- if (refusal !== null) console.error(`[twin:stripe] webhook delivery DROPPED — ${event.type} ${event.id} -> ${url}: ${refusal}`);
268
- return refusal !== null ? undefined : postEvent(url, event, payload, secret);
269
- };
270
-
271
- /** The HTTP POST of a signed event to an endpoint the World lets it reach, re-attempted on a transport failure. */
272
- async function postEvent(url: string, event: StripeEvent, payload: string, secret: string): Promise<void> {
273
- let last = '';
274
- for (let attempt = 1; attempt <= DELIVERY_ATTEMPTS; attempt += 1) {
275
- try {
276
- const header = generateTestHeaderString({ payload, secret });
277
- await appFetch(url, { method: 'POST', headers: { 'content-type': 'application/json', 'stripe-signature': header, ...deliveryTraceHeaders() }, body: payload });
278
- return;
279
- } catch (error) {
280
- last = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
281
- if (attempt < DELIVERY_ATTEMPTS) await new Promise((resolve) => setTimeout(resolve, 25 * attempt));
282
- }
283
- }
284
- console.error(`[twin:stripe] webhook delivery DROPPED after ${DELIVERY_ATTEMPTS} attempts — ${event.type} ${event.id} -> ${url}: ${last}`);
285
- }
286
-
287
- // Default deliverer override (a TEST SEAM). The twin's write path emits events without
288
- // threading a per-call deliverer, so to drive event delivery fully OFFLINE/deterministically
289
- // — no real sockets — a test installs a fake sink here. When unset, real HTTP POST is used
290
- // (prod behavior). This keeps D5 honest: verify() exercises the twin's own emission, in-process.
291
- // a slot, not module truth (protocol 2): WHERE a delivery goes, set once by the host that mounts the twin
292
- const defaultDelivery: { current: StripeEventDelivery | null } = { current: null };
293
- /** Install a default deliverer used by the write path when no per-call `deliver` is passed. */
294
- export function setStripeEventDelivery(deliver: StripeEventDelivery | null): void {
295
- defaultDelivery.current = deliver;
296
- }
297
-
298
- // ── Real-time authorization (issuing_authorization.request) — the SYNCHRONOUS leg ────────
299
- // Real Stripe delivers issuing_authorization.request to the ONE enrolled endpoint while the
300
- // card network holds the authorization open, and the endpoint must answer within Stripe's
301
- // documented 2-second window ("This request should be made within the timeout window of the
302
- // real-time authorization flow" — stripe@22.3.0 Issuing/Authorizations.d.ts; the current
303
- // method is to "respond directly to the webhook request to approve an authorization"). The
304
- // endpoint's HTTP response body carries the decision: a JSON object with a boolean
305
- // `approved` (and, for an amount-controllable request, an optional integer `amount` to hold
306
- // a different amount). Outcomes map onto the vendor's request_history.reason values
307
- // (stripe@22.3.0 RequestHistory.Reason):
308
- // webhook_approved / webhook_declined — the endpoint answered in time;
309
- // webhook_timeout — no response arrived inside the window (incl. unreachable endpoint);
310
- // webhook_error — "the direct webhook response is invalid (for example, parsing errors
311
- // or missing parameters)" (verbatim from the SDK's reason_message doc),
312
- // i.e. non-2xx status, non-JSON body, or a missing `approved`.
313
- // On timeout/error real Stripe DECLINES the authorization (fail-closed; the card's own
314
- // spending_controls remain the backstop) — the account-level "approve on timeout" opt-in is
315
- // not API-configurable and is not modeled.
316
- export const STRIPE_REALTIME_AUTH_TIMEOUT_MS = 2_000;
317
-
318
- export type StripeAuthRequestOutcome =
319
- | { kind: 'approved'; amount?: number }
320
- | { kind: 'declined' }
321
- | { kind: 'timeout'; message: string }
322
- | { kind: 'error'; message: string };
323
-
324
- // The live synchronous deliverer: POST the signed event and WAIT for the response within
325
- // the 2s window. Signs with the enrolled endpoint's own whsec (passed by the caller, read
326
- // from the endpoint's state row) — never a placeholder.
327
- async function httpAuthRequestDelivery(url: string, event: StripeEvent, secret: string): Promise<StripeWebhookEndpointResponse> {
328
- // the event as its API version renders it (stripe-version.ts)
329
- const payload = JSON.stringify(render(event, (event as { api_version?: string }).api_version));
330
- const header = generateTestHeaderString({ payload, secret });
331
- // the World's egress rule: a refused endpoint is an unreachable one (the caller's timeout fallback); the
332
- // application's own hostnames reach it inside the World
333
- const refusal = appDestination(url) ? null : worldEgressRefusal(url);
334
- return refusal !== null ? Promise.reject(new Error(refusal)) : postAuthRequest(url, payload, header);
335
- }
336
-
337
- /** The HTTP POST of a real-time authorization request, cut off at the window, and the endpoint's answer. */
338
- async function postAuthRequest(url: string, payload: string, header: string): Promise<StripeWebhookEndpointResponse> {
339
- const res = await appFetch(url, {
340
- method: 'POST',
341
- headers: { 'content-type': 'application/json', 'stripe-signature': header, ...deliveryTraceHeaders() },
342
- body: payload,
343
- signal: AbortSignal.timeout(STRIPE_REALTIME_AUTH_TIMEOUT_MS),
344
- });
345
- return { status: res.status, body: await res.text() };
346
- }
347
-
348
- /**
349
- * Deliver an issuing_authorization.request event to the enrolled endpoint and interpret
350
- * its synchronous response as the authorization decision. Honors the installed test
351
- * deliverer (setStripeEventDelivery) so verifies run fully offline: a fake returning a
352
- * {status, body} response drives the decision; a fake that throws a TimeoutError (the
353
- * exact error AbortSignal.timeout produces) exercises the timeout fallback.
354
- */
355
- export async function requestAuthorizationDecision(url: string, event: StripeEvent, secret: string): Promise<StripeAuthRequestOutcome> {
356
- let res: void | StripeWebhookEndpointResponse;
357
- try {
358
- res = defaultDelivery.current ? await defaultDelivery.current(url, event) : await httpAuthRequestDelivery(url, event, secret);
359
- } catch (e) {
360
- // no answer inside the window: cut off (TimeoutError, AbortError), or never reached (refused, DNS, reset), alike
361
- return (e instanceof Error && (e.name === 'TimeoutError' || e.name === 'AbortError')) ? TIMED_OUT : UNREACHED;
362
- }
363
- return readDecision(res);
364
- }
365
-
366
- const TIMED_OUT: StripeAuthRequestOutcome = { kind: 'timeout', message: 'Webhook endpoint did not respond within the real-time authorization window.' };
367
- const UNREACHED: StripeAuthRequestOutcome = { kind: 'timeout', message: 'Webhook endpoint was unreachable within the real-time authorization window.' };
368
-
369
- /** The endpoint's synchronous answer read as the decision: `approved` (and an optional `amount`), or an error for an
370
- * answer Stripe cannot read. */
371
- function readDecision(res: void | StripeWebhookEndpointResponse): StripeAuthRequestOutcome {
372
- if (!res || typeof res !== 'object') {
373
- return { kind: 'error', message: 'Webhook endpoint returned no response body.' };
374
- }
375
- if (res.status < 200 || res.status >= 300) {
376
- return { kind: 'error', message: `Webhook endpoint responded with HTTP status ${res.status}.` };
377
- }
378
- let parsed: unknown;
379
- try {
380
- parsed = JSON.parse(res.body);
381
- } catch {
382
- return { kind: 'error', message: 'Webhook response body was not valid JSON.' };
383
- }
384
- const approved = (parsed as { approved?: unknown } | null)?.approved;
385
- if (typeof approved !== 'boolean') {
386
- return { kind: 'error', message: 'Webhook response is missing the required `approved` parameter.' };
387
- }
388
- if (!approved) return { kind: 'declined' };
389
- const amount = (parsed as { amount?: unknown }).amount;
390
- return { kind: 'approved', ...(typeof amount === 'number' && Number.isInteger(amount) && amount > 0 ? { amount } : {}) };
391
- }
392
-
393
- /**
394
- * Emit a Stripe event for a twin write to registered endpoints. `resource` is the
395
- * Stripe object the event is about. `occurredAt` is caller-supplied (deterministic).
396
- * Returns the delivered events (for assertions); no-op when nothing is registered
397
- * or the operation has no mapped event type.
398
- *
399
- * Fan-out filters by each endpoint's registered enabled_events (stripeEventMatches — '*',
400
- * 'family.*', exact), like the vendor: a consumer enrolled for one event type receives
401
- * only that type. A URL registered without a list (test seam) receives everything.
402
- */
403
- export async function emitStripeEvent(
404
- operation: string,
405
- resource: Record<string, unknown>,
406
- opts: { occurredAt: string; deliver?: StripeEventDelivery; endpoints?: StripeWebhookTarget[]; account?: string; /** the id the stored event takes (afterStripeWrite) */ id?: string } = { occurredAt: '1970-01-01T00:00:00.000Z' },
407
- ): Promise<StripeEvent[]> {
408
- const type = eventTypeFor(operation);
409
- // the World's own endpoints (its tree is the record), then any URL a host registered directly
410
- const targets: StripeWebhookTarget[] = [...(opts.endpoints ?? [])];
411
- for (const url of registry) if (!targets.some((t) => t.url === url)) targets.push({ url, ...(secrets.has(url) ? { secret: secrets.get(url)! } : {}), ...(subscriptions.has(url) ? { enabledEvents: subscriptions.get(url)! } : {}) });
412
- if (!type || targets.length === 0) return [];
413
- const deliver = opts.deliver ?? defaultDelivery.current ?? httpDelivery;
414
- const event: StripeEvent = {
415
- id: opts.id ?? eventId(operation, resource, opts.occurredAt, opts.account),
416
- object: 'event',
417
- type,
418
- created: Math.floor(Date.parse(opts.occurredAt) / 1000) || 0,
419
- livemode: false,
420
- data: { object: resource },
421
- // "Each event for a connected account contains a top-level `account` property that identifies the connected account"
422
- ...(opts.account ? { account: opts.account } : {}),
423
- };
424
- const out: StripeEvent[] = [];
425
- for (const target of targets) {
426
- if (target.enabledEvents && !stripeEventMatches(target.enabledEvents, type)) continue;
427
- // an endpoint's scope (docs.stripe.com/connect/webhooks): `connect: true` receives the Connected accounts scope,
428
- // `connect: false` Your account; a URL registered without a scope (a host's seam) receives both
429
- if (target.connect !== undefined && target.connect !== Boolean(opts.account)) continue;
430
- await deliver(target.url, event, target.secret);
431
- out.push(event);
432
- }
433
- return out;
434
- }