Files
commerce/api/checkout/wire/instructions.go
zeekay 73b7290e7f square: one webhook route and one verifier; secrets come from the host's KMS
Square keeps an event's created_at across every retry, and the receiver
refused anything older than five minutes, so every retry of an event
first sent while cloud was down (Recreate, ten rollouts a day) was refused
until Square gave up. The verifier now lives in payment/providers/square
and needs only the key and the URL; thirdparty/square is the API client
and no longer registers itself or verifies anything, so every registry
holds one Square type.

Replay is stopped by the event id instead: each verified event is
recorded once for the whole deployment, keyed on the verifying processor
and the event id with an insert-if-absent swap, so a repeat is a
duplicate under any path or org. The path names the one processor that
verifies, and only one that holds its webhook secret may: an unknown name,
or a verifier that needs no secret, is refused rather than tried.
The books an event lands in are the verifying account's, else the
deployment's brand — never a header or session, which on a replay are the
replayer's. An effect that did not land (a store or the ledger did not
answer) leaves the delivery open with a 503, so the provider redelivers;
it used to be acknowledged and never retried. A settlement reads the
payment index before the org's books, because an in-session charge's
receipt is in the payer's org.

POST /v1/commerce/webhooks/:provider is declared once, in setupRoutes, so
standalone and embedded commerce serve the same address; the
/v1/billing/webhooks spelling is gone.

thirdparty/kms read /v1/kms/orgs/<org>/secrets/..., which cloud no longer
serves, and was off in production anyway, so per-org payment credentials
never loaded. It is deleted. secrets.Org reads orgs/<org>/<name> through
the host's reader, the caller re-pointed at that org with zip.ActingAs so
the KMS tenancy check admits it, and payment.Hydrate loads an org's
credentials where the org receives the money: storefront checkout,
storefront subscriptions and wire, and the brand's bank for the billing
wire rail. Hanzo's own billing no longer reads the paying org's
credentials; it charges the deployment's account, as it did. The
standalone binary reads through hanzoai/kms/sdk/go.
2026-10-19 09:12:56 -07:00

61 lines
2.0 KiB
Go

package wire
import (
"errors"
"github.com/zap-proto/zip"
"github.com/hanzoai/commerce/middleware"
"github.com/hanzoai/commerce/payment"
"github.com/hanzoai/commerce/util/json/http"
)
type wireInstructionsResponse struct {
BankName string `json:"bankName,omitempty"`
AccountHolder string `json:"accountHolder,omitempty"`
RoutingNumber string `json:"routingNumber,omitempty"`
AccountNumber string `json:"accountNumber,omitempty"`
SWIFT string `json:"swift,omitempty"`
IBAN string `json:"iban,omitempty"`
BankAddress string `json:"bankAddress,omitempty"`
Reference string `json:"reference,omitempty"`
Instructions string `json:"instructions,omitempty"`
}
// Instructions returns the caller org's wire-transfer instructions.
// GET /v1/checkout/wire/instructions
//
// Fails CLOSED: the org comes ONLY from the authenticated principal (set by the
// identity/token middleware). A request with no resolved org gets a clean 401 —
// never a panic (the previous GetOrganization MustGet 500'd on any org-less
// call after EdgeAuth strips a spoofed X-Org-Id) and never another org's bank
// details. Account/routing/SWIFT are org-specific sensitive data, so there is
// no anonymous read.
func Instructions(c *zip.Ctx) error {
org, ok := middleware.GetOrganizationOK(c)
if !ok || org == nil {
return http.Fail(c, 401, "Authentication required",
errors.New("no authenticated organization for wire instructions"))
}
// The org is the merchant here, so its own wire details are read.
payment.Hydrate(c.Forward(), org, "wire") // Forward carries the caller the host's KMS admits
w := org.Wire
if w.BankName == "" && w.AccountHolder == "" {
return http.Fail(c, 404, "Wire transfer not configured", nil)
}
return http.Render(c, 200, wireInstructionsResponse{
BankName: w.BankName,
AccountHolder: w.AccountHolder,
RoutingNumber: w.RoutingNumber,
AccountNumber: w.AccountNumber,
SWIFT: w.SWIFT,
IBAN: w.IBAN,
BankAddress: w.BankAddress,
Reference: w.Reference,
Instructions: w.Instructions,
})
}