@volter/twin-stripe 2.0.37 → 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
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// connection. The page is @volter/world-ui's consent piece under Stripe's skin; nothing of Stripe's page is copied.
|
|
10
10
|
//
|
|
11
11
|
// What the documentation says and the twin keeps:
|
|
12
|
-
// - authorize: `client_id` is the platform's (its Connect OAuth settings, ./
|
|
12
|
+
// - authorize: `client_id` is the platform's (its Connect OAuth settings, ./dashboard.tsx, where OAuth is enabled
|
|
13
13
|
// and the redirect URIs are registered); `response_type` "The only option at the moment is code"; `redirect_uri`, "If
|
|
14
14
|
// provided, this must exactly match one of the ... redirect_uri values in your application settings" and "Defaults to
|
|
15
15
|
// the redirect_uri in your application settings" ("the first URI configured"); `scope` read_write or read_only,
|
|
@@ -26,13 +26,13 @@
|
|
|
26
26
|
// unsupported_grant_type and invalid_scope as `{ error, error_description }`.
|
|
27
27
|
// - deauthorize: client_id and stripe_user_id, answered `{ stripe_user_id }`; errors invalid_request and invalid_client
|
|
28
28
|
// ("stripe_user_id doesn't exist or isn't connected to your application"); afterwards "the account can't be accessed by
|
|
29
|
-
// your platform ... through the API" (
|
|
29
|
+
// your platform ... through the API" (../engine/front.ts refuses the Stripe-Account header for it, account_invalid).
|
|
30
30
|
// - events: account.application.authorized when the person connects and account.application.deauthorized when the
|
|
31
31
|
// connection is revoked, each the connected account's (top-level `account`, Connect endpoints), carrying the
|
|
32
32
|
// `application` object ({ id: the client_id, object: 'application', name }).
|
|
33
33
|
//
|
|
34
34
|
// Where the documentation stops and the twin decides:
|
|
35
|
-
// - the twin is a test-mode Stripe: its client_id is a test one (./
|
|
35
|
+
// - the twin is a test-mode Stripe: its client_id is a test one (./dashboard.tsx PLATFORM_CLIENT_ID), redirect
|
|
36
36
|
// URIs may be http and localhost ("Your test client_id allows you to ... Set your redirect_uri to a non-HTTPS URL /
|
|
37
37
|
// to localhost"), a code, token and connection are livemode false, and a live secret key (sk_live_) is refused as the
|
|
38
38
|
// documented mode mismatch (invalid_grant at token, invalid_client at deauthorize).
|
|
@@ -50,50 +50,34 @@
|
|
|
50
50
|
// key (sk_…/rk_…) 401 invalid_request too. The token and deauthorize wording other than the one quoted above is the
|
|
51
51
|
// twin's. An expired code is invalid_grant "Authorization code expired: {code}"; a reused one invalid_grant "This
|
|
52
52
|
// authorization code has already been used. All tokens issued with this code have been revoked." and revokes.
|
|
53
|
-
// - tokens are
|
|
54
|
-
//
|
|
55
|
-
// `
|
|
56
|
-
// Stripe-Account header) and act as the account (
|
|
53
|
+
// - tokens are drawn from secrets the World holds (ctx.secret), so none is computable from the account's id:
|
|
54
|
+
// access_token `sk_test_oauth_{account}_{secret of its generation}`, refresh_token `rt_{secret of the code}`,
|
|
55
|
+
// stripe_publishable_key `pk_test_oauth_{account}` (publishable, so public); a refresh answers the same refresh_token. The access_token and publishable key are deprecated by Stripe (use the
|
|
56
|
+
// Stripe-Account header) and act as the account (../engine/front.ts actingAsOAuthKey); a read_only one is not held to
|
|
57
57
|
// reads (manifest todo `stripe.connect.oauth_scope_enforcement`).
|
|
58
58
|
// - concurrency: a code's number is taken with its write in one move; a code redeemed twice at once lets one exchange
|
|
59
59
|
// win, and whichever of the two finishes last revokes the connection, so a double exchange always ends revoked.
|
|
60
|
-
import {
|
|
60
|
+
import type { HandlerContext } from '@volter/world-core';
|
|
61
61
|
import { Consent, CONSENT_CSS, flowPage } from '@volter/world-ui';
|
|
62
|
-
import
|
|
63
|
-
import {
|
|
64
|
-
import {
|
|
65
|
-
import {
|
|
66
|
-
import { accountSettings,
|
|
67
|
-
import { formOf, seeOther, STRIPE_CONSENT_SKIN } from './
|
|
68
|
-
import { connectOAuthSettings,
|
|
62
|
+
import type { Row } from '../engine/common.ts';
|
|
63
|
+
import { PLATFORM_CLIENT_ID, SETTINGS_URL } from '../engine/connect-settings.ts';
|
|
64
|
+
import { OAUTH_CONNECTIONS, publicBusinessName } from '../engine/display.ts';
|
|
65
|
+
import { redactKey } from '../engine/oauth.ts';
|
|
66
|
+
import { accountSettings, nowUnix, PLATFORM_ACCOUNT_ID } from '../engine/stripe.ts';
|
|
67
|
+
import { formOf, notFound, seeOther, STRIPE_CONSENT_SKIN } from './shared.tsx';
|
|
68
|
+
import { connectOAuthSettings, created, decide } from '../semantics/shared.ts';
|
|
69
69
|
|
|
70
|
-
const OPERATION = (surface.operations as Array<{ id: string; method: string; path: string; class: string }>).find((o) => o.id === 'PostAccountsAccount')!;
|
|
71
70
|
/** A code "expires in 5 minutes". */
|
|
72
|
-
|
|
71
|
+
const CODE_TTL_SECONDS = 300;
|
|
73
72
|
const CODES = '_oauth_code';
|
|
74
73
|
/** One connection per connected account (the twin's one platform): its scope, tokens, and whether it was revoked. */
|
|
75
|
-
|
|
74
|
+
const CONNECTIONS = OAUTH_CONNECTIONS;
|
|
76
75
|
const SCOPES = ['read_write', 'read_only'] as const;
|
|
77
76
|
type Scope = (typeof SCOPES)[number];
|
|
78
77
|
|
|
79
|
-
type Scoped = { root?: string; clock?: () => string };
|
|
80
78
|
const json = (body: Row, status = 200): Response => Response.json(body, { status, headers: { 'request-id': 'req_twin', 'cache-control': 'no-store' } });
|
|
81
79
|
const oauthError = (error: string, description: string, status = 400, extra: Row = {}): Response => json({ error, error_description: description, ...extra }, status);
|
|
82
80
|
|
|
83
|
-
/** Whether a connected account's OAuth connection to the platform was revoked (deauthorized, or its code reused). */
|
|
84
|
-
export function connectionRevoked(ctx: SemanticsContext, account: string): boolean {
|
|
85
|
-
return ctx.rowsRaw(CONNECTIONS).some((c) => c.id === account && c.revoked === true);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/** The connected account an OAuth key acts as — the connection's current access_token (sk_test_oauth_…) or its
|
|
89
|
-
* stripe_publishable_key (pk_test_oauth_…) — with the scope it was issued; undefined for any other key, and null for an
|
|
90
|
-
* OAuth-shaped key no live connection holds (revoked, replaced by a refresh, or never issued). */
|
|
91
|
-
export function oauthKeyAccount(ctx: SemanticsContext, key: string): { account: string; scope: string } | null | undefined {
|
|
92
|
-
if (!/^(sk|pk)_test_oauth_/.test(key)) return undefined;
|
|
93
|
-
const held = ctx.rowsRaw(CONNECTIONS).find((c) => c.revoked !== true && (c.access_token === key || c.publishable_key === key));
|
|
94
|
-
return held ? { account: String(held.id), scope: String(key.startsWith('pk_') ? held.scope : held.access_scope ?? held.scope) } : null;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
81
|
// ── authorize ──
|
|
98
82
|
|
|
99
83
|
/** A query parameter, or undefined when absent (an empty value is absent too, as a browser form sends one). */
|
|
@@ -102,7 +86,7 @@ const param = (q: URLSearchParams, k: string): string | undefined => { const v =
|
|
|
102
86
|
type Authorize = { redirect: string; scope: Scope; state: string | undefined };
|
|
103
87
|
|
|
104
88
|
/** The authorize request checked as Stripe checks it, or the JSON error it answers. */
|
|
105
|
-
function checked(ctx:
|
|
89
|
+
function checked(ctx: HandlerContext, q: URLSearchParams): Authorize | Response {
|
|
106
90
|
const state = param(q, 'state');
|
|
107
91
|
const fail = (error: string, description: string) => oauthError(error, description, 400, state !== undefined ? { state } : {});
|
|
108
92
|
const client = param(q, 'client_id');
|
|
@@ -137,7 +121,7 @@ function withParams(uri: string, params: Array<[string, string | undefined]>): s
|
|
|
137
121
|
/** The prefill the request carries, invalid values silently dropped (docs.stripe.com/connect/oauth-reference). Only
|
|
138
122
|
* what the new account's object shows the platform, or the page shows the person, is read: the person's birth date and
|
|
139
123
|
* the business address are the account application's, which the twin does not model (the test-mode skip). */
|
|
140
|
-
|
|
124
|
+
function prefill(q: URLSearchParams): Record<string, string> {
|
|
141
125
|
const v = (k: string) => param(q, `stripe_user[${k}]`)?.trim() || undefined;
|
|
142
126
|
const out: Record<string, string> = {};
|
|
143
127
|
const keep = (k: string, ok: (s: string) => boolean = () => true) => { const s = v(k); if (s !== undefined && ok(s)) out[k] = s; };
|
|
@@ -160,7 +144,7 @@ const COUNTRY_CURRENCY: Record<string, string> = { US: 'usd', CA: 'cad', GB: 'gb
|
|
|
160
144
|
const BUSINESS_TYPE: Record<string, string> = { sole_prop: 'individual', corporation: 'company', llc: 'company', partnership: 'company', non_profit: 'non_profit' };
|
|
161
145
|
|
|
162
146
|
/** The Standard account the skipped form creates, from the prefill. */
|
|
163
|
-
async function createAccount(ctx:
|
|
147
|
+
async function createAccount(ctx: HandlerContext, p: Record<string, string>): Promise<string> {
|
|
164
148
|
const country = p.country ?? 'US';
|
|
165
149
|
const id = ctx.mint('account');
|
|
166
150
|
const none = { alternatives: [], current_deadline: null, currently_due: [], disabled_reason: null, errors: [], eventually_due: [], past_due: [], pending_verification: [] };
|
|
@@ -180,20 +164,17 @@ async function createAccount(ctx: SemanticsContext, p: Record<string, string>):
|
|
|
180
164
|
}
|
|
181
165
|
|
|
182
166
|
/** The platform's application as the account.application.* events carry it. */
|
|
183
|
-
function application(ctx:
|
|
167
|
+
function application(ctx: HandlerContext): Row {
|
|
184
168
|
return { id: PLATFORM_CLIENT_ID, object: 'application', name: publicBusinessName(ctx.get('account', PLATFORM_ACCOUNT_ID)) };
|
|
185
169
|
}
|
|
186
170
|
|
|
187
|
-
/** Send one of the account.application.* events as the connected account's
|
|
188
|
-
|
|
189
|
-
|
|
171
|
+
/** Send one of the account.application.* events as the connected account's: the notice it is sent for (the kernel
|
|
172
|
+
* sends it, ../semantics/events.ts renders it). */
|
|
173
|
+
async function applicationEvent(ctx: HandlerContext, type: 'account.application.authorized' | 'account.application.deauthorized', account: string): Promise<void> {
|
|
174
|
+
await ctx.write('_application_notice', `${account}:${type}:${ctx.occurredAt}`, { account, application: application(ctx) }, type);
|
|
190
175
|
}
|
|
191
176
|
|
|
192
|
-
|
|
193
|
-
* other and neither is counted separately. */
|
|
194
|
-
const codeNumber = (code: string): string => code.replace(/^ac_twin_/, '');
|
|
195
|
-
|
|
196
|
-
function page(ctx: SemanticsContext, action: string, a: Authorize, p: Record<string, string>): Response {
|
|
177
|
+
function page(ctx: HandlerContext, action: string, a: Authorize, p: Record<string, string>): Response {
|
|
197
178
|
const name = publicBusinessName(ctx.get('account', PLATFORM_ACCOUNT_ID));
|
|
198
179
|
const who = [p.email, [p.first_name, p.last_name].filter(Boolean).join(' '), p.business_name].filter((s) => s).join(' · ');
|
|
199
180
|
return flowPage({
|
|
@@ -221,22 +202,20 @@ function page(ctx: SemanticsContext, action: string, a: Authorize, p: Record<str
|
|
|
221
202
|
}
|
|
222
203
|
|
|
223
204
|
/** The person connects: a new Standard account, its connection's code, the authorized event, and the redirect. */
|
|
224
|
-
async function connect(ctx:
|
|
205
|
+
async function connect(ctx: HandlerContext, a: Authorize, p: Record<string, string>): Promise<Response> {
|
|
225
206
|
const account = await createAccount(ctx, p);
|
|
226
|
-
// the code's
|
|
227
|
-
const
|
|
228
|
-
const code = await ctx.
|
|
229
|
-
|
|
230
|
-
return { value: id, write: { resource: CODES, id, fields, operation: 'oauth_code.issue' } };
|
|
231
|
-
});
|
|
207
|
+
// the code's row id is minted and its value drawn from the World's secret; the row keeps the value's SHA-256 alone
|
|
208
|
+
const id = ctx.mint(CODES);
|
|
209
|
+
const code = `ac_${(await ctx.secret(`stripe-oauth-code:${id}`)).replace(/[^A-Za-z0-9]/g, '').slice(0, 24)}`;
|
|
210
|
+
await ctx.record(CODES, { client_id: PLATFORM_CLIENT_ID, account, scope: a.scope, redirect_uri: a.redirect, expires_at: nowUnix(ctx.occurredAt) + CODE_TTL_SECONDS, used: false, livemode: false, sha256: ctx.crypto.sha256(code) }, id);
|
|
232
211
|
await applicationEvent(ctx, 'account.application.authorized', account);
|
|
233
212
|
return seeOther(withParams(a.redirect, [['scope', a.scope], ['code', code], ['state', a.state]]));
|
|
234
213
|
}
|
|
235
214
|
|
|
236
|
-
async function authorize(
|
|
215
|
+
async function authorize(ctx: HandlerContext): Promise<Response> {
|
|
216
|
+
const request = ctx.call.request;
|
|
237
217
|
const url = new URL(request.url);
|
|
238
218
|
const form = request.method === 'POST' ? await formOf(request.clone()) : {};
|
|
239
|
-
const ctx = await semanticsContext(manifest, request, OPERATION, scope);
|
|
240
219
|
const a = checked(ctx, url.searchParams);
|
|
241
220
|
if (a instanceof Response) return a;
|
|
242
221
|
const p = prefill(url.searchParams);
|
|
@@ -267,13 +246,6 @@ function secretKey(request: Request, params: Record<string, string>): string | u
|
|
|
267
246
|
return params.client_secret || undefined;
|
|
268
247
|
}
|
|
269
248
|
|
|
270
|
-
/** A key shown the way Stripe's errors show one: its prefix and its last four. */
|
|
271
|
-
export const redactKey = (key: string): string => {
|
|
272
|
-
const prefix = /^(sk|rk|pk)_(test|live)_/.exec(key)?.[0] ?? '';
|
|
273
|
-
const rest = key.slice(prefix.length);
|
|
274
|
-
return rest.length > 4 ? `${prefix}${'*'.repeat(rest.length - 4)}${rest.slice(-4)}` : `${prefix}${'*'.repeat(rest.length)}`;
|
|
275
|
-
};
|
|
276
|
-
|
|
277
249
|
/** The refusal of a request without a usable secret key, or undefined when it has one (answered with whether it is live). */
|
|
278
250
|
function keyRefused(key: string | undefined): Response | undefined {
|
|
279
251
|
if (!key) return oauthError('invalid_request', 'No API key provided. Provide your secret key in the Authorization header, using Bearer auth (e.g. \'Authorization: Bearer YOUR_SECRET_KEY\').', 401);
|
|
@@ -286,26 +258,29 @@ function keyRefused(key: string | undefined): Response | undefined {
|
|
|
286
258
|
const live = (key: string): boolean => /^(sk|rk)_live_/.test(key);
|
|
287
259
|
|
|
288
260
|
/** A new access token for a connection: its generation counted from the ones it had. */
|
|
289
|
-
|
|
261
|
+
/** A connection's access token of one generation: its account, then a digest of the connection's secret and the
|
|
262
|
+
* generation (the secret read before the atomic move that numbers the generation). */
|
|
263
|
+
// p3: keyed by the connection's secret, drawn from ctx.secret
|
|
264
|
+
const accessToken = (ctx: Pick<HandlerContext, 'crypto'>, secret: string, account: string, n: number): string => `sk_test_oauth_${account}_${ctx.crypto.digest('sha256', `${secret}:${n}`, 'base64url').replace(/[-_]/g, '').slice(0, 24)}`;
|
|
290
265
|
|
|
291
266
|
/** Revoke an account's connection, once: the check and the write are one move, and only the move that revoked it sends
|
|
292
267
|
* account.application.deauthorized. Answers whether this call revoked it. */
|
|
293
|
-
async function revoke(ctx:
|
|
294
|
-
const revoked = await ctx
|
|
268
|
+
async function revoke(ctx: HandlerContext, account: string): Promise<boolean> {
|
|
269
|
+
const revoked = await decide(ctx, (rows) => {
|
|
295
270
|
const held = rows(CONNECTIONS).find((c) => c.id === account);
|
|
296
271
|
if (!held || held.revoked === true) return { value: false };
|
|
297
|
-
return { value: true, write: { resource: CONNECTIONS, id: account, fields: { revoked: true, access_token: null, revoked_at:
|
|
272
|
+
return { value: true, write: { resource: CONNECTIONS, id: account, fields: { revoked: true, access_token: null, revoked_at: nowUnix(ctx.occurredAt) }, operation: 'oauth_connection.revoke' } };
|
|
298
273
|
});
|
|
299
274
|
if (revoked) await applicationEvent(ctx, 'account.application.deauthorized', account);
|
|
300
275
|
return revoked;
|
|
301
276
|
}
|
|
302
277
|
|
|
303
|
-
async function token(
|
|
278
|
+
async function token(ctx: HandlerContext): Promise<Response> {
|
|
279
|
+
const request = ctx.call.request;
|
|
304
280
|
const params = await bodyOf(request);
|
|
305
281
|
const key = secretKey(request, params);
|
|
306
282
|
const refused = keyRefused(key);
|
|
307
283
|
if (refused) return refused;
|
|
308
|
-
const ctx = await semanticsContext(manifest, request, OPERATION, scope);
|
|
309
284
|
const grant = params.grant_type;
|
|
310
285
|
if (!grant) return oauthError('invalid_request', 'No grant type specified');
|
|
311
286
|
if (grant !== 'authorization_code' && grant !== 'refresh_token') return oauthError('unsupported_grant_type', `Unsupported grant type: ${grant}. The only currently supported types are authorization_code and refresh_token.`);
|
|
@@ -315,13 +290,13 @@ async function token(request: Request, scope: Scoped): Promise<Response> {
|
|
|
315
290
|
if (live(key!)) return oauthError('invalid_grant', `Authorization code provided does not belong to the livemode of the API key: ${code} is a test mode code, but a live mode key was used.`);
|
|
316
291
|
// a code is spent once: the read and the spend are one move, so two exchanges at once cannot both succeed
|
|
317
292
|
type Redeemed = { outcome: 'missing' } | { outcome: 'used' | 'expired' | 'ok'; row: Row };
|
|
318
|
-
const found = await
|
|
319
|
-
const row = rows(CODES).find((r) => r.
|
|
293
|
+
const found = await decide<Redeemed>(ctx, (rows) => {
|
|
294
|
+
const row = rows(CODES).find((r) => r.sha256 === ctx.crypto.sha256(code));
|
|
320
295
|
if (!row) return { value: { outcome: 'missing' } };
|
|
321
296
|
// a second use is marked in the same move, so the exchange that spent it sees it after writing its connection
|
|
322
|
-
if (row.used === true) return { value: { outcome: 'used', row }, write: { resource: CODES, id:
|
|
323
|
-
if (
|
|
324
|
-
return { value: { outcome: 'ok', row }, write: { resource: CODES, id:
|
|
297
|
+
if (row.used === true) return { value: { outcome: 'used', row }, write: { resource: CODES, id: String(row.id), fields: { reused: true }, operation: 'oauth_code.reuse' } };
|
|
298
|
+
if (nowUnix(ctx.occurredAt) >= Number(row.expires_at)) return { value: { outcome: 'expired', row } };
|
|
299
|
+
return { value: { outcome: 'ok', row }, write: { resource: CODES, id: String(row.id), fields: { used: true, used_at: nowUnix(ctx.occurredAt) }, operation: 'oauth_code.redeem' } };
|
|
325
300
|
});
|
|
326
301
|
if (found.outcome === 'missing') return oauthError('invalid_grant', `Authorization code does not exist: ${code}`);
|
|
327
302
|
const account = String(found.row.account);
|
|
@@ -332,11 +307,12 @@ async function token(request: Request, scope: Scoped): Promise<Response> {
|
|
|
332
307
|
}
|
|
333
308
|
if (found.outcome === 'expired') return oauthError('invalid_grant', `Authorization code expired: ${code}`);
|
|
334
309
|
const granted = String(found.row.scope) as Scope;
|
|
335
|
-
const refresh = `
|
|
336
|
-
const
|
|
310
|
+
const refresh = `rt_${(await ctx.secret(`stripe-oauth-refresh:${String(found.row.id)}`)).replace(/[-_]/g, '').slice(0, 32)}`;
|
|
311
|
+
const secret = await ctx.secret(`stripe-oauth-access:${account}`);
|
|
312
|
+
const connection = { client_id: PLATFORM_CLIENT_ID, account, scope: granted, code: found.row.id, refresh_token: refresh, access_token: accessToken(ctx, secret, account, 1), access_scope: granted, generation: 1, publishable_key: `pk_test_oauth_${account}`, revoked: false, livemode: false };
|
|
337
313
|
await ctx.record(CONNECTIONS, connection, account);
|
|
338
314
|
// the code used again while this exchange was writing: that use found no connection to revoke, so this one does
|
|
339
|
-
if (ctx.rowsRaw(CODES).some((r) => r.id ===
|
|
315
|
+
if (ctx.rowsRaw(CODES).some((r) => r.id === found.row.id && r.reused === true)) {
|
|
340
316
|
await revoke(ctx, account);
|
|
341
317
|
return oauthError('invalid_grant', 'This authorization code has already been used. All tokens issued with this code have been revoked.');
|
|
342
318
|
}
|
|
@@ -354,11 +330,12 @@ async function token(request: Request, scope: Scoped): Promise<Response> {
|
|
|
354
330
|
// "Any existing access token with the same scope and mode ... is revoked": the connection holds the one now issued; the
|
|
355
331
|
// generation is read and written in one move, so two refreshes at once issue two tokens, and a refresh never lands
|
|
356
332
|
// on a connection revoked meanwhile
|
|
357
|
-
const
|
|
333
|
+
const secret = await ctx.secret(`stripe-oauth-access:${account}`);
|
|
334
|
+
const renewed = await decide(ctx, (rows) => {
|
|
358
335
|
const now = rows(CONNECTIONS).find((c) => c.id === account && c.revoked !== true);
|
|
359
336
|
if (!now) return { value: undefined };
|
|
360
337
|
const generation = Number(now.generation ?? 1) + 1;
|
|
361
|
-
const fields = { access_token: accessToken(account, generation), access_scope: asked, generation };
|
|
338
|
+
const fields = { access_token: accessToken(ctx, secret, account, generation), access_scope: asked, generation };
|
|
362
339
|
return { value: { ...now, ...fields }, write: { resource: CONNECTIONS, id: account, fields, operation: 'oauth_connection.refresh' } };
|
|
363
340
|
});
|
|
364
341
|
if (!renewed) return oauthError('invalid_grant', `Refresh token does not exist: ${refresh}`);
|
|
@@ -370,12 +347,12 @@ function tokenBody(c: Row, scope: Scope): Row {
|
|
|
370
347
|
return { access_token: c.access_token, livemode: false, refresh_token: c.refresh_token, scope, stripe_publishable_key: c.publishable_key, stripe_user_id: c.account ?? c.id, token_type: 'bearer' };
|
|
371
348
|
}
|
|
372
349
|
|
|
373
|
-
async function deauthorize(
|
|
350
|
+
async function deauthorize(ctx: HandlerContext): Promise<Response> {
|
|
351
|
+
const request = ctx.call.request;
|
|
374
352
|
const params = await bodyOf(request);
|
|
375
353
|
const key = secretKey(request, params);
|
|
376
354
|
const refused = keyRefused(key);
|
|
377
355
|
if (refused) return refused;
|
|
378
|
-
const ctx = await semanticsContext(manifest, request, OPERATION, scope);
|
|
379
356
|
const client = params.client_id;
|
|
380
357
|
const account = params.stripe_user_id;
|
|
381
358
|
if (!client) return oauthError('invalid_request', 'No client_id provided');
|
|
@@ -388,13 +365,12 @@ async function deauthorize(request: Request, scope: Scoped): Promise<Response> {
|
|
|
388
365
|
return json({ stripe_user_id: account });
|
|
389
366
|
}
|
|
390
367
|
|
|
391
|
-
/** connect.stripe.com's OAuth endpoints
|
|
392
|
-
export function
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
};
|
|
368
|
+
/** connect.stripe.com's OAuth endpoints; Stripe's 404 for anything else there. */
|
|
369
|
+
export async function screen(ctx: HandlerContext): Promise<Response> {
|
|
370
|
+
const request = ctx.call.request;
|
|
371
|
+
const path = new URL(request.url).pathname.replace(/\/+$/, '');
|
|
372
|
+
if (path === '/oauth/authorize' && (request.method === 'GET' || request.method === 'POST')) return authorize(ctx);
|
|
373
|
+
if (path === '/oauth/token' && request.method === 'POST') return token(ctx);
|
|
374
|
+
if (path === '/oauth/deauthorize' && request.method === 'POST') return deauthorize(ctx);
|
|
375
|
+
return notFound();
|
|
400
376
|
}
|
|
@@ -23,25 +23,25 @@
|
|
|
23
23
|
// merchant category codes as its list gives them (./industries.ts), and an industry the page does not offer leaves
|
|
24
24
|
// business_profile.mcc due; the states are the fifty and the District of Columbia; a request with no forwarded address
|
|
25
25
|
// records 127.0.0.1. An update link (account_update) shows the same page.
|
|
26
|
-
import {
|
|
26
|
+
import type { HandlerContext } from '@volter/world-core';
|
|
27
27
|
import { CONSENT_CSS, flowPage, Payment, PAYMENT_CSS, type PaymentField } from '@volter/world-ui';
|
|
28
|
-
import
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
28
|
+
import type { Row } from '../engine/common.ts';
|
|
29
|
+
import { nowUnix } from '../engine/stripe.ts';
|
|
30
|
+
import { formOf, notFound, seeOther, STRIPE_CONSENT_SKIN } from './shared.tsx';
|
|
31
|
+
import { INDUSTRIES as MCCS } from '../engine/industries.ts';
|
|
32
|
+
import { currentlyDue, type Form, type Kind, kindOf, OFFERED } from '../engine/onboarding.ts';
|
|
33
|
+
import { created, syncExternals } from '../semantics/shared.ts';
|
|
33
34
|
|
|
34
35
|
const LINK = 'account_link';
|
|
35
|
-
const OPERATION =
|
|
36
|
+
const OPERATION = { id: 'PostAccountsAccount' };
|
|
36
37
|
|
|
37
38
|
const gone = (): Response => flowPage({ title: 'Stripe', status: 404, css: [CONSENT_CSS, STRIPE_CONSENT_SKIN], body: <main className="consent"><h1 className="consent-heading">This link is invalid.</h1></main> });
|
|
38
39
|
|
|
39
40
|
/** A link that can still be used: not used and not past its expiry. */
|
|
40
|
-
const usable = (ctx:
|
|
41
|
+
const usable = (ctx: HandlerContext, link: Row): boolean => link.used !== true && Number(link.expires_at) > nowUnix(ctx.occurredAt);
|
|
41
42
|
|
|
42
43
|
// the field asks, with no industry chosen until the person chooses one (an unanswered field leaves the MCC due)
|
|
43
44
|
const INDUSTRIES = [{ value: '', label: 'Select your industry' }, ...MCCS.map(([value, label]) => ({ value, label }))];
|
|
44
|
-
const OFFERED = new Set(MCCS.map(([mcc]) => mcc));
|
|
45
45
|
// the fifty states and the District of Columbia, by their USPS codes (the address's `state`, ISO 3166-2 without its
|
|
46
46
|
// country, docs.stripe.com/api/accounts/object)
|
|
47
47
|
const STATES = ([
|
|
@@ -55,49 +55,6 @@ const STATES = ([
|
|
|
55
55
|
['WA', 'Washington'], ['WV', 'West Virginia'], ['WI', 'Wisconsin'], ['WY', 'Wyoming'],
|
|
56
56
|
] as const).map(([value, label]) => ({ value, label }));
|
|
57
57
|
|
|
58
|
-
type Form = Record<string, string>;
|
|
59
|
-
type Kind = 'company' | 'individual';
|
|
60
|
-
const has = (v: Form, k: string): boolean => (v[k] ?? '').trim() !== '';
|
|
61
|
-
const dobPart = (v: Form, i: number): boolean => /^\d{4}-\d{2}-\d{2}$/.test(v.dob ?? '') && Number((v.dob ?? '').split('-')[i]) > 0;
|
|
62
|
-
|
|
63
|
-
/** Each requirement Stripe names, the form's answer that meets it, and which of the two sets it belongs to. */
|
|
64
|
-
const REQUIREMENTS: Array<[string, (v: Form) => boolean, Kind[]]> = [
|
|
65
|
-
['business_profile.mcc', (v) => OFFERED.has((v.mcc ?? '').trim()), ['company', 'individual']],
|
|
66
|
-
['business_profile.url', (v) => has(v, 'url'), ['company', 'individual']],
|
|
67
|
-
['settings.payments.statement_descriptor', (v) => has(v, 'statement_descriptor'), ['company', 'individual']],
|
|
68
|
-
['company.name', (v) => has(v, 'company_name'), ['company']],
|
|
69
|
-
['company.tax_id', (v) => has(v, 'company_tax_id'), ['company']],
|
|
70
|
-
['company.phone', (v) => has(v, 'company_phone'), ['company']],
|
|
71
|
-
['company.address.line1', (v) => has(v, 'company_line1'), ['company']],
|
|
72
|
-
['company.address.city', (v) => has(v, 'company_city'), ['company']],
|
|
73
|
-
['company.address.state', (v) => has(v, 'company_state'), ['company']],
|
|
74
|
-
['company.address.postal_code', (v) => has(v, 'company_postal_code'), ['company']],
|
|
75
|
-
['company.owners_provided', (v) => v.owners_provided === 'provided', ['company']],
|
|
76
|
-
['owners.first_name', (v) => has(v, 'owner_first_name'), ['company']],
|
|
77
|
-
['owners.last_name', (v) => has(v, 'owner_last_name'), ['company']],
|
|
78
|
-
['owners.email', (v) => has(v, 'owner_email'), ['company']],
|
|
79
|
-
...(['first_name', 'last_name', 'email', 'phone'] as const).flatMap((f): Array<[string, (v: Form) => boolean, Kind[]]> => [[`representative.${f}`, (v) => has(v, f), ['company']], [`individual.${f}`, (v) => has(v, f), ['individual']]]),
|
|
80
|
-
...(['day', 'month', 'year'] as const).flatMap((f, i): Array<[string, (v: Form) => boolean, Kind[]]> => [[`representative.dob.${f}`, (v) => dobPart(v, 2 - i), ['company']], [`individual.dob.${f}`, (v) => dobPart(v, 2 - i), ['individual']]]),
|
|
81
|
-
...(['line1', 'city', 'state', 'postal_code'] as const).flatMap((f): Array<[string, (v: Form) => boolean, Kind[]]> => [[`representative.address.${f}`, (v) => has(v, f), ['company']], [`individual.address.${f}`, (v) => has(v, f), ['individual']]]),
|
|
82
|
-
['representative.relationship.title', (v) => has(v, 'title'), ['company']],
|
|
83
|
-
['representative.ssn_last_4', (v) => /^\d{4}$/.test(v.ssn_last_4 ?? ''), ['company']],
|
|
84
|
-
['individual.ssn_last_4', (v) => /^\d{4}$/.test(v.ssn_last_4 ?? ''), ['individual']],
|
|
85
|
-
['tos_acceptance.date', (v) => v.tos === 'accepted', ['company', 'individual']],
|
|
86
|
-
['tos_acceptance.ip', (v) => v.tos === 'accepted', ['company', 'individual']],
|
|
87
|
-
['external_account', (v) => has(v, 'routing_number') && has(v, 'account_number'), ['company', 'individual']],
|
|
88
|
-
];
|
|
89
|
-
|
|
90
|
-
/** The set an account onboards against: the two US sets the twin models, or none. */
|
|
91
|
-
export function kindOf(account: Row): Kind | undefined {
|
|
92
|
-
const type = account.business_type ?? 'individual';
|
|
93
|
-
return (account.country ?? 'US') === 'US' && (type === 'company' || type === 'individual') ? type : undefined;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/** What an account of this kind still owes, given the form's answers (none: all of it). */
|
|
97
|
-
export function currentlyDue(kind: Kind, v: Form = {}): string[] {
|
|
98
|
-
return REQUIREMENTS.filter(([, met, kinds]) => kinds.includes(kind) && !met(v)).map(([req]) => req);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
58
|
const field = (id: string, label: string, v: Form, extra: Partial<PaymentField> = {}): PaymentField => ({ id, label, value: v[id] ?? '', ...extra });
|
|
102
59
|
const addressFields = (prefix: string, v: Form): PaymentField[] => [
|
|
103
60
|
field(`${prefix}line1`, 'Address', v, { autoComplete: 'address-line1' }), field(`${prefix}city`, 'City', v),
|
|
@@ -107,7 +64,7 @@ const addressFields = (prefix: string, v: Form): PaymentField[] => [
|
|
|
107
64
|
/** The industry the field shows: what the form sent, else what the account already holds, when the page offers it. */
|
|
108
65
|
const industryOf = (mcc: unknown): string => (typeof mcc === 'string' && OFFERED.has(mcc.trim()) ? mcc.trim() : '');
|
|
109
66
|
|
|
110
|
-
function page(ctx:
|
|
67
|
+
function page(ctx: HandlerContext, sessionUrl: string, link: Row, kind: Kind, values: Form = {}, error?: string): Response {
|
|
111
68
|
const v = values;
|
|
112
69
|
const business: PaymentField[] = [
|
|
113
70
|
field('mcc', 'Industry', v, { options: INDUSTRIES, value: industryOf(v.mcc ?? (ctx.get('account', String(link.account))?.business_profile as Row | undefined)?.mcc) }),
|
|
@@ -157,13 +114,13 @@ function unmodelled(account: Row): Response {
|
|
|
157
114
|
}
|
|
158
115
|
|
|
159
116
|
/** A submit that leaves something out is refused on the page with what is missing, the owner's answers kept. */
|
|
160
|
-
function incomplete(ctx:
|
|
117
|
+
function incomplete(ctx: HandlerContext, sessionUrl: string, link: Row, kind: Kind, v: Form, missing: string[]): Response {
|
|
161
118
|
return page(ctx, sessionUrl, link, kind, v, `Still required: ${missing.join(', ')}.`);
|
|
162
119
|
}
|
|
163
120
|
|
|
164
121
|
/** The owner submits everything: it is stored on the account, nothing is due, and charges, payouts and the requested
|
|
165
122
|
* capabilities are enabled. */
|
|
166
|
-
async function submit(ctx:
|
|
123
|
+
async function submit(ctx: HandlerContext, link: Row, kind: Kind, v: Form, ip: string): Promise<void> {
|
|
167
124
|
const id = String(link.account);
|
|
168
125
|
const account = ctx.get('account', id);
|
|
169
126
|
if (!account) return;
|
|
@@ -186,7 +143,7 @@ async function submit(ctx: SemanticsContext, link: Row, kind: Kind, v: Form, ip:
|
|
|
186
143
|
...(company
|
|
187
144
|
? { company: { name: v.company_name, phone: v.company_phone, address: addressOf('company_'), tax_id_provided: true, owners_provided: true, directors_provided: false, executives_provided: false } }
|
|
188
145
|
: { individual: { ...person, id: `person_${id}` } }),
|
|
189
|
-
tos_acceptance: { date:
|
|
146
|
+
tos_acceptance: { date: nowUnix(ctx.occurredAt), ip, user_agent: null },
|
|
190
147
|
}, 'account.updated');
|
|
191
148
|
if (company) {
|
|
192
149
|
await created(ctx, 'person', { account: id }, { ...person, relationship: { representative: true, executive: true, director: false, owner: false, percent_ownership: null, title: v.title }, metadata: {} });
|
|
@@ -205,7 +162,7 @@ const SESSIONS = 'https://connect.stripe.com/setup/s/';
|
|
|
205
162
|
|
|
206
163
|
/** Visiting an Account Link consumes it and sends the owner to their onboarding session; a used or expired one sends
|
|
207
164
|
* them to refresh_url for a fresh link. */
|
|
208
|
-
async function visit(ctx:
|
|
165
|
+
async function visit(ctx: HandlerContext, id: string): Promise<Response> {
|
|
209
166
|
const link = ctx.get(LINK, id);
|
|
210
167
|
if (!link) return gone();
|
|
211
168
|
if (!usable(ctx, link)) return seeOther(String(link.refresh_url));
|
|
@@ -215,29 +172,28 @@ async function visit(ctx: SemanticsContext, id: string): Promise<Response> {
|
|
|
215
172
|
}
|
|
216
173
|
|
|
217
174
|
/** connect.stripe.com's onboarding pages, or undefined for any other request. */
|
|
218
|
-
export function
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
return seeOther(String(link.return_url));
|
|
242
|
-
};
|
|
175
|
+
export async function screen(ctx: HandlerContext): Promise<Response> {
|
|
176
|
+
const request = ctx.call.request;
|
|
177
|
+
const path = new URL(request.url).pathname;
|
|
178
|
+
const entry = /^\/setup\/(acctlink_[A-Za-z0-9_]+)\/?$/.exec(path);
|
|
179
|
+
const at = /^\/setup\/s\/(onbs_[A-Za-z0-9_]+)\/?$/.exec(path);
|
|
180
|
+
if (entry && request.method === 'GET') return visit(ctx, entry[1]!);
|
|
181
|
+
if (!at || (request.method !== 'GET' && request.method !== 'POST')) return notFound();
|
|
182
|
+
const sid = at[1]!;
|
|
183
|
+
const form = request.method === 'POST' ? await formOf(request.clone()) : {};
|
|
184
|
+
// the session the visited link opened, until it is submitted
|
|
185
|
+
const stored = ctx.rowsRaw(LINK).find((l) => l._session === sid && l._submitted !== true);
|
|
186
|
+
const link = stored ? ctx.get(LINK, String(stored.id)) : undefined;
|
|
187
|
+
if (!stored || !link) return gone();
|
|
188
|
+
const sessionUrl = `${SESSIONS}${sid}`;
|
|
189
|
+
const account = ctx.get('account', String(link.account)) ?? {};
|
|
190
|
+
const kind = kindOf(account);
|
|
191
|
+
if (!kind) return unmodelled(account);
|
|
192
|
+
if (request.method === 'GET') return page(ctx, sessionUrl, link, kind);
|
|
193
|
+
const missing = currentlyDue(kind, form);
|
|
194
|
+
if (missing.length) return incomplete(ctx, sessionUrl, link, kind, form, missing);
|
|
195
|
+
await ctx.write(LINK, String(stored.id), { _submitted: true }, 'account_link.submitted');
|
|
196
|
+
await submit(ctx, link, kind, form, request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() || '127.0.0.1');
|
|
197
|
+
return seeOther(String(link.return_url));
|
|
243
198
|
}
|
|
199
|
+
|