mirror of
https://github.com/hanzoai/commerce.git
synced 2026-10-30 15:08:42 +00:00
Multi-tenant commerce and billing: orders, subscriptions, invoices, plans, usage metering and a credit ledger. One Go binary. MIT OR Apache-2.0. Claude-Session: https://claude.ai/code/session_01SAtaDFm8VDiWrhYHNNtxMw
146 lines
8.8 KiB
Go
146 lines
8.8 KiB
Go
package kms
|
|
|
|
import (
|
|
"errors"
|
|
|
|
"github.com/hanzoai/commerce/models/organization"
|
|
)
|
|
|
|
// secretMapping maps a KMS secret name to a function that sets the value on the org.
|
|
type secretMapping struct {
|
|
path string
|
|
name string
|
|
set func(org *organization.Organization, val string)
|
|
}
|
|
|
|
// Path is where a tenant's credentials for one payment provider live in KMS.
|
|
//
|
|
// The client already scopes every read and write to its own org, so what this
|
|
// has to name is the TENANT commerce is charging for, not the org holding the
|
|
// vault. Seeding and hydrating both call it, because a credential written to an
|
|
// address nobody reads is the same as no credential at all, and says nothing
|
|
// while it fails.
|
|
func Path(tenant, provider string) string {
|
|
return "/tenants/" + tenant + "/" + provider
|
|
}
|
|
|
|
// mappings returns all provider credential mappings for a given org name.
|
|
func mappings(orgName string) []secretMapping {
|
|
stripe := Path(orgName, "stripe")
|
|
square := Path(orgName, "square")
|
|
authnet := Path(orgName, "authorizenet")
|
|
paypal := Path(orgName, "paypal")
|
|
adyen := Path(orgName, "adyen")
|
|
braintree := Path(orgName, "braintree")
|
|
recurly := Path(orgName, "recurly")
|
|
lemon := Path(orgName, "lemonsqueezy")
|
|
wire := Path(orgName, "wire")
|
|
merc := Path(orgName, "mercury")
|
|
|
|
return []secretMapping{
|
|
// Stripe — Live
|
|
{stripe, "STRIPE_LIVE_ACCESS_TOKEN", func(o *organization.Organization, v string) { o.Stripe.Live.AccessToken = v }},
|
|
{stripe, "STRIPE_LIVE_PUBLISHABLE_KEY", func(o *organization.Organization, v string) { o.Stripe.Live.PublishableKey = v }},
|
|
{stripe, "STRIPE_LIVE_REFRESH_TOKEN", func(o *organization.Organization, v string) { o.Stripe.Live.RefreshToken = v }},
|
|
{stripe, "STRIPE_LIVE_USER_ID", func(o *organization.Organization, v string) { o.Stripe.Live.UserId = v }},
|
|
// Stripe — Test
|
|
{stripe, "STRIPE_TEST_ACCESS_TOKEN", func(o *organization.Organization, v string) { o.Stripe.Test.AccessToken = v }},
|
|
{stripe, "STRIPE_TEST_PUBLISHABLE_KEY", func(o *organization.Organization, v string) { o.Stripe.Test.PublishableKey = v }},
|
|
{stripe, "STRIPE_TEST_REFRESH_TOKEN", func(o *organization.Organization, v string) { o.Stripe.Test.RefreshToken = v }},
|
|
{stripe, "STRIPE_TEST_USER_ID", func(o *organization.Organization, v string) { o.Stripe.Test.UserId = v }},
|
|
|
|
// Square — Production
|
|
{square, "SQUARE_PRODUCTION_ACCESS_TOKEN", func(o *organization.Organization, v string) { o.Square.Production.AccessToken = v }},
|
|
{square, "SQUARE_PRODUCTION_LOCATION_ID", func(o *organization.Organization, v string) { o.Square.Production.LocationId = v }},
|
|
{square, "SQUARE_PRODUCTION_APPLICATION_ID", func(o *organization.Organization, v string) { o.Square.Production.ApplicationId = v }},
|
|
// Square — Sandbox
|
|
{square, "SQUARE_SANDBOX_ACCESS_TOKEN", func(o *organization.Organization, v string) { o.Square.Sandbox.AccessToken = v }},
|
|
{square, "SQUARE_SANDBOX_LOCATION_ID", func(o *organization.Organization, v string) { o.Square.Sandbox.LocationId = v }},
|
|
{square, "SQUARE_SANDBOX_APPLICATION_ID", func(o *organization.Organization, v string) { o.Square.Sandbox.ApplicationId = v }},
|
|
// Square — Webhook
|
|
{square, "SQUARE_WEBHOOK_SIGNATURE_KEY", func(o *organization.Organization, v string) { o.Square.WebhookSignatureKey = v }},
|
|
{square, "SQUARE_WEBHOOK_URL", func(o *organization.Organization, v string) { o.Square.WebhookURL = v }},
|
|
|
|
// AuthorizeNet — Live
|
|
{authnet, "AUTHORIZENET_LIVE_LOGIN_ID", func(o *organization.Organization, v string) { o.AuthorizeNet.Live.LoginId = v }},
|
|
{authnet, "AUTHORIZENET_LIVE_TRANSACTION_KEY", func(o *organization.Organization, v string) { o.AuthorizeNet.Live.TransactionKey = v }},
|
|
// AuthorizeNet — Sandbox
|
|
{authnet, "AUTHORIZENET_SANDBOX_LOGIN_ID", func(o *organization.Organization, v string) { o.AuthorizeNet.Sandbox.LoginId = v }},
|
|
{authnet, "AUTHORIZENET_SANDBOX_TRANSACTION_KEY", func(o *organization.Organization, v string) { o.AuthorizeNet.Sandbox.TransactionKey = v }},
|
|
|
|
// PayPal — Live
|
|
{paypal, "PAYPAL_LIVE_EMAIL", func(o *organization.Organization, v string) { o.Paypal.Live.Email = v }},
|
|
{paypal, "PAYPAL_LIVE_SECURITY_USER_ID", func(o *organization.Organization, v string) { o.Paypal.Live.SecurityUserId = v }},
|
|
{paypal, "PAYPAL_LIVE_SECURITY_PASSWORD", func(o *organization.Organization, v string) { o.Paypal.Live.SecurityPassword = v }},
|
|
{paypal, "PAYPAL_LIVE_SECURITY_SIGNATURE", func(o *organization.Organization, v string) { o.Paypal.Live.SecuritySignature = v }},
|
|
{paypal, "PAYPAL_LIVE_APPLICATION_ID", func(o *organization.Organization, v string) { o.Paypal.Live.ApplicationId = v }},
|
|
// PayPal — Test
|
|
{paypal, "PAYPAL_TEST_EMAIL", func(o *organization.Organization, v string) { o.Paypal.Test.Email = v }},
|
|
{paypal, "PAYPAL_TEST_SECURITY_USER_ID", func(o *organization.Organization, v string) { o.Paypal.Test.SecurityUserId = v }},
|
|
{paypal, "PAYPAL_TEST_SECURITY_PASSWORD", func(o *organization.Organization, v string) { o.Paypal.Test.SecurityPassword = v }},
|
|
{paypal, "PAYPAL_TEST_SECURITY_SIGNATURE", func(o *organization.Organization, v string) { o.Paypal.Test.SecuritySignature = v }},
|
|
{paypal, "PAYPAL_TEST_APPLICATION_ID", func(o *organization.Organization, v string) { o.Paypal.Test.ApplicationId = v }},
|
|
|
|
// Adyen
|
|
{adyen, "ADYEN_API_KEY", func(o *organization.Organization, v string) { o.Adyen.APIKey = v }},
|
|
{adyen, "ADYEN_MERCHANT_ACCOUNT", func(o *organization.Organization, v string) { o.Adyen.MerchantAccount = v }},
|
|
{adyen, "ADYEN_HMAC_KEY", func(o *organization.Organization, v string) { o.Adyen.HMACKey = v }},
|
|
{adyen, "ADYEN_ENVIRONMENT", func(o *organization.Organization, v string) { o.Adyen.Environment = v }},
|
|
{adyen, "ADYEN_LIVE_URL_PREFIX", func(o *organization.Organization, v string) { o.Adyen.LiveURLPrefix = v }},
|
|
|
|
// Braintree
|
|
{braintree, "BRAINTREE_MERCHANT_ID", func(o *organization.Organization, v string) { o.Braintree.MerchantID = v }},
|
|
{braintree, "BRAINTREE_PUBLIC_KEY", func(o *organization.Organization, v string) { o.Braintree.PublicKey = v }},
|
|
{braintree, "BRAINTREE_PRIVATE_KEY", func(o *organization.Organization, v string) { o.Braintree.PrivateKey = v }},
|
|
{braintree, "BRAINTREE_ENVIRONMENT", func(o *organization.Organization, v string) { o.Braintree.Environment = v }},
|
|
|
|
// Recurly
|
|
{recurly, "RECURLY_API_KEY", func(o *organization.Organization, v string) { o.Recurly.APIKey = v }},
|
|
{recurly, "RECURLY_SUBDOMAIN", func(o *organization.Organization, v string) { o.Recurly.Subdomain = v }},
|
|
|
|
// LemonSqueezy
|
|
{lemon, "LEMONSQUEEZY_API_KEY", func(o *organization.Organization, v string) { o.LemonSqueezy.APIKey = v }},
|
|
{lemon, "LEMONSQUEEZY_STORE_ID", func(o *organization.Organization, v string) { o.LemonSqueezy.StoreID = v }},
|
|
{lemon, "LEMONSQUEEZY_WEBHOOK_SECRET", func(o *organization.Organization, v string) { o.LemonSqueezy.WebhookSecret = v }},
|
|
{lemon, "LEMONSQUEEZY_DEFAULT_VARIANT_ID", func(o *organization.Organization, v string) { o.LemonSqueezy.DefaultVariantID = v }},
|
|
|
|
// Wire Transfer
|
|
{wire, "WIRE_BANK_NAME", func(o *organization.Organization, v string) { o.Wire.BankName = v }},
|
|
{wire, "WIRE_ACCOUNT_HOLDER", func(o *organization.Organization, v string) { o.Wire.AccountHolder = v }},
|
|
{wire, "WIRE_ROUTING_NUMBER", func(o *organization.Organization, v string) { o.Wire.RoutingNumber = v }},
|
|
{wire, "WIRE_ACCOUNT_NUMBER", func(o *organization.Organization, v string) { o.Wire.AccountNumber = v }},
|
|
{wire, "WIRE_SWIFT", func(o *organization.Organization, v string) { o.Wire.SWIFT = v }},
|
|
{wire, "WIRE_IBAN", func(o *organization.Organization, v string) { o.Wire.IBAN = v }},
|
|
{wire, "WIRE_BANK_ADDRESS", func(o *organization.Organization, v string) { o.Wire.BankAddress = v }},
|
|
{wire, "WIRE_REFERENCE", func(o *organization.Organization, v string) { o.Wire.Reference = v }},
|
|
{wire, "WIRE_INSTRUCTIONS", func(o *organization.Organization, v string) { o.Wire.Instructions = v }},
|
|
|
|
// Mercury
|
|
{merc, "MERCURY_API_TOKEN", func(o *organization.Organization, v string) { o.Mercury.APIToken = v }},
|
|
{merc, "MERCURY_WEBHOOK_SECRET", func(o *organization.Organization, v string) { o.Mercury.WebhookSecret = v }},
|
|
{merc, "MERCURY_ACCOUNT_ID", func(o *organization.Organization, v string) { o.Mercury.AccountID = v }},
|
|
}
|
|
}
|
|
|
|
// Hydrate fetches all provider credentials from KMS and populates the org's
|
|
// integration fields. Missing secrets are silently skipped (not every org uses
|
|
// every provider). Only KMS communication failures are returned as errors.
|
|
func Hydrate(cc *CachedClient, org *organization.Organization) error {
|
|
for _, m := range mappings(org.Name) {
|
|
val, err := cc.GetSecret(m.path, m.name)
|
|
if err != nil {
|
|
// A 404/400 from KMS means the secret is absent — expected, skip.
|
|
var se *StatusError
|
|
if errors.As(err, &se) && (se.Code == 404 || se.Code == 400) {
|
|
continue
|
|
}
|
|
// Real communication failure
|
|
return err
|
|
}
|
|
if val != "" {
|
|
m.set(org, val)
|
|
}
|
|
}
|
|
return nil
|
|
}
|