Skip to content

✨ server: handle bridge webhook events#861

Open
mainqueg wants to merge 2 commits intomainfrom
bridge-hook
Open

✨ server: handle bridge webhook events#861
mainqueg wants to merge 2 commits intomainfrom
bridge-hook

Conversation

@mainqueg
Copy link
Contributor

@mainqueg mainqueg commented Mar 4, 2026

closes #436

Summary by CodeRabbit

  • New Features

    • Added Bridge webhook integration to handle payment events, customer status changes, and deposits with push notifications and analytics for fiat onramp activity; new HTTP route enabled.
  • Tests

    • Added comprehensive tests for webhook validation, signature handling, event-driven tracking, and notifications; updated expectations for onramp property rename.
  • Chores

    • Renamed analytics property fiatAmount → amount for Onramp events.
    • Exposed bridge currency constant and added webhook public key env configuration.

Open with Devin

@changeset-bot
Copy link

changeset-bot bot commented Mar 4, 2026

🦋 Changeset detected

Latest commit: 7672e26

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@exactly/server Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@gemini-code-assist
Copy link

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a new webhook handler for the 'Bridge' service, enabling the server to process various events such as customer status updates, fund receipts, and liquidation address drains. It includes robust signature validation for security, integrates with notification and analytics platforms, and standardizes the Onramp event tracking property. This enhancement allows for real-time processing of Bridge-related activities and improves data consistency across different financial ramp integrations.

Highlights

  • New Bridge Webhook Handler: A new Hono route and logic were introduced to process webhook events from the 'Bridge' service, including customer status updates, fund receipts, and liquidation address drains.
  • Webhook Signature Validation: A robust headerValidator function was implemented to verify the authenticity and integrity of incoming Bridge webhooks using RSA-SHA256 signatures and timestamp checks.
  • Integration with Existing Services: The new webhook handler integrates with Sentry for error capture, OneSignal for push notifications, and Segment for analytics tracking based on Bridge events.
  • Standardized Onramp Event Property: The Onramp event in Segment tracking was updated to use a generic amount property instead of fiatAmount for better consistency across different ramp providers.
  • Comprehensive Test Suite: A new test suite (server/test/hooks/bridge.test.ts) was added to ensure the correct functioning of the Bridge webhook handler under various scenarios, including valid/invalid signatures and different payload types.
Changelog
  • .changeset/thin-pumas-brush.md
    • Added a new changeset file.
  • server/hooks/bridge.ts
    • Added a new file to handle Bridge webhook events.
  • server/hooks/manteca.ts
    • Updated the Onramp event property from fiatAmount to amount.
  • server/index.ts
    • Integrated the new Bridge webhook handler into the main Hono application.
  • server/test/hooks/bridge.test.ts
    • Added a new test file for the Bridge webhook handler.
  • server/test/hooks/manteca.test.ts
    • Updated the Onramp event property from fiatAmount to amount in tests.
  • server/utils/ramps/bridge.ts
    • Exported BridgeCurrency for use in other modules.
  • server/utils/segment.ts
    • Modified the Onramp event type to use amount instead of fiatAmount.
  • server/vitest.config.mts
    • Added BRIDGE_WEBHOOK_PUBLIC_KEY to the test environment variables.
Activity
  • No specific activity (comments, reviews) has been recorded yet for this pull request.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@coderabbitai
Copy link

coderabbitai bot commented Mar 4, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds a new Bridge webhook POST endpoint with signed-payload validation, credential lookup, user context resolution, push notifications, and analytics tracking; updates Onramp event property fiatAmountamount; exports BridgeCurrency; adds tests, a changeset, and test env key.

Changes

