AgentSkillsCN

Bridge Webhooks

通过Bridge.xyz API实现Webhook集成。支持事件处理、签名验证(RSA-SHA256)、防重放攻击,以及安全可靠的事件处理流程。适用场景包括:接收各类事件通知、验证签名、处理转账、客户信息与卡片相关的Webhook事件。

SKILL.md
--- frontmatter
name: Bridge Webhooks
description: Webhook integration with Bridge.xyz API. Event handling, signature verification (RSA-SHA256), replay attack prevention, and secure event processing. Use for: receiving events, verifying signatures, handling transfers, customers, and cards webhooks.

Bridge Webhooks

Quick Reference

typescript
const WEBHOOK_TIMEOUT_MS = 600000; // 10 minutes
type WebhookCategory = 'customers' | 'transfers' | 'wallets' | 'cards' | 'virtual_accounts';

Webhook Event Structure

typescript
interface WebhookEvent {
  api_version: string;
  event_id: string;
  event_category: WebhookCategory;
  event_type: string;
  event_object: Record<string, unknown>;
  event_object_changes?: Record<string, unknown>;
  event_created_at: string;
}

Event Types by Category

typescript
const EVENT_TYPES = {
  customers: [
    'customer.created',
    'customer.updated',
    'customer.kyc_completed',
    'customer.tos_accepted',
  ],
  transfers: [
    'transfer.created',
    'transfer.state_updated',
    'transfer.completed',
    'transfer.failed',
  ],
  wallets: [
    'wallet.created',
    'wallet.tagged',
  ],
  cards: [
    'card_account.created',
    'card_account.status_updated',
    'card_account.freeze_created',
    'card_account.freeze_removed',
  ],
  virtual_accounts: [
    'virtual_account.created',
    'virtual_account.deposit_received',
    'virtual_account.conversion_completed',
  ],
} as const;

Signature Verification (Python Reference)

python
import hashlib
import base64
import time
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding

def verify_webhook_signature(
  payload: bytes,
  signature_header: str,
  public_key_pem: str
) -> tuple[bool, str]:
  try:
    # Parse signature header
    signature_parts = signature_header.split(',')
    timestamp = next(
      (p.split('=', 1)[1] for p in signature_parts if p.startswith('t=')),
      None
    )
    signature = next(
      (p.split('=', 1)[1] for p in signature_parts if p.startswith('v0=')),
      None
    )
    
    if not timestamp or not signature:
      return False, 'Missing timestamp or signature'
    
    # Check timestamp (reject > 10 minutes)
    current_time = int(time.time() * 1000)
    if current_time - int(timestamp) > WEBHOOK_TIMEOUT_MS:
      return False, 'Timestamp too old'
    
    # Create signed payload: timestamp.payload
    signed_payload = f"{timestamp}.{payload.decode()}"
    digester = hashlib.sha256(signed_payload.encode())
    
    # Verify signature
    public_key = serialization.load_pem_public_key(public_key_pem.encode())
    signature_bytes = base64.b64decode(signature)
    
    public_key.verify(
      signature_bytes,
      digester.digest(),
      padding.PKCS1v15(),
      hashes.SHA256()
    )
    
    return True, ''
  except Exception as e:
    return False, f'Signature verification failed: {e}'

Signature Verification (TypeScript)

typescript
import crypto from 'crypto';

interface SignatureVerificationResult {
  isValid: boolean;
  error?: string;
}

