Skip to content

Webhook dashboard: recent-events and delivery-URL endpoints (#1393) - #1571

Merged
helpcodeai merged 6 commits into
HelpCode-ai:mainfrom
rangarajan19:feat/webhook-dashboard-endpoints
Nov 10, 2026
Merged

helpcodeai merged 6 commits into
HelpCode-ai:mainfrom
rangarajan19:feat/webhook-dashboard-endpoints

Conversation

@rangarajan19

@rangarajan19 rangarajan19 commented Nov 7, 2026 •

Copy link
Copy Markdown
Contributor

Backend and UI for #1393's point 2.

What's here

  • GET /api/connectors/:id/webhook/events — wraps the existing WebhookEventsService.recentEvents(). Scoped by connector.organizationId (never a client-supplied value); the service's own query filters by it too.
  • GET /api/connectors/:id/webhook/url — the delivery URL to paste into the provider, built server-side from SERVER_URL, the same way GET /api/connectors/oauth/redirect-uri builds the OAuth callback.
  • webhook-card.tsx — the connector page's "Inbound webhooks" card: a switch for webhookEnabled, the delivery URL with a copy button, a write-only field for the signing secret (never read back, like authConfig), and the last ten deliveries with a refresh button. It calls only the endpoints above, so it carries no gating, retention, or cap logic of its own.

Against your 4 notes

  1. Reuses the existing service method; scoped by organizationId; gated by assertCanWrite, confirmed as the intended check.
  2. Done — its own component, rendered from the connector page; 9 lines added to the page itself to wire it in.
  3. Done, mirrors oauthRedirectUri().
  4. No plan-limit code added.

Testing

  • packages/backend/src/connectors/connectors.controller.spec.ts: 9 new tests (SERVER_URL built correctly + localhost fallback, viewer rejected, non-owner member rejected, cross-org rejected, limit/since passed through correctly).
  • npx tsc --noEmit and eslint clean on every touched file, backend and frontend.
  • Adding WebhookEventsService to the controller's constructor required updating 8 spec files that build ConnectorsController positionally with mocks.
  • Ran the app locally end to end: enabled the toggle, saved a secret, sent a real signed test delivery to the webhook endpoint, confirmed it showed up in the card's events list, then disabled it again and confirmed the card and its auto-created tool both tore down cleanly.
  • Doc note added to docs/operations/observability.md; llms-full.txt regenerated.
  • Translated the new card's strings into the other two locales this repo ships (de.json, it.json).

…de-ai#1393)

GET /api/connectors/:id/webhook/events wraps the existing
WebhookEventsService.recentEvents() for the dashboard, scoped by
organizationId and gated like any other connector setting (owner or
admin). GET /api/connectors/:id/webhook/url returns the delivery URL
to paste into the provider, built server-side from SERVER_URL the
same way oauthRedirectUri() builds the OAuth callback.

Plan limits are intentionally left out per the maintainer's note on
HelpCode-ai#1393; the dashboard UI is a follow-up once the endpoint shape is
confirmed.
CI's amcp-cli-operations check caught that packages/cli/src/generated/operations.ts
was stale after adding the two webhook GET endpoints.
@helpcodeai

Copy link
Copy Markdown
Contributor

Thanks @rangarajan19, the endpoint shape looks good to us.

On your question: owner-or-admin via assertCanWrite is what we meant. It matches the other connector settings and the connector page, so keep it as it is.

We also checked the rest: both routes take the organization from the connector, recentEvents caps the limit at 50 and handles bad values, and the URL comes from SERVER_URL. Please go ahead with the UI component in the same PR, and mark it ready when it is done.

Adds the connector page's "Inbound webhooks" card: a switch to opt the
connector in, the delivery URL to copy, the signing secret (write-only,
never read back), and the last ten verified deliveries. Calls the
recent-events and delivery-URL endpoints from the earlier backend PR;
carries no logic of its own about gating, retention, or the per-event
cap.
@rangarajan19
rangarajan19 marked this pull request as ready for review November 8, 2026 13:11
@rangarajan19

rangarajan19 commented Nov 8, 2026 •

Copy link
Copy Markdown
Contributor Author

@helpcodeai Implemented the UI — the "Inbound webhooks" card is now in the same PR, built from your 4 notes and confirmed against them in the description above. Marked it ready for review.

@helpcodeai helpcodeai left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @rangarajan19, this is close. We read the whole diff: both routes check assertCanWrite first, the organization always comes from the connector, the secret is write-only and never sent back, and the card renders event fields as plain text. No security problem found.

Two small changes before we merge:

  1. Show the card only where it applies. Right now it appears on every connector page (Etsy, Odoo, databases, File Search...), and the text says Stripe. Please render WebhookCard only when the connector is a Stripe connector (connector.config?.adapterSlug === 'stripe') or when webhookEnabled is already on, so someone who turned it on through the API can still see and turn it off.
  2. Leave the payload out of the dashboard response. The card only shows type, id and time, but GET :id/webhook/events returns each full payload (up to 256 KB each, with customer data). Please map the events to { id, provider, eventType, externalId, receivedAt } in the controller. The MCP tool keeps the full payload as it is now. A test that checks payload is not in the response would be good.

The branch is also behind main; a merge from main when you push is enough.

…rd endpoint

- Render WebhookCard only for a Stripe connector or one that already has
  webhookEnabled on, instead of every connector type.
- GET :id/webhook/events now returns event metadata only (id, provider,
  eventType, externalId, receivedAt); the payload stays out of the
  dashboard response. The MCP tool's own read path is untouched and still
  returns the full payload.
- Test asserting payload is absent from the dashboard response.
@rangarajan19

Copy link
Copy Markdown
Contributor Author

@helpcodeai Addressed both:

  1. WebhookCard now only renders when connector.config?.adapterSlug === 'stripe' or webhookEnabled is already on.
  2. GET :id/webhook/events now maps each row to { id, provider, eventType, externalId, receivedAt } in the controller — payload never leaves it. The MCP tool's own read path calls the service directly and is untouched, so it still gets the full payload. Added a test asserting payload is absent from the dashboard response.

Also merged main in. CI is green. Re-requesting review.

@helpcodeai helpcodeai left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @rangarajan19, both points are done: the card now shows only on Stripe connectors or when webhooks are already on, and the dashboard events no longer carry the payload. We checked the routes again (organization from the connector, owner or admin only, limit capped). Merging now.

@helpcodeai
helpcodeai merged commit c1dd9b1 into HelpCode-ai:main Nov 10, 2026
16 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Nov 10, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants