[Agent] Issue #401: create post apibillingwebhook endpoint 1 #405

Open
pook wants to merge 49 commits from agent-task/401 into main
Owner

Closes #401

Changes

feat: issue #401 create-post-apibillingwebhook-endpoint-1 (agent task liancebot401)

Acceptance Criteria

Create POST /api/billing/webhook endpoint: 1) Apply express.raw({ type: 'application/json' }) middleware before JSON parsing to preserve raw body for signature verification, 2) Verify stripe.webhooks.constructEvent(rawBody, sig, STRIPE_WEBHOOK_SECRET) — return 400 with structured error if signature invalid, 3) Log event type and ID using structured format, 4) Return 200 for all verified events (handler logic in separate tasks). Add STRIPE_WEBHOOK_SECRET to startup env validation. Without signature verification, anyone can forge billing events to unlock paid features — this is a revenue-protection security requirement.


Generated by CEO Planner (priority: 2)

Tokens: 20 in / 4165 out

Closes #401 ## Changes feat: issue #401 create-post-apibillingwebhook-endpoint-1 (agent task liancebot401) ## Acceptance Criteria Create POST /api/billing/webhook endpoint: 1) Apply `express.raw({ type: 'application/json' })` middleware before JSON parsing to preserve raw body for signature verification, 2) Verify `stripe.webhooks.constructEvent(rawBody, sig, STRIPE_WEBHOOK_SECRET)` — return 400 with structured error if signature invalid, 3) Log event type and ID using structured format, 4) Return 200 for all verified events (handler logic in separate tasks). Add STRIPE_WEBHOOK_SECRET to startup env validation. Without signature verification, anyone can forge billing events to unlock paid features — this is a revenue-protection security requirement. --- *Generated by CEO Planner (priority: 2)* Tokens: 20 in / 4165 out
feat: issue #401 create-post-apibillingwebhook-endpoint-1 (agent task liancebot401)
Some checks failed
CI Quality Gate / Lint / Typecheck / Test / Build (pull_request) Has been cancelled
9993afd73c
Author
Owner

PR #405 — Stripe Webhook Endpoint Review

Criteria Assessment

# Criterion Verdict Notes
1 Raw body preservation (not JSON-parsed) PASS Uses Buffer.from(await c.req.arrayBuffer()) which preserves exact bytes. Comment correctly notes that c.req.text() re-encodes through UTF-8 and can break HMAC. No JSON body-parsing middleware intercepts the request before the handler.
2 stripe.webhooks.constructEvent with proper error handling ⚠️ PASS with note Uses synchronous stripe.webhooks.constructEvent(payload, sig, secret) — this works correctly. Error is caught and returns HTTP 400 with the error message. Recommendation: Consider migrating to stripe.webhooks.constructEventAsync() which uses timing-safe comparison internally (available since Stripe SDK v8+). The sync version uses crypto.timingSafeEqual in recent versions so this is not a blocker, but the async variant is Stripe's recommended path.
3 Signature verification before any processing PASS Signature is verified in the try/catch block before the event-type switch statement. Missing stripe-signature header returns 400 immediately. Failed verification throws 400. No event data is accessed until after successful verification.
4 Returns 200 for valid events, 400/401 for invalid PASS Valid events → { received: true } (HTTP 200). Missing signature header → HTTP 400. Failed signature verification → HTTP 400. Handler errors → HTTP 500 (appropriate).

Additional Findings

Structured logging added (good):

  • Error path: JSON.stringify({ level: "error", source: "billing/webhook", error: "signature_verification_failed", detail: msg })
  • Success path: JSON.stringify({ level: "info", source: "billing/webhook", eventType: event.type, eventId: event.id })

Dual route mount:

  • billingRouter is now mounted at both /billing and /api/billing in index.ts. The webhook is therefore accessible at both /billing/webhook and /api/billing/webhook. Confirm this is intentional — having two paths to the same webhook could complicate Stripe dashboard configuration and log analysis.

STRIPE_WEBHOOK_SECRET added to required env vars — correctly added to env.ts validation.

Body size limit: Global 100KB bodyLimit middleware applies to all routes including the webhook. Stripe payloads rarely exceed this, but if you ever enable expanded objects in webhook events, payloads can grow. Worth monitoring.

Rate limiter: Webhook endpoint has a 20 req/60s rate limiter. Stripe retries failed webhooks with exponential backoff, but during high-volume events (e.g., mass subscription changes), 20/min could cause legitimate drops. Consider raising or exempting the webhook from rate limiting since signature verification already provides authentication.

Type Check

tsc --noEmit shows 0 errors in billing code. All 18 errors are pre-existing in test files (document-generator.test.ts, input-limits.test.ts) — unrelated to this PR.

