feat(webhook): add local HTTP transport to account-webhook#6
Merged
Conversation
Currently account-webhook is Lambda-only and refuses to start outside
the Lambda runtime. That blocks the obvious local dev loop:
stripe listen --forward-to localhost:8092/webhook
Mirror the api-platform pattern (cmd/account/main.go:251-260,
cmd/applications/main.go, cmd/dispatch/main.go) where the same binary
auto-detects Lambda via AWS_LAMBDA_FUNCTION_NAME and falls back to
http.ListenAndServe otherwise. One body of logic, two thin transports.
What changed:
- cmd/account-webhook/main.go
- main() dispatches on isLambda():
lambda.Start(proxyHandler(router)) // Lambda
http.ListenAndServe(":"+port, mux) // local HTTP
- Env-reading moved out of init() into buildRouter() so tests don't
trip env validation when the test binary starts.
- proxyHandler refactored to closure constructor taking *webhook.Router
(testability — closures over the shared dep).
- New httpHandler(*webhook.Router) http.HandlerFunc mirrors
proxyHandler for the HTTP transport.
- New writeJSONResponse helper for HTTP; existing jsonResponse renamed
proxyResponse for accuracy (both transports emit JSON; the
distinction is the wire envelope).
- Body read wrapped in http.MaxBytesReader (512 KB ceiling) to defend
the local HTTP path against pathological dev requests; Stripe caps
webhook payloads at ~256 KB.
- isLambda() now checks AWS_LAMBDA_FUNCTION_NAME only (matches
cmd/account-api/main.go:251). The earlier AWS_EXECUTION_ENV fallback
was unnecessary and divergent.
- defer r.Body.Close() registered at handler entry so the error path
still releases the body.
- cmd/account-webhook/main_test.go (new)
- 5 tests against the new closure-form handlers using local
stubVerifier + stubStore implementations:
TestHTTPHandler_BadSignature (400 + bad signature)
TestHTTPHandler_UnhandledEvent (200 + unhandled)
TestHTTPHandler_Duplicate (200 + duplicate; short-circuits dispatch)
TestProxyHandler_BadSignature (Lambda transport, same router contract)
TestProxyHandler_LowercaseHeaderFallback
(API Gateway lowercase header probe)
- Makefile: new `make dev-webhook` target (`cd cmd/account-webhook && go run .`)
- CLAUDE.md: Quickstart updated to document both local dev paths.
What's NOT in this PR (deferred to a dedicated refactor):
The /simplify Code Reuse review flagged that this PR introduces the
second cmd binary in the repo, which is the natural inflection point to
graduate cmd/account-api's open-coded helpers (mustEnv, mustPgxPool,
Port, IsLambda, the JSON-response writer) into internal/shared/config/
and internal/shared/httputil/, and to extract the fakeVerifier +
fakeStore from internal/account/webhook/router_test.go into a
webhooktest subpackage shared between this test and the existing
router_test.go.
That's a cross-binary cleanup with its own scope and review surface —
opening as a separate PR per the project's "separate PRs per
component" convention. This PR's value (unblocking local webhook
testing) doesn't depend on it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
I-am-nothing
added a commit
that referenced
this pull request
Jun 16, 2026
Milestone D PR #3 — the ms_billing usage-metering storage + ingest spine. Metering is declaration-first: a module declares each metric once via ms.Meter(name, kind, ms.Unit, ms.Price); the declaration flows into the manifest and api-platform syncs it into metric_definitions via SetMetricDefinitions. kind is never on the wire (the catalog is authoritative) and an undeclared metric is rejected at ingest. Migrations 006-010 (born-clean, all ms_billing.*): - 006 metric_definitions + metric_kind enum (manifest-fed catalog; unit_price_micros = final per-unit customer price, NULL = unpriced) - 007 usage_events (raw, immutable, idempotent on event_id; lazy NULL account; declared kind snapshotted) - 008 billing_periods (table only; its writer ships with the cycle binary) - 009 usage_aggregates (snapshot billable record; 10/10 = NO markup for custom metrics; margin_share_class is developer-settlement only) - 010 module_visibility (developer margin-share mirror; default private) internal/account/usage (new pkg) — Store + pgxStore + in-memory fake + Service: - RecordUsage: idempotent ON CONFLICT(event_id) DO NOTHING; resolves the declared kind from the catalog and REJECTS an undeclared or retired metric (INVALID_INPUT); rejects reserved platform.* / infra.*; lazy owner-account resolution. - GetUsageSummary: live current-period charged per metric = quantity × declared unit_price (NO blanket 1.2×; custom metric = declared price). - SetMetricDefinitions: validates the full set, then upserts it in ONE transaction (all-or-nothing) so the catalog is never partially synced. - SetModuleVisibility: upserts the developer margin-share mirror. cmd/account-api — dispatcher cases + chi routes; RecordUsage gated by a dedicated meter secret/header (METER_SECRET / X-MS-Meter-Secret), never overloading X-MS-Internal-Secret. Deferred to PR #5/#6: period rollup + per-kind aggregation, the flat-1.2× platform-infra markup, OpenPeriodForAccount, Stripe charge + cycle binary + migrations 011-013. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced Jun 17, 2026
I-am-nothing
added a commit
that referenced
this pull request
Jun 18, 2026
feat: Stripe charge + billing-cycle (usage/arrears leg, Milestone D PR #6)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
cmd/account-webhookis currently Lambda-only and refuses to start outside the Lambda runtime. That blocks the obvious local dev loop for verifying signature verification + idempotency + handler dispatch against real test-mode events:The sister
cmd/account-apibinary already runs in dual mode, and so does every comparable Lambda inapi-platform(cmd/account,cmd/applications,cmd/dispatch). This PR bringsaccount-webhookin line with that convention.What
One body of logic, two thin transports —
main()detects the Lambda runtime and dispatches:Both transports decode the request body +
+ "Stripe-Signature" +header and feed them to the samewebhook.Router.Process— the verifier and per-event handlers don't change.Code changes
cmd/account-webhook/main.goinit()intobuildRouter();proxyHandlerand newhttpHandlerare closures taking*webhook.Router(testable);main()dispatches viaisLambda(); body wrapped inhttp.MaxBytesReader(512 KB) for local-path defense.cmd/account-webhook/main_test.gostubVerifier+stubStore. Covers bad signature, unhandled event, duplicate (idempotency short-circuit), and the Lambda transport's lowercase-header fallback.Makefilemake dev-webhooktarget.CLAUDE.mdBehavior matrix
AWS_LAMBDA_FUNCTION_NAMEsetACCOUNT_WEBHOOK_PORT(default8092)/webhookWhy
AWS_LAMBDA_FUNCTION_NAME?Single env var, matching
cmd/account-api/main.go:251andapi-platform/internal/shared/config/config.go. The first draft of this PR also probedAWS_EXECUTION_ENV; the /simplify reuse review flagged the divergence — fixed in this PR.How to use locally
The handler verifies signatures, marks the event processed (idempotent), and writes the side effects to
ms_billing.*./simplify pass
Ran the 3-agent review before push. Applied fixes:
isLambda()to useAWS_LAMBDA_FUNCTION_NAMEonly (was probing two env vars; divergent from sister binary).defer r.Body.Close()to handler entry so the error path still releases the body.http.MaxBytesReader(512 KB) to defend the local HTTP path against pathological dev requests; Stripe caps webhook payloads at ~256 KB.localWebhookPath→webhookPath(the path applies in both transports, not just local).http.StatusBadRequestconstant instead of magic400.silentLogger()test helper.What's NOT in this PR (deferred to a dedicated refactor)
The /simplify Code Reuse review flagged that this PR introduces the second cmd binary in the repo, which is the natural inflection point to:
cmd/account-api's open-coded helpers (mustEnv,mustPgxPool, port resolution,IsLambda, JSON response writer) intointernal/shared/config/andinternal/shared/httputil/so both binaries call into one place.fakeVerifier+fakeStorefrominternal/account/webhook/router_test.gointo ainternal/account/webhook/webhooktest/subpackage shared between that test and this newcmd/account-webhook/main_test.go.That's a cross-binary cleanup with its own scope and review surface — opening as a separate PR per the project's "separate PRs per component" convention. This PR's value (unblocking local webhook testing) doesn't depend on it.
Verification
🤖 Generated with Claude Code