feat(registry): Square web checkout: payment links and a signed webhook that grants and revokes #15

Merged
kblack0610 merged 1 commit from feat/square-checkout into main 2026-11-04 02:24:29 +00:00
Owner

Phase 3 of the premium-content plan: the registry sells catalog items through Square-hosted checkout and grants entitlements from Square's webhook.

  • POST /api/v1/checkout (account, rate limited): {game, itemId} -> {url}. Price from catalog_items, never the client; refuses unknown or inactive (404), owned (409), free (400); Square failure is 502. Stores a pending row in square_checkouts keyed by order id.
  • POST /api/v1/webhooks/square: base64 HMAC-SHA256 of notification URL + raw body, constant-time compare, 401 otherwise. Event ids recorded in square_events in the same transaction as the grant, so a replay does nothing. payment.updated COMPLETED with a matching amount and currency grants (source square, receipt = payment id); refund.* COMPLETED or refunded_money on a payment revokes via the new revokeByReceipt.
  • Env: SQUARE_ACCESS_TOKEN, SQUARE_LOCATION_ID, SQUARE_WEBHOOK_SIGNATURE_KEY, SQUARE_WEBHOOK_URL, SQUARE_ENVIRONMENT (sandbox default, bad value stops boot), SQUARE_API_BASE_URL, CHECKOUT_REDIRECT_URL. Any of the first four unset -> both routes 503.
  • Migration 0005_square_checkout.sql.
  • Removed: the admin grant's inline INSERT, now grantEntitlement, shared with the webhook.

Tested against a fake Square server (square_test.go); go vet ./... and go test -count=1 ./... green locally. No real Square credentials yet.

https://claude.ai/code/session_01BALUC4Q8G6QL3byFZR6Mhu

Phase 3 of the premium-content plan: the registry sells catalog items through Square-hosted checkout and grants entitlements from Square's webhook. - `POST /api/v1/checkout` (account, rate limited): `{game, itemId}` -> `{url}`. Price from `catalog_items`, never the client; refuses unknown or inactive (404), owned (409), free (400); Square failure is 502. Stores a pending row in `square_checkouts` keyed by order id. - `POST /api/v1/webhooks/square`: base64 HMAC-SHA256 of notification URL + raw body, constant-time compare, 401 otherwise. Event ids recorded in `square_events` in the same transaction as the grant, so a replay does nothing. `payment.updated` COMPLETED with a matching amount and currency grants (source `square`, receipt = payment id); `refund.*` COMPLETED or `refunded_money` on a payment revokes via the new `revokeByReceipt`. - Env: `SQUARE_ACCESS_TOKEN`, `SQUARE_LOCATION_ID`, `SQUARE_WEBHOOK_SIGNATURE_KEY`, `SQUARE_WEBHOOK_URL`, `SQUARE_ENVIRONMENT` (sandbox default, bad value stops boot), `SQUARE_API_BASE_URL`, `CHECKOUT_REDIRECT_URL`. Any of the first four unset -> both routes 503. - Migration `0005_square_checkout.sql`. - Removed: the admin grant's inline INSERT, now `grantEntitlement`, shared with the webhook. Tested against a fake Square server (`square_test.go`); `go vet ./...` and `go test -count=1 ./...` green locally. No real Square credentials yet. https://claude.ai/code/session_01BALUC4Q8G6QL3byFZR6Mhu
POST /api/v1/checkout makes a Square payment link at the catalog price and stores the
pending order (migration 0005). POST /api/v1/webhooks/square checks the HMAC signature,
handles each event once, grants on a completed payment whose amount matches, and revokes
on a completed refund. Off (503) without the SQUARE_* settings; sandbox by default.

The admin grant now goes through grantEntitlement, shared with the webhook, and
revokeByReceipt sits next to the admin revoke.

Claude-Session: https://claude.ai/code/session_01BALUC4Q8G6QL3byFZR6Mhu
kblack0610 deleted branch feat/square-checkout 2026-11-04 02:24:29 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
kblack0610/bnb-game-servers!15
No description provided.