const WEBHOOK_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
your_webhook_public_key_here
-----END PUBLIC KEY-----`;

function verifyWebhookSignature(
  payload: Buffer,
  signatureHeader: string,
  publicKey: string = WEBHOOK_PUBLIC_KEY
): SignatureVerificationResult {
  try {
    const signatureParts = signatureHeader.split(',');
    const timestamp = signatureParts.find(p => p.startsWith('t='))?.split('=')[1];
    const signature = signatureParts.find(p => p.startsWith('v0='))?.split('=')[1];
    
    if (!timestamp || !signature) {
      return { isValid: false, error: 'Missing timestamp or signature' };
    }
    
    // Check timestamp (reject > 10 minutes)
    const currentTime = Date.now();
    if (currentTime - parseInt(timestamp) > 600000) {
      return { isValid: false, error: 'Timestamp too old' };
    }
    
    // Create signed payload
    const signedPayload = `${timestamp}.${payload.toString()}`;
    const digest = crypto.createHash('sha256').update(signedPayload).digest();
    
    // Verify signature
    const verifier = crypto.createVerify('RSA-SHA256');
    verifier.update(digest);
    const isValid = verifier.verify(publicKey, signature, 'base64');
    
    return { isValid };
  } catch (error) {
    return { isValid: false, error: `Verification failed: ${error}` };
  }
}

Webhook Handler (Express.js)

typescript
import express, { Request, Response } from 'express';
import crypto from 'crypto';

const app = express();

app.use('/webhooks/bridge', express.raw({ type: 'application/json' }));

function handleWebhookEvent(event: WebhookEvent): void {
  switch (event.event_type) {
    case 'customer.created':
      console.log(`New customer: ${event.event_object.id}`);
      break;
    case 'customer.updated':
      console.log(`Customer updated: ${event.event_object.id}`);
      break;
    case 'transfer.created':
      console.log(`Transfer initiated: ${event.event_object.id}`);
      break;
    case 'transfer.completed':
      console.log(`Transfer completed: ${event.event_object.id}`);
      break;
    case 'transfer.failed':
      console.log(`Transfer failed: ${event.event_object.id}`);
      break;
    case 'wallet.created':
      console.log(`Wallet created: ${event.event_object.id}`);
      break;
    case 'card_account.created':
      console.log(`Card provisioned: ${event.event_object.id}`);
      break;
    case 'card_account.freeze_created':
      console.log(`Card frozen: ${event.event_object.id}`);
      break;
    default:
      console.log(`Unhandled event: ${event.event_type}`);
  }
}

app.post('/webhooks/bridge', (req: Request, res: Response) => {
  const payload = req.body as Buffer;
  const signatureHeader = req.headers['x-webhook-signature'] as string;
  
  if (!signatureHeader) {
    res.status(400).json({ error: 'Missing signature header' });
    return;
  }
  
  const verification = verifyWebhookSignature(payload, signatureHeader);
  
  if (!verification.isValid) {
    console.error('Signature verification failed:', verification.error);
    res.status(400).json({ error: 'Invalid signature' });
    return;
  }
  
  try {
    const event: WebhookEvent = JSON.parse(payload.toString());
    handleWebhookEvent(event);
    res.status(200).json({ received: true });
  } catch (error) {
    console.error('Failed to parse webhook event:', error);
    res.status(400).json({ error: 'Invalid JSON' });
  }
});

app.listen(3000, () => console.log('Webhook server running on port 3000'));

Webhook Handler (Python Flask)

python
from flask import Flask, request, jsonify
import json
import hashlib
import base64
import time
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.exceptions import InvalidSignature

app = Flask(__name__)

WEBHOOK_PUBLIC_KEY = """-----BEGIN PUBLIC KEY-----
your_webhook_public_key_here
-----END PUBLIC KEY-----"""

@app.route('/webhooks/bridge', methods=['POST'])
def handle_webhook():
    payload = request.get_data()
    signature_header = request.headers.get('X-Webhook-Signature')
    
    if not signature_header:
        return jsonify({'error': 'Missing signature header'}), 400
    
    is_valid, error = verify_webhook_signature(payload, signature_header, WEBHOOK_PUBLIC_KEY)
    
    if not is_valid:
        return jsonify({'error': error}), 400
    
    try:
        event_data = json.loads(payload)
        
        event_type = event_data.get('event_type')
        
        if event_type == 'customer.created':
            print(f"New customer: {event_data['event_object']['id']}")
        elif event_type == 'transfer.completed':
            print(f"Transfer completed: {event_data['event_object']['id']}")
        elif event_type == 'card_account.freeze_created':
            print(f"Card frozen: {event_data['event_object']['id']}")
        else:
            print(f"Unhandled event: {event_type}")
        
        return jsonify({'received': True})
    except Exception as e:
        return jsonify({'error': str(e)}), 400

if __name__ == '__main__':
    app.run(port=3000, debug=True)

Webhook Handler (Ruby)

ruby
require 'json'
require 'openssl'
require 'base64'

class WebhookHandler
  WEBHOOK_PUBLIC_KEY = <<~KEY
    -----BEGIN PUBLIC KEY-----
    your_webhook_public_key_here
    -----END PUBLIC KEY-----
  KEY
  
  def self.verify_webhook_signature(payload, signature_header)
    signature_parts = signature_header.split(',')
    timestamp = signature_parts.find { |p| p.start_with?('t=') }&.split('=', 2)&.last
    signature = signature_parts.find { |p| p.start_with?('v0=') }&.split('=', 2)&.last
    
    return [false, 'Missing timestamp or signature'] unless timestamp && signature
    
    # Check timestamp (reject > 10 minutes)
    current_time = Time.now.to_i * 1000
    return [false, 'Timestamp too old'] if current_time - timestamp.to_i > 600_000
    
    # Verify signature
    signed_payload = "#{timestamp}.#{payload}"
    public_key = OpenSSL::PKey::RSA.new(WEBHOOK_PUBLIC_KEY)
    signature_bytes = Base64.decode64(signature)
    is_valid = public_key.verify(OpenSSL::Digest::SHA256.new, signature_bytes, signed_payload)
    
    [is_valid, is_valid ? nil : 'Invalid signature']
  end
  
  def self.handle_event(event)
    case event['event_type']
    when 'customer.created'
      puts "New customer: #{event['event_object']['id']}"
    when 'transfer.completed'
      puts "Transfer completed: #{event['event_object']['id']}"
    else
      puts "Unhandled event: #{event['event_type']}"
    end
  end
end

post '/webhooks/bridge' do
  payload = request.body.read
  signature_header = env['HTTP_X_WEBHOOK_SIGNATURE']
  
  unless signature_header
    status 400
    return { error: 'Missing signature header' }.to_json
  end
  
  is_valid, error = WebhookHandler.verify_webhook_signature(payload, signature_header)
  
  unless is_valid
    status 400
    return { error: error }.to_json
  end
  
  event = JSON.parse(payload)
  WebhookHandler.handle_event(event)
  
  status 200
  { received: true }.to_json
end

Webhook Event Handler (Java)

java
import org.springframework.web.bind.annotation.*;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.security.*;
import java.util.Base64;

@RestController
public class BridgeWebhookHandler {
    
    private static final String WEBHOOK_PUBLIC_KEY = """
        -----BEGIN PUBLIC KEY-----
        your_webhook_public_key_here
        -----END PUBLIC KEY-----
        """;
    
    @PostMapping("/webhooks/bridge")
    public ResponseEntity<Map<String, Object>> handleWebhook(
            @RequestHeader("X-Webhook-Signature") String signatureHeader,
            @RequestBody String payload) {
        
        try {
            // Parse signature header
            String[] parts = signatureHeader.split(",");
            String timestamp = "";
            String signature = "";
            
            for (String part : parts) {
                if (part.startsWith("t=")) {
                    timestamp = part.substring(2);
                } else if (part.startsWith("v0=")) {
                    signature = part.substring(3);
                }
            }
            
            // Check timestamp
            long currentTime = System.currentTimeMillis();
            if (currentTime - Long.parseLong(timestamp) > 600000) {
                return ResponseEntity.badRequest()
                    .body(Map.of("error", "Timestamp too old"));
            }
            
            // Verify signature
            String signedPayload = timestamp + "." + payload;
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            byte[] hash = digest.digest(signedPayload.getBytes());
            
            PublicKey publicKey = KeyFactory.getInstance("RSA")
                .generatePublic(new X509EncodedKeySpec(
                    Base64.getDecoder().decode(WEBHOOK_PUBLIC_KEY)));
            
            Signature sig = Signature.getInstance("SHA256withRSA");
            sig.initVerify(publicKey);
            sig.update(hash);
            
            if (!sig.verify(Base64.getDecoder().decode(signature))) {
                return ResponseEntity.badRequest()
                    .body(Map.of("error", "Invalid signature"));
            }
            
            // Handle event
            ObjectMapper mapper = new ObjectMapper();
            Map<String, Object> event = mapper.readValue(payload, Map.class);
            
            String eventType = (String) event.get("event_type");
            
            switch (eventType) {
                case "customer.created":
                    System.out.println("New customer: " + event.get("id"));
                    break;
                case "transfer.completed":
                    System.out.println("Transfer completed: " + event.get("id"));
                    break;
            }
            
            return ResponseEntity.ok(Map.of("received", true));
            
        } catch (Exception e) {
            return ResponseEntity.badRequest()
                .body(Map.of("error", e.getMessage()));
        }
    }
}

Managing Webhooks

typescript
// List configured webhooks
async function listWebhooks(): Promise<{
  data: {
    id: string;
    url: string;
    status: 'active' | 'inactive';
    public_key: string;
  }[];
}> {
  const response = await fetch(`${BRIDGE_API_URL}/webhooks`, {
    headers: { 'Api-Key': API_KEY },
  });

  if (!response.ok) {
    throw new Error('Failed to list webhooks');
  }

  return response.json();
}

// Create webhook
async function createWebhook(url: string): Promise<{
  id: string;
  url: string;
  status: string;
  public_key: string;
}> {
  const response = await fetch(`${BRIDGE_API_URL}/webhooks`, {
    method: 'POST',
    headers: {
      'Api-Key': API_KEY,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ url }),
  });

  if (!response.ok) {
    throw new Error('Failed to create webhook');
  }

  return response.json();
}

Best Practices

  1. Always verify signatures - Never trust webhook payloads without verification
  2. Reject old events - Implement replay attack prevention (10 minute timeout)
  3. Use idempotent handlers - Handle duplicate events gracefully
  4. Respond quickly - Return 200 within seconds
  5. Process asynchronously - Queue events for background processing
  6. Log all events - Keep audit trail for debugging