Cohort / File(s) Summary
Bridge Webhook
server/hooks/bridge.ts, server/test/hooks/bridge.test.ts, server/index.ts, server/vitest.config.mts
New Hono POST route at /hooks/bridge implementing RSA‑SHA256 header signature validation, Valibot payload validation, DB credential lookup, user context establishment, OneSignal push notifications, Segment tracking, Sentry error capture; comprehensive tests added and BRIDGE public key injected into test env; route registered in server index.
Onramp Tracking Shape
server/utils/segment.ts, server/hooks/manteca.ts, server/test/hooks/manteca.test.ts
Renamed Onramp tracking property from fiatAmount to amount in tracking payloads and tests; manteca hook updated to use amount (computed value unchanged).
Ramp Bridge Utilities
server/utils/ramps/bridge.ts
Exported BridgeCurrency constant (visibility change only).
Release Metadata
.changeset/thin-pumas-brush.md
New changeset declaring a patch release for @exactly/server with description about handling bridge webhook events.
Deployment Config
.do/app.yaml
Added BRIDGE_WEBHOOK_PUBLIC_KEY environment variable for the server service.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Bridge (Webhook Sender)
    participant Server as Bridge Hook Handler
    participant DB as Database
    participant Sentry as Sentry
    participant OneSignal as Push Notification Service
    participant Segment as Analytics Service

    Client->>Server: POST webhook payload + signature header
    Server->>Server: Validate signature (RSA‑SHA256)
    alt Signature invalid
        Server-->>Client: 401 { code: "unauthorized" }
    else Signature valid
        Server->>Server: Validate payload schema
        alt Payload invalid
            Server-->>Client: 200 { code: "bad bridge" }
        else Payload valid
            Server->>DB: Lookup credential by bridgeId
            alt Credential not found
                Server->>Sentry: Capture context
                Server-->>Client: 200 { code: "credential not found" }
            else Credential found
                Server->>Server: Establish user context
                alt Event = payment_submitted or drain.payment_submitted
                    Server->>OneSignal: Send deposit notification
                    Server->>Segment: Track Onramp event (amount, currency, source, usdcAmount)
                    Server-->>Client: 200 { code: "ok" }
                else Event = customer.updated.status_transitioned
                    alt status = "active"
                        Server->>Segment: Track RampAccount event
                        Server->>OneSignal: Send fiat onramp activation notification
                        Server-->>Client: 200 { code: "ok" }
                    else
                        Server-->>Client: 200 { code: "ok" }
                    end
                else
                    Server-->>Client: 200 { code: "ok" }
                end
                Note over OneSignal,Segment: Errors captured in Sentry but do not block response
            end
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • exactly/exa#756: Overlaps on Onramp event schema changes and manteca tracking updates (fiatAmount → amount).
  • exactly/exa#814: Related bridge onramp functionality and changes to ramp/bridge utilities.

Suggested reviewers

  • nfmelendez
  • cruzdanilo
  • dieguezguille
  • franm91
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: implementing bridge webhook event handling in the server.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch bridge-hook

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

