Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -333,11 +333,21 @@ The backend validates all environment variables **at startup**. If a required va

### Webhook signing

- Header: `X-Webhook-Signature`
- Header: `X-StellarStream-Signature`
- Format: `sha256=<hex-digest>`
- Digest input: raw JSON request body string
- Algorithm: HMAC-SHA256 using `WEBHOOK_SIGNING_SECRET`

To verify a delivery, compute `sha256=` + HMAC-SHA256 of the raw request body using your `WEBHOOK_SIGNING_SECRET` and compare it to the `X-StellarStream-Signature` header value using a constant-time comparison.

Example (Node.js):
```js
const { createHmac, timingSafeEqual } = require("crypto");
const expected = "sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex");
const received = req.headers["x-stellarstream-signature"];
const valid = timingSafeEqual(Buffer.from(expected), Buffer.from(received));
```

If `WEBHOOK_DESTINATION_URL` is set without `WEBHOOK_SIGNING_SECRET`, webhooks are delivered unsigned and a warning is logged at startup.

### Startup validation behaviour
Expand Down
8 changes: 0 additions & 8 deletions backend/src/services/webhook.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,3 @@
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { getRetryDelaySeconds, triggerWebhook, getDeadLetters } from "./webhook";
import { initDb, getDb } from "./db";
import fs from "fs";
import path from "path";

const TEST_DB_PATH = path.join(__dirname, "..", "..", "data", "webhook-test.db");


describe("Webhook Retry Logic", () => {
it("should return correct retry delays", () => {
Expand Down
5 changes: 5 additions & 0 deletions backend/src/services/webhook.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { createHmac } from "crypto";
import { getDb } from "./db";

export function computeSignature(secret: string, body: string): string {
return `sha256=${createHmac("sha256", secret).update(body).digest("hex")}`;
}

const MAX_RETRIES = 5;
const RETRY_DELAYS = [5, 15, 60, 300, 900]; // seconds: 5s, 15s, 60s, 300s, 900s

Expand Down
5 changes: 2 additions & 3 deletions backend/src/services/webhookWorker.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import axios from "axios";
import { getDb } from "./db";
import { getRetryDelaySeconds } from "./webhook";
import { getWebhookHeaders } from "./webhookSignature";


let isProcessing = false;
let pollingInterval: NodeJS.Timeout | null = null;
Expand Down Expand Up @@ -44,7 +43,7 @@ export const processWebhookQueue = async () => {
timestamp,
};
const bodyString = JSON.stringify(body);
const headers = getWebhookHeaders(bodyString, process.env.WEBHOOK_SIGNING_SECRET);


await axios.post(url, bodyString, { headers });
success = true;
Expand Down