Verdict: MERGE-READY

All 4 acceptance criteria pass. The recommendations above (async construct, rate limit tuning, dual-mount confirmation) are non-blocking improvements that can be addressed in follow-up PRs.


Reviewed by Claude Code — automated billing pipeline review

## PR #405 — Stripe Webhook Endpoint Review ### Criteria Assessment | # | Criterion | Verdict | Notes | |---|-----------|---------|-------| | 1 | Raw body preservation (not JSON-parsed) | ✅ PASS | Uses `Buffer.from(await c.req.arrayBuffer())` which preserves exact bytes. Comment correctly notes that `c.req.text()` re-encodes through UTF-8 and can break HMAC. No JSON body-parsing middleware intercepts the request before the handler. | | 2 | `stripe.webhooks.constructEvent` with proper error handling | ⚠️ PASS with note | Uses synchronous `stripe.webhooks.constructEvent(payload, sig, secret)` — this works correctly. Error is caught and returns HTTP 400 with the error message. **Recommendation:** Consider migrating to `stripe.webhooks.constructEventAsync()` which uses timing-safe comparison internally (available since Stripe SDK v8+). The sync version uses `crypto.timingSafeEqual` in recent versions so this is not a blocker, but the async variant is Stripe's recommended path. | | 3 | Signature verification before any processing | ✅ PASS | Signature is verified in the try/catch block **before** the event-type switch statement. Missing `stripe-signature` header returns 400 immediately. Failed verification throws 400. No event data is accessed until after successful verification. | | 4 | Returns 200 for valid events, 400/401 for invalid | ✅ PASS | Valid events → `{ received: true }` (HTTP 200). Missing signature header → HTTP 400. Failed signature verification → HTTP 400. Handler errors → HTTP 500 (appropriate). | ### Additional Findings **Structured logging added (good):** - Error path: `JSON.stringify({ level: "error", source: "billing/webhook", error: "signature_verification_failed", detail: msg })` - Success path: `JSON.stringify({ level: "info", source: "billing/webhook", eventType: event.type, eventId: event.id })` **Dual route mount:** - `billingRouter` is now mounted at both `/billing` and `/api/billing` in `index.ts`. The webhook is therefore accessible at both `/billing/webhook` and `/api/billing/webhook`. Confirm this is intentional — having two paths to the same webhook could complicate Stripe dashboard configuration and log analysis. **`STRIPE_WEBHOOK_SECRET` added to required env vars** — correctly added to `env.ts` validation. **Body size limit:** Global 100KB `bodyLimit` middleware applies to all routes including the webhook. Stripe payloads rarely exceed this, but if you ever enable expanded objects in webhook events, payloads can grow. Worth monitoring. **Rate limiter:** Webhook endpoint has a 20 req/60s rate limiter. Stripe retries failed webhooks with exponential backoff, but during high-volume events (e.g., mass subscription changes), 20/min could cause legitimate drops. Consider raising or exempting the webhook from rate limiting since signature verification already provides authentication. ### Type Check `tsc --noEmit` shows **0 errors in billing code**. All 18 errors are pre-existing in test files (`document-generator.test.ts`, `input-limits.test.ts`) — unrelated to this PR. ### Verdict: ✅ MERGE-READY All 4 acceptance criteria pass. The recommendations above (async construct, rate limit tuning, dual-mount confirmation) are non-blocking improvements that can be addressed in follow-up PRs. --- *Reviewed by Claude Code — automated billing pipeline review*
Some checks failed
CI Quality Gate / Lint / Typecheck / Test / Build (pull_request) Has been cancelled
This pull request has changes conflicting with the target branch.
  • .forgejo/workflows/ci.yml
  • bun.lock
  • package.json
  • packages/api/src/db/schema.ts
  • packages/api/src/index.ts
  • packages/api/src/middleware/rate-limit.ts
  • packages/api/src/middleware/security-headers.ts
  • packages/api/src/routes/billing.ts
  • packages/api/src/routes/generate-tos.ts
  • packages/api/src/routes/generate.ts
  • packages/api/src/routes/health.ts
  • packages/api/src/routes/questionnaire.ts
  • packages/api/src/services/document-generator.ts
  • packages/api/src/services/llm.ts
  • packages/api/src/templates/index.ts
  • packages/api/tsconfig.json
  • packages/shared/src/types.ts
  • packages/web/src/app/questionnaire/page.tsx
  • packages/web/src/components/documents/DocumentList.tsx
  • packages/web/src/components/questionnaire/ReviewStep.tsx
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin agent-task/401:agent-task/401
git switch agent-task/401
Sign in to join this conversation.
No reviewers
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
pook/compliancebot!405
No description provided.