@volter/twin-stripe 2.0.38 → 3.0.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.
- package/README.md +82 -207
- package/dist/generated/pack-facts.json +94 -0
- package/dist/spec/SOURCE.md +14 -0
- package/dist/spec/fixtures3.json.gz +0 -0
- package/dist/spec/openapi.json.gz +0 -0
- package/dist/spec/patches.json +34 -0
- package/dist/spec/sources.json +378 -0
- package/dist/src/cli.js +12 -30
- package/dist/src/engine/after-payment.d.ts +9 -0
- package/dist/src/engine/after-payment.js +28 -0
- package/dist/src/engine/apps-secrets.d.ts +1 -0
- package/dist/src/engine/apps-secrets.js +3 -0
- package/dist/src/engine/balance.d.ts +5 -0
- package/dist/src/engine/balance.js +15 -0
- package/dist/src/engine/billing.d.ts +5 -0
- package/dist/src/engine/billing.js +6 -0
- package/dist/src/engine/charges.d.ts +6 -0
- package/dist/src/engine/charges.js +13 -0
- package/dist/src/engine/checkout.d.ts +24 -0
- package/dist/src/engine/checkout.js +30 -0
- package/dist/src/engine/common.d.ts +19 -0
- package/dist/src/engine/common.js +32 -0
- package/dist/src/{screens → engine}/connect-settings.d.ts +1 -7
- package/dist/src/engine/connect-settings.js +26 -0
- package/dist/src/engine/connect.d.ts +66 -0
- package/dist/src/engine/connect.js +189 -0
- package/dist/src/engine/coupons.d.ts +0 -0
- package/dist/src/engine/coupons.js +5 -0
- package/dist/src/engine/credit-notes.d.ts +1 -0
- package/dist/src/engine/credit-notes.js +5 -0
- package/dist/src/engine/customers.d.ts +12 -0
- package/dist/src/engine/customers.js +30 -0
- package/dist/src/engine/display.d.ts +14 -0
- package/dist/src/engine/display.js +16 -0
- package/dist/src/engine/disputes.d.ts +0 -0
- package/dist/src/engine/disputes.js +9 -0
- package/dist/src/engine/entitlements.d.ts +1 -0
- package/dist/src/engine/entitlements.js +4 -0
- package/dist/src/engine/ephemeral-keys.d.ts +0 -0
- package/dist/src/engine/ephemeral-keys.js +4 -0
- package/dist/src/engine/files.d.ts +18 -0
- package/dist/src/engine/files.js +33 -0
- package/dist/src/engine/front.d.ts +5 -0
- package/dist/src/engine/front.js +25 -0
- package/dist/src/engine/invoices.d.ts +24 -0
- package/dist/src/engine/invoices.js +57 -0
- package/dist/src/engine/issuing.d.ts +35 -0
- package/dist/src/engine/issuing.js +45 -0
- package/dist/src/engine/ledger.d.ts +9 -0
- package/dist/src/engine/ledger.js +9 -0
- package/dist/src/engine/oauth.d.ts +4 -0
- package/dist/src/engine/oauth.js +25 -0
- package/dist/src/engine/onboarding.d.ts +13 -0
- package/dist/src/engine/onboarding.js +42 -0
- package/dist/src/{stripe-params.d.ts → engine/params.d.ts} +20 -3
- package/dist/src/{stripe-params.js → engine/params.js} +7 -60
- package/dist/src/engine/payment-intents.d.ts +10 -0
- package/dist/src/engine/payment-intents.js +27 -0
- package/dist/src/engine/payment-links.d.ts +1 -0
- package/dist/src/engine/payment-links.js +5 -0
- package/dist/src/engine/payment-methods.d.ts +16 -0
- package/dist/src/engine/payment-methods.js +22 -0
- package/dist/src/engine/plans.d.ts +6 -0
- package/dist/src/engine/plans.js +26 -0
- package/dist/src/engine/platform.d.ts +39 -0
- package/dist/src/engine/platform.js +34 -0
- package/dist/src/engine/products.d.ts +0 -0
- package/dist/src/engine/products.js +4 -0
- package/dist/src/engine/radar.d.ts +2 -0
- package/dist/src/engine/radar.js +5 -0
- package/dist/src/engine/refunds.d.ts +1 -0
- package/dist/src/engine/refunds.js +28 -0
- package/dist/src/engine/renewals.d.ts +36 -0
- package/dist/src/engine/renewals.js +65 -0
- package/dist/src/engine/setup-intents.d.ts +1 -0
- package/dist/src/engine/setup-intents.js +4 -0
- package/dist/src/{stripe-twin.d.ts → engine/stripe.d.ts} +11 -39
- package/dist/src/{stripe-twin.js → engine/stripe.js} +25 -310
- package/dist/src/engine/subscription-schedules.d.ts +3 -0
- package/dist/src/engine/subscription-schedules.js +1 -0
- package/dist/src/engine/subscriptions.d.ts +12 -0
- package/dist/src/engine/subscriptions.js +31 -0
- package/dist/src/engine/tax.d.ts +7 -0
- package/dist/src/engine/tax.js +28 -0
- package/dist/src/engine/terminal.d.ts +4 -0
- package/dist/src/engine/terminal.js +20 -0
- package/dist/src/engine/test-clocks.d.ts +5 -0
- package/dist/src/engine/test-clocks.js +9 -0
- package/dist/src/engine/tokens.d.ts +7 -0
- package/dist/src/engine/tokens.js +20 -0
- package/dist/src/engine/transfers.d.ts +2 -0
- package/dist/src/engine/transfers.js +1 -0
- package/dist/src/engine/treasury.d.ts +23 -0
- package/dist/src/engine/treasury.js +45 -0
- package/dist/src/{stripe-version.js → engine/version.js} +4 -3
- package/dist/src/engine/webhook-endpoints.d.ts +1 -0
- package/dist/src/engine/webhook-endpoints.js +12 -0
- package/dist/src/engine/webhooks.d.ts +31 -0
- package/dist/src/engine/webhooks.js +68 -0
- package/dist/src/fetch.d.ts +2 -0
- package/dist/src/fetch.js +26 -0
- package/dist/src/generated/surface.gen.json +1 -1
- package/dist/src/generated/ui.gen.json +1 -1
- package/dist/src/index.d.ts +5 -14
- package/dist/src/index.js +8 -75
- package/dist/src/manifest.js +232 -758
- package/dist/src/screens/billing-portal.d.ts +3 -0
- package/dist/src/screens/{portal.js → billing-portal.js} +28 -54
- package/dist/src/screens/checkout.d.ts +2 -31
- package/dist/src/screens/checkout.js +32 -121
- package/dist/src/screens/connect-oauth.d.ts +3 -27
- package/dist/src/screens/connect-oauth.js +51 -132
- package/dist/src/screens/connect-onboarding.d.ts +3 -0
- package/dist/src/screens/{onboarding.js → connect-onboarding.js} +36 -102
- package/dist/src/screens/dashboard.d.ts +3 -0
- package/dist/src/screens/dashboard.js +117 -0
- package/dist/src/screens/shared.d.ts +27 -0
- package/dist/src/screens/shared.js +87 -0
- package/dist/src/screens/stripe-js.d.ts +3 -0
- package/dist/src/screens/stripe-js.js +135 -0
- package/dist/src/semantics/account_links.d.ts +2 -0
- package/dist/src/semantics/account_links.js +22 -0
- package/dist/src/semantics/accounts.d.ts +4 -0
- package/dist/src/semantics/accounts.js +38 -0
- package/dist/src/semantics/apps.d.ts +4 -0
- package/dist/src/semantics/{apps-secrets.js → apps.js} +14 -26
- package/dist/src/semantics/around.d.ts +2 -0
- package/dist/src/semantics/around.js +16 -0
- package/dist/src/semantics/balance.d.ts +2 -11
- package/dist/src/semantics/balance.js +3 -194
- package/dist/src/semantics/balance_transactions.d.ts +2 -0
- package/dist/src/semantics/balance_transactions.js +15 -0
- package/dist/src/semantics/billing_portal.d.ts +5 -0
- package/dist/src/semantics/billing_portal.js +42 -0
- package/dist/src/semantics/charges.d.ts +3 -28
- package/dist/src/semantics/charges.js +5 -202
- package/dist/src/semantics/checkout.d.ts +3 -20
- package/dist/src/semantics/checkout.js +8 -261
- package/dist/src/semantics/clock.d.ts +2 -0
- package/dist/src/semantics/clock.js +21 -0
- package/dist/src/semantics/coupons.d.ts +2 -6
- package/dist/src/semantics/coupons.js +2 -60
- package/dist/src/semantics/customers.d.ts +6 -6
- package/dist/src/semantics/customers.js +27 -387
- package/dist/src/semantics/disputes.d.ts +2 -2
- package/dist/src/semantics/disputes.js +3 -49
- package/dist/src/semantics/doors.d.ts +23 -0
- package/dist/src/semantics/doors.js +73 -0
- package/dist/src/semantics/entitlements.d.ts +2 -2
- package/dist/src/semantics/entitlements.js +5 -50
- package/dist/src/semantics/events.d.ts +12 -0
- package/dist/src/semantics/events.js +36 -0
- package/dist/src/semantics/index.d.ts +2 -0
- package/dist/src/semantics/index.js +107 -0
- package/dist/src/semantics/invoice_payments.d.ts +2 -0
- package/dist/src/semantics/invoice_payments.js +12 -0
- package/dist/src/semantics/invoiceitems.d.ts +3 -0
- package/dist/src/semantics/invoiceitems.js +59 -0
- package/dist/src/semantics/invoices.d.ts +16 -18
- package/dist/src/semantics/invoices.js +118 -444
- package/dist/src/semantics/payment_intents.d.ts +4 -0
- package/dist/src/semantics/payment_intents.js +79 -0
- package/dist/src/semantics/payment_methods.d.ts +5 -0
- package/dist/src/semantics/payment_methods.js +39 -0
- package/dist/src/semantics/payouts.d.ts +2 -0
- package/dist/src/semantics/payouts.js +21 -0
- package/dist/src/semantics/prices.d.ts +4 -0
- package/dist/src/semantics/prices.js +37 -0
- package/dist/src/semantics/products.d.ts +6 -2
- package/dist/src/semantics/products.js +15 -130
- package/dist/src/semantics/promotion_codes.d.ts +3 -0
- package/dist/src/semantics/promotion_codes.js +35 -0
- package/dist/src/semantics/radar.d.ts +2 -2
- package/dist/src/semantics/radar.js +5 -74
- package/dist/src/semantics/refunds.d.ts +3 -9
- package/dist/src/semantics/refunds.js +6 -192
- package/dist/src/semantics/setup_intents.d.ts +3 -0
- package/dist/src/semantics/{setup-intents.js → setup_intents.js} +10 -53
- package/dist/src/semantics/shared.d.ts +822 -47
- package/dist/src/semantics/shared.js +3044 -78
- package/dist/src/semantics/states.d.ts +531 -0
- package/dist/src/semantics/states.js +852 -0
- package/dist/src/semantics/subscription_items.d.ts +5 -0
- package/dist/src/semantics/subscription_items.js +55 -0
- package/dist/src/semantics/subscription_schedules.d.ts +4 -0
- package/dist/src/semantics/subscription_schedules.js +60 -0
- package/dist/src/semantics/subscriptions.d.ts +7 -11
- package/dist/src/semantics/subscriptions.js +44 -415
- package/dist/src/semantics/test_helpers.d.ts +6 -0
- package/dist/src/semantics/{test-clocks.js → test_helpers.js} +14 -40
- package/dist/src/semantics/transfers.d.ts +2 -2
- package/dist/src/semantics/transfers.js +6 -101
- package/dist/src/server.d.ts +7 -0
- package/dist/src/server.js +7 -0
- package/generated/pack-facts.json +94 -0
- package/package.json +10 -20
- package/spec/SOURCE.md +14 -0
- package/spec/fixtures3.json.gz +0 -0
- package/spec/openapi.json.gz +0 -0
- package/spec/patches.json +34 -0
- package/spec/sources.json +378 -0
- package/src/cli.ts +12 -26
- package/src/engine/after-payment.ts +31 -0
- package/src/engine/apps-secrets.ts +4 -0
- package/src/engine/balance.ts +33 -0
- package/src/engine/billing.ts +16 -0
- package/src/engine/charges.ts +19 -0
- package/src/engine/checkout.ts +40 -0
- package/src/engine/common.ts +47 -0
- package/src/engine/connect-settings.ts +23 -0
- package/src/engine/connect.ts +184 -0
- package/src/engine/coupons.ts +5 -0
- package/src/engine/credit-notes.ts +6 -0
- package/src/engine/customers.ts +35 -0
- package/src/engine/display.ts +19 -0
- package/src/engine/disputes.ts +9 -0
- package/src/engine/entitlements.ts +5 -0
- package/src/engine/ephemeral-keys.ts +3 -0
- package/src/engine/files.ts +41 -0
- package/src/engine/front.ts +31 -0
- package/src/engine/invoices.ts +71 -0
- package/src/engine/issuing.ts +61 -0
- package/src/engine/ledger.ts +50 -0
- package/src/engine/oauth.ts +19 -0
- package/src/engine/onboarding.ts +54 -0
- package/src/{stripe-params.ts → engine/params.ts} +8 -56
- package/src/engine/payment-intents.ts +33 -0
- package/src/engine/payment-links.ts +6 -0
- package/src/engine/payment-methods.ts +25 -0
- package/src/engine/plans.ts +42 -0
- package/src/engine/platform.ts +47 -0
- package/src/engine/products.ts +3 -0
- package/src/engine/radar.ts +6 -0
- package/src/engine/refunds.ts +29 -0
- package/src/engine/renewals.ts +73 -0
- package/src/engine/setup-intents.ts +5 -0
- package/src/{stripe-twin.ts → engine/stripe.ts} +28 -323
- package/src/engine/subscription-schedules.ts +10 -0
- package/src/engine/subscriptions.ts +40 -0
- package/src/engine/tax.ts +39 -0
- package/src/engine/terminal.ts +23 -0
- package/src/engine/test-clocks.ts +11 -0
- package/src/engine/tokens.ts +22 -0
- package/src/engine/transfers.ts +23 -0
- package/src/engine/treasury.ts +47 -0
- package/src/{stripe-version.ts → engine/version.ts} +4 -3
- package/src/engine/webhook-endpoints.ts +14 -0
- package/src/engine/webhooks.ts +72 -0
- package/src/fetch.ts +27 -0
- package/src/generated/surface.gen.json +1 -1
- package/src/generated/ui.gen.json +1 -1
- package/src/index.ts +8 -116
- package/src/manifest.ts +234 -786
- package/src/screens/{portal.tsx → billing-portal.tsx} +32 -37
- package/src/screens/checkout.tsx +33 -95
- package/src/screens/connect-oauth.tsx +64 -88
- package/src/screens/{onboarding.tsx → connect-onboarding.tsx} +38 -82
- package/src/screens/dashboard.tsx +198 -0
- package/src/screens/shared.tsx +84 -0
- package/src/screens/stripe-js.ts +170 -0
- package/src/semantics/account_links.ts +21 -0
- package/src/semantics/accounts.ts +43 -0
- package/src/semantics/{apps-secrets.ts → apps.ts} +17 -32
- package/src/semantics/around.ts +20 -0
- package/src/semantics/balance.ts +6 -207
- package/src/semantics/balance_transactions.ts +20 -0
- package/src/semantics/billing_portal.ts +51 -0
- package/src/semantics/charges.ts +12 -211
- package/src/semantics/checkout.ts +13 -268
- package/src/semantics/clock.ts +26 -0
- package/src/semantics/coupons.ts +5 -67
- package/src/semantics/customers.ts +29 -392
- package/src/semantics/disputes.ts +9 -59
- package/src/semantics/doors.ts +80 -0
- package/src/semantics/entitlements.ts +9 -56
- package/src/semantics/events.ts +44 -0
- package/src/semantics/index.ts +111 -0
- package/src/semantics/invoice_payments.ts +18 -0
- package/src/semantics/invoiceitems.ts +59 -0
- package/src/semantics/invoices.ts +109 -443
- package/src/semantics/payment_intents.ts +83 -0
- package/src/semantics/payment_methods.ts +43 -0
- package/src/semantics/payouts.ts +23 -0
- package/src/semantics/prices.ts +42 -0
- package/src/semantics/products.ts +18 -142
- package/src/semantics/promotion_codes.ts +39 -0
- package/src/semantics/radar.ts +8 -77
- package/src/semantics/refunds.ts +9 -212
- package/src/semantics/setup_intents.ts +45 -0
- package/src/semantics/shared.ts +3092 -106
- package/src/semantics/states.ts +883 -0
- package/src/semantics/subscription_items.ts +54 -0
- package/src/semantics/subscription_schedules.ts +66 -0
- package/src/semantics/subscriptions.ts +49 -432
- package/src/semantics/{test-clocks.ts → test_helpers.ts} +23 -47
- package/src/semantics/transfers.ts +11 -131
- package/src/server.ts +8 -0
- package/client/dashboard-api.ts +0 -286
- package/client/stripe-mirror.css +0 -277
- package/client/stripe-mirror.tsx +0 -1559
- package/dist/client/dashboard-api.d.ts +0 -107
- package/dist/client/dashboard-api.js +0 -238
- package/dist/client/dashboard-api.ts +0 -286
- package/dist/client/stripe-mirror.bundle.js +0 -236
- package/dist/client/stripe-mirror.css +0 -277
- package/dist/client/stripe-mirror.d.ts +0 -134
- package/dist/client/stripe-mirror.js +0 -859
- package/dist/client/stripe-mirror.tsx +0 -1559
- package/dist/src/screens/connect-settings.js +0 -103
- package/dist/src/screens/consent-skin.d.ts +0 -4
- package/dist/src/screens/consent-skin.js +0 -18
- package/dist/src/screens/financial-connections.d.ts +0 -5
- package/dist/src/screens/financial-connections.js +0 -90
- package/dist/src/screens/identity.d.ts +0 -5
- package/dist/src/screens/identity.js +0 -86
- package/dist/src/screens/onboarding.d.ts +0 -13
- package/dist/src/screens/portal.d.ts +0 -5
- package/dist/src/screens/public-details.d.ts +0 -5
- package/dist/src/screens/public-details.js +0 -90
- package/dist/src/semantics/after-payment.d.ts +0 -22
- package/dist/src/semantics/after-payment.js +0 -99
- package/dist/src/semantics/apps-secrets.d.ts +0 -2
- package/dist/src/semantics/billing.d.ts +0 -2
- package/dist/src/semantics/billing.js +0 -220
- package/dist/src/semantics/connect.d.ts +0 -5
- package/dist/src/semantics/connect.js +0 -493
- package/dist/src/semantics/credit-notes.d.ts +0 -2
- package/dist/src/semantics/credit-notes.js +0 -172
- package/dist/src/semantics/ephemeral-keys.d.ts +0 -2
- package/dist/src/semantics/ephemeral-keys.js +0 -34
- package/dist/src/semantics/files.d.ts +0 -2
- package/dist/src/semantics/files.js +0 -125
- package/dist/src/semantics/issuing.d.ts +0 -13
- package/dist/src/semantics/issuing.js +0 -575
- package/dist/src/semantics/ledger.d.ts +0 -59
- package/dist/src/semantics/ledger.js +0 -200
- package/dist/src/semantics/payment-intents.d.ts +0 -18
- package/dist/src/semantics/payment-intents.js +0 -452
- package/dist/src/semantics/payment-links.d.ts +0 -2
- package/dist/src/semantics/payment-links.js +0 -133
- package/dist/src/semantics/payment-methods.d.ts +0 -20
- package/dist/src/semantics/payment-methods.js +0 -140
- package/dist/src/semantics/plans.d.ts +0 -5
- package/dist/src/semantics/plans.js +0 -121
- package/dist/src/semantics/platform.d.ts +0 -9
- package/dist/src/semantics/platform.js +0 -206
- package/dist/src/semantics/renewals.d.ts +0 -47
- package/dist/src/semantics/renewals.js +0 -281
- package/dist/src/semantics/setup-intents.d.ts +0 -2
- package/dist/src/semantics/subscription-schedules.d.ts +0 -2
- package/dist/src/semantics/subscription-schedules.js +0 -119
- package/dist/src/semantics/tax.d.ts +0 -2
- package/dist/src/semantics/tax.js +0 -197
- package/dist/src/semantics/terminal.d.ts +0 -5
- package/dist/src/semantics/terminal.js +0 -182
- package/dist/src/semantics/test-clocks.d.ts +0 -6
- package/dist/src/semantics/tokens.d.ts +0 -4
- package/dist/src/semantics/tokens.js +0 -44
- package/dist/src/semantics/treasury.d.ts +0 -2
- package/dist/src/semantics/treasury.js +0 -377
- package/dist/src/semantics/webhook-endpoints.d.ts +0 -3
- package/dist/src/semantics/webhook-endpoints.js +0 -85
- package/dist/src/stripe-budget.d.ts +0 -55
- package/dist/src/stripe-budget.js +0 -155
- package/dist/src/stripe-capabilities.d.ts +0 -3
- package/dist/src/stripe-capabilities.js +0 -5836
- package/dist/src/stripe-conformance.d.ts +0 -43
- package/dist/src/stripe-conformance.js +0 -105
- package/dist/src/stripe-connector.d.ts +0 -161
- package/dist/src/stripe-connector.js +0 -414
- package/dist/src/stripe-emit.d.ts +0 -2
- package/dist/src/stripe-emit.js +0 -145
- package/dist/src/stripe-events.d.ts +0 -93
- package/dist/src/stripe-events.js +0 -392
- package/dist/src/stripe-js.d.ts +0 -4
- package/dist/src/stripe-js.js +0 -70
- package/dist/src/stripe-mirror-ui.d.ts +0 -15
- package/dist/src/stripe-mirror-ui.js +0 -87
- package/dist/src/stripe-perform-harness.d.ts +0 -9
- package/dist/src/stripe-perform-harness.js +0 -26
- package/dist/src/stripe-server.d.ts +0 -33
- package/dist/src/stripe-server.js +0 -429
- package/dist/src/stripe-shared.d.ts +0 -109
- package/dist/src/stripe-shared.js +0 -276
- package/dist/src/stripe-ui-conformance.d.ts +0 -5
- package/dist/src/stripe-ui-conformance.js +0 -79
- package/dist/src/stripe-ui-structure.d.ts +0 -3
- package/dist/src/stripe-ui-structure.js +0 -168
- package/dist/test-fixtures/stripe-known-deviations.json +0 -110
- package/dist/test-fixtures/stripe-openapi-operations.SOURCE.md +0 -14
- package/dist/test-fixtures/stripe-openapi-operations.json +0 -4717
- package/dist/test-fixtures/stripe-schemas.SOURCE.md +0 -35
- package/dist/test-fixtures/stripe-schemas.json +0 -3813
- package/src/screens/connect-settings.tsx +0 -121
- package/src/screens/consent-skin.ts +0 -20
- package/src/screens/financial-connections.tsx +0 -101
- package/src/screens/identity.tsx +0 -96
- package/src/screens/public-details.tsx +0 -105
- package/src/semantics/after-payment.ts +0 -118
- package/src/semantics/billing.ts +0 -216
- package/src/semantics/connect.ts +0 -487
- package/src/semantics/credit-notes.ts +0 -168
- package/src/semantics/ephemeral-keys.ts +0 -34
- package/src/semantics/files.ts +0 -143
- package/src/semantics/issuing.ts +0 -590
- package/src/semantics/ledger.ts +0 -253
- package/src/semantics/payment-intents.ts +0 -463
- package/src/semantics/payment-links.ts +0 -148
- package/src/semantics/payment-methods.ts +0 -145
- package/src/semantics/plans.ts +0 -131
- package/src/semantics/platform.ts +0 -220
- package/src/semantics/renewals.ts +0 -279
- package/src/semantics/setup-intents.ts +0 -87
- package/src/semantics/subscription-schedules.ts +0 -129
- package/src/semantics/tax.ts +0 -220
- package/src/semantics/terminal.ts +0 -195
- package/src/semantics/tokens.ts +0 -52
- package/src/semantics/treasury.ts +0 -383
- package/src/semantics/webhook-endpoints.ts +0 -87
- package/src/stripe-budget.ts +0 -181
- package/src/stripe-capabilities.ts +0 -5651
- package/src/stripe-conformance.ts +0 -124
- package/src/stripe-connector.ts +0 -462
- package/src/stripe-emit.ts +0 -157
- package/src/stripe-events.ts +0 -434
- package/src/stripe-js.ts +0 -70
- package/src/stripe-mirror-ui.ts +0 -90
- package/src/stripe-perform-harness.ts +0 -29
- package/src/stripe-server.ts +0 -393
- package/src/stripe-shared.ts +0 -297
- package/src/stripe-ui-conformance.ts +0 -88
- package/src/stripe-ui-structure.ts +0 -182
- package/test-fixtures/stripe-known-deviations.json +0 -110
- package/test-fixtures/stripe-openapi-operations.json +0 -4717
- package/test-fixtures/stripe-schemas.json +0 -3813
- /package/dist/src/{screens → engine}/industries.d.ts +0 -0
- /package/dist/src/{screens → engine}/industries.js +0 -0
- /package/dist/src/{semantics → engine}/test-cards.d.ts +0 -0
- /package/dist/src/{semantics → engine}/test-cards.js +0 -0
- /package/dist/src/{stripe-version.d.ts → engine/version.d.ts} +0 -0
- /package/src/{screens → engine}/industries.ts +0 -0
- /package/src/{semantics → engine}/test-cards.ts +0 -0
package/src/stripe-emit.ts
DELETED
|
@@ -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
|
-
};
|
package/src/stripe-events.ts
DELETED
|
@@ -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
|
-
}
|