gemini-code-assist[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

Base automatically changed from bridge to main March 4, 2026 14:40
@mainqueg mainqueg force-pushed the bridge-hook branch 2 times, most recently from 468bb15 to d67d97d Compare March 4, 2026 14:57
@sentry
Copy link

sentry bot commented Mar 4, 2026

Codecov Report

❌ Patch coverage is 89.06250% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 71.94%. Comparing base (3b75acd) to head (7672e26).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
server/hooks/bridge.ts 90.16% 4 Missing and 2 partials ⚠️
server/utils/ramps/bridge.ts 50.00% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #861      +/-   ##
==========================================
+ Coverage   71.13%   71.94%   +0.81%     
==========================================
  Files         211      212       +1     
  Lines        8349     8530     +181     
  Branches     2727     2813      +86     
==========================================
+ Hits         5939     6137     +198     
+ Misses       2132     2123       -9     
+ Partials      278      270       -8     
Flag Coverage Δ
e2e 70.45% <43.75%> (-0.68%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
server/hooks/bridge.ts (1)

125-177: ⚠️ Potential issue | 🟠 Major

Guard side effects with webhook idempotency.

track() and sendPushNotification() run on every delivery in this block. Retries/duplicates will emit repeated analytics and notifications unless you dedupe on a stable event key before side effects.

server/test/hooks/bridge.test.ts (1)

25-36: ⚠️ Potential issue | 🟠 Major

Add teardown for inserted credential fixture.

The suite inserts persistent DB state in beforeAll but never deletes it. This can leak state across reruns and make the suite flaky.

Suggested cleanup
-import { afterEach, beforeAll, describe, expect, inject, it, vi } from "vitest";
+import { eq } from "drizzle-orm";
+import { afterAll, afterEach, beforeAll, describe, expect, inject, it, vi } from "vitest";
@@
   beforeAll(async () => {
     await database.insert(credentials).values([
       {
         id: "bridge-test",
         publicKey: new Uint8Array(hexToBytes(owner)),
         account,
         factory,
         pandaId: "bridgePandaId",
         bridgeId: "bridgeCustomerId",
       },
     ]);
   });
+
+  afterAll(async () => {
+    await database.delete(credentials).where(eq(credentials.id, "bridge-test"));
+  });

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1f375179-b8c8-43dc-9548-2da431b5b274

📥 Commits

Reviewing files that changed from the base of the PR and between 468bb15 and d67d97d.

📒 Files selected for processing (9)
  • .changeset/thin-pumas-brush.md
  • server/hooks/bridge.ts
  • server/hooks/manteca.ts
  • server/index.ts
  • server/test/hooks/bridge.test.ts
  • server/test/hooks/manteca.test.ts
  • server/utils/ramps/bridge.ts
  • server/utils/segment.ts
  • server/vitest.config.mts

coderabbitai[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

@mainqueg mainqueg force-pushed the bridge-hook branch 2 times, most recently from 333ef5d to c8ea3f5 Compare March 5, 2026 17:23
@cruzdanilo cruzdanilo marked this pull request as ready for review March 5, 2026 19:45
Comment on lines +193 to +195
const digest = createHash("sha256").update(`${timestamp}.${body}`).digest();
const verifier = createVerify("RSA-SHA256");
verifier.update(digest);
Copy link

Choose a reason for hiding this comment

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

Bug: The Bridge webhook handler uses RSA-SHA256 for signature verification, but the Bridge API requires HMAC-SHA256. This will cause all legitimate webhooks to fail verification.
Severity: CRITICAL

Suggested Fix

Replace the RSA-SHA256 verification logic with HMAC-SHA256. Use crypto.createHmac("sha256", webhookSecret) to generate a signature from the request body and timestamp. Compare this generated signature against the one provided in the bridge-signature header. Update the environment variable from BRIDGE_WEBHOOK_PUBLIC_KEY to a shared secret provided by Bridge.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: server/hooks/bridge.ts#L193-L195

Potential issue: The Bridge webhook signature verification is incorrectly implemented
using `RSA-SHA256` with a public key, as seen with `createVerify("RSA-SHA256")`.
According to Bridge's API documentation, webhooks must be verified using `HMAC-SHA256`
with a shared secret. This mismatch will cause the verification to fail for all incoming
webhooks from Bridge, resulting in a `401 Unauthorized` response. Consequently, critical
functionalities like customer activation, onramp transaction tracking, and deposit
notifications will be completely broken. The implementation appears to be an incorrect
adaptation from a Stripe webhook handler.

Did we get this right? 👍 / 👎 to inform future reviews.

Copy link

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 2 potential issues.

View 4 additional findings in Devin Review.

Open in Devin Review

if (!match) return c.json({ code: "unauthorized" }, 401);
const [, timestamp, base64Signature] = match;
if (!timestamp || !base64Signature) return c.json({ code: "unauthorized" }, 401);
if (Math.abs(Date.now() - Number(timestamp)) > 600_000) return c.json({ code: "unauthorized" }, 401);

Choose a reason for hiding this comment

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

🚩 Timestamp validation assumes millisecond precision

The timestamp comparison at line 191 uses Math.abs(Date.now() - Number(timestamp)) > 600_000, treating the bridge timestamp as milliseconds (matching Date.now()). The test at server/test/hooks/bridge.test.ts:83 confirms this assumption by using Date.now() - 600_001 as an expired timestamp. If Bridge's webhook actually sends timestamps in Unix seconds (not milliseconds), the 10-minute window (600_000 ms) would be compared against a value ~1000x larger, causing every request to be rejected. This depends on Bridge's actual API behavior which should be verified against their documentation.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 16b4784071

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +100 to +107
const bridgeId =
payload.event_type === "customer.updated.status_transitioned"
? payload.event_object.id
: payload.event_object.customer_id;
const credential = await database.query.credentials.findFirst({
columns: { account: true, source: true },
where: eq(credentials.bridgeId, bridgeId),
});

Choose a reason for hiding this comment

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

P2 Badge Skip credential lookup for ignored bridge events

The handler fetches credentials before checking subtype/state-specific no-op paths, so events you intentionally ignore (for example virtual_account.activity.created with type !== "payment_submitted" or drain transitions not in payment_submitted) still hit the database and can emit credential not found errors for unknown customer_ids. In production webhook traffic where Bridge sends many non-actionable transitions, this creates avoidable DB load and noisy Sentry errors even though the final behavior is just 200 { code: "ok" }.

Useful? React with 👍 / 👎.

devin-ai-integration[bot]

This comment was marked as resolved.

Copy link

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View 4 additional findings in Devin Review.

Open in Devin Review

| {
event: "Onramp";
properties: {
amount: number;

Choose a reason for hiding this comment

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

🚩 fiatAmount → amount rename is complete and consistent

The property rename from fiatAmount to amount in the Segment Onramp event type (server/utils/segment.ts:45) is consistently applied in the only two callers: server/hooks/manteca.ts:204 and server/hooks/bridge.ts:143,164. The test assertions in server/test/hooks/manteca.test.ts:349 and server/test/hooks/bridge.test.ts:149,290 also use the new amount name. This is a breaking change for any downstream Segment consumers (dashboards, analytics pipelines) that reference the old fiatAmount property name.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

server: on ramp bridge

1 participant