Skip to content
Open
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
38 changes: 38 additions & 0 deletions components/eventdock/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# EventDock

[EventDock](https://eventdock.app) is a webhook reliability layer. It receives
your incoming webhooks at a stable ingest URL, **retries** failed deliveries with
exponential backoff (up to 7 attempts over several hours), **de-duplicates**
repeats, parks permanent failures in a **Dead Letter Queue**, and forwards
successful events to a destination you configure.

These Pipedream components let you put EventDock in front of your workflows so a
brief outage, deploy, or error never means a **lost** webhook.

> **Free tier: 5,000 events/month.** [Sign up at eventdock.app](https://eventdock.app).

## Components

| Component | Type | What it does |
|---|---|---|
| **New Reliable Webhook Event (Instant)** | source | Creates an EventDock endpoint pointing at this source's Pipedream URL, then emits one event per reliable delivery. Point your provider at the EventDock ingest URL. |
| **Reliable HTTP Forward** | action | Sends a payload to a destination URL *through* EventDock so the delivery is buffered, retried, and DLQ'd on permanent failure. |

## Authentication

Both components use an **EventDock API key** (starts with `evdk_`), created in
the EventDock dashboard under **Settings → API Keys**. Pipedream sends it as a
Bearer token. The default API base URL is `https://api.eventdock.app`; override
with the `EVENTDOCK_BASE_URL` environment variable only for self-hosted/staging.

## How the source works

```
Provider ──▶ EventDock ingest URL ──▶ (buffer · retry · de-dupe · DLQ) ──▶ Pipedream source URL ──▶ your workflow
```

On deploy the source calls `POST /v1/endpoints` with `upstream_url` set to its
own `$.interface.http` endpoint, and logs the **ingest URL** to configure in your
provider. On teardown it soft-deletes the endpoint (`DELETE /v1/endpoints/:id`).
The EventDock event id is used as the **dedupe key**, so a retried delivery of
the same event never double-fires your workflow.
37 changes: 37 additions & 0 deletions components/eventdock/actions/reliable-http-forward/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Reliable HTTP Forward

## Overview

Sends a payload to a destination URL **through [EventDock](https://eventdock.app)**
so the delivery is buffered, **retried** with exponential backoff, and parked in a
**Dead Letter Queue** if the destination stays down. Use it anywhere you'd
normally do an HTTP POST but can't afford to silently lose the request.

On the first run for a given destination URL it creates a reusable EventDock
endpoint; subsequent runs reuse it.

## Example use cases

- Forward processed data to a **flaky internal API** with automatic retries.
- Fan results out to a partner webhook that occasionally returns 5xx.
- Replace a bare `$.send.http(...)` with a delivery you can **replay** from the
EventDock dashboard if it permanently fails.

## Getting started

1. Add this action to a workflow step and connect your **EventDock** account
([free tier](https://eventdock.app) = 5,000 events/month).
2. Set the **Destination URL** (where the payload should ultimately land) and the
**Payload** (the JSON body to deliver).
3. Run it. The action ensures an EventDock endpoint exists for that destination,
POSTs the payload to EventDock's ingest URL, and returns the **endpoint id**
and **event id**. EventDock then handles reliable delivery to your
destination.

## Troubleshooting

- **Endpoint limit reached**: the free tier allows 3 endpoints; this action
creates one per distinct destination URL. Reuse destinations or upgrade.
- **Delivery not arriving at the destination?** Check the event in the EventDock
dashboard — if it failed all retries it's in the DLQ and can be replayed.
- **Invalid upstream URL**: the destination must be a valid absolute URL.
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import eventdock from "../../eventdock.app.mjs";
import { axios } from "@pipedream/platform";
import {
isPrivateIpLiteral,
normalizeHostLiteral,
} from "../../common/ssrf.mjs";

export default {
key: "eventdock-reliable-http-forward",
name: "Reliable HTTP Forward",
description:
"Send a payload to a destination URL *through EventDock* so the delivery is buffered, retried with exponential backoff, and parked in a DLQ if the destination stays down. Returns the EventDock event id. On the first run for a given destination this creates a reusable EventDock endpoint. [See the docs](https://eventdock.app/docs)",
version: "0.0.1",
type: "action",
annotations: {
destructiveHint: false,
openWorldHint: true,
readOnlyHint: false,
},
props: {
eventdock,
destinationUrl: {
type: "string",
label: "Destination URL",
description:
"The final URL EventDock should deliver the payload to (with retries). For example your app's API or another webhook receiver. Example: `https://api.example.com/webhook`",
},
payload: {
type: "object",
label: "Payload",
description:
"The JSON body to forward. EventDock stores it, then attempts delivery to the destination URL, retrying on failure. Example: `{ \"event\": \"user.created\", \"id\": 123 }`",
},
endpointName: {
type: "string",
label: "Endpoint Name",
description:
"Optional name for the EventDock endpoint created for this destination. Defaults to the destination host.",
optional: true,
},
debug: {
type: "boolean",
label: "Debug",
description:
"Include the EventDock ingest URL in this step's output. Off by default — the ingest URL is a capability URL (anyone with it can post events to your endpoint), so it's withheld from step exports unless you explicitly need it.",
optional: true,
default: false,
},
},
methods: {
/**
* Validate the destination is a well-formed http(s) URL before we do any
* work. This is a client-side check for fast feedback — the EventDock BACKEND
* is the authoritative SSRF guard: it re-resolves DNS and re-validates the
* upstream_url, blocking private/internal/link-local ranges (the same probe
* logic the platform applies to every webhook destination) regardless of what
* we send. We do NOT resolve DNS here (it can change between this check and
* delivery — that's the backend's job), but we DO reject the full set of
* private/reserved IP *literals* (IPv4 + IPv6) via the ported isPrivateIpLiteral
* so an obvious 127.0.0.1 / 10.x / 169.254.169.254 / ::1 / fc00:: target is
* caught immediately rather than slipping past a partial check.
*/
validateDestinationUrl(destinationUrl) {
let parsed;
try {
parsed = new URL(destinationUrl);
} catch {
throw new Error(
`Destination URL is not a valid URL: ${destinationUrl}`,
);
}
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
throw new Error(
`Destination URL must use http:// or https:// (got "${parsed.protocol}").`,
);
}
const host = normalizeHostLiteral(parsed.hostname);
// Reject internal-looking hostnames regardless of DNS.
const internalName =
host === "localhost" ||
host === "ip6-localhost" ||
host === "ip6-loopback" ||
host.endsWith(".localhost") ||
host.endsWith(".internal") ||
host.endsWith(".local") ||
host.endsWith(".intranet") ||
host.endsWith(".corp") ||
host.endsWith(".home") ||
host.endsWith(".lan");
// Reject the full set of private/reserved IP literals (IPv4 + IPv6).
if (internalName || isPrivateIpLiteral(host)) {
throw new Error(
`Destination URL "${host}" looks internal/non-routable. EventDock only delivers to public destinations.`,
);
}
return parsed;
},
/**
* Find an existing, active EventDock endpoint whose upstream_url matches the
* destination so repeated runs reuse one endpoint instead of creating many.
* This action always creates `generic` endpoints, so we also require the
* provider to be `generic` — that way we never reuse a provider-specific
* endpoint (e.g. a stripe endpoint created by the source for signature
* verification) that happens to share the same upstream URL, which would
* apply the wrong verification settings to a plain forward.
*/
async findEndpointForDestination($, destinationUrl) {
const { endpoints = [] } = await this.eventdock.listEndpoints({ $ });
return endpoints.find(
(ep) =>
ep.upstream_url === destinationUrl &&
ep.status !== "deleted" &&
(ep.provider || "generic") === "generic",
);
},
async ensureEndpoint($, destinationUrl) {
const existing = await this.findEndpointForDestination($, destinationUrl);
if (existing) {
return existing;
}
const host = this.validateDestinationUrl(destinationUrl).host;
return this.eventdock.createEndpoint({
$,
name: this.endpointName || `Pipedream forward → ${host}`,
upstreamUrl: destinationUrl,
provider: "generic",
});
},
},
async run({ $ }) {
// Client-side sanity check (fast feedback). EventDock's backend is the
// authoritative SSRF guard and re-validates the destination on its side.
this.validateDestinationUrl(this.destinationUrl);

const endpoint = await this.ensureEndpoint($, this.destinationUrl);

// POST the payload to EventDock's ingest URL. EventDock accepts (202),
// stores it, then handles the reliable delivery to destinationUrl for us.
const ingestResponse = await axios($, {
method: "POST",
url: endpoint.ingest_url,
headers: { "Content-Type": "application/json" },
data: this.payload,
});

$.export(
"$summary",
`Queued reliable delivery to ${this.destinationUrl} (event ${ingestResponse?.event_id ?? "accepted"})`,
);

const result = {
endpoint_id: endpoint.id,
...ingestResponse,
};
// The ingest URL is a capability URL — only surface it when explicitly
// requested via the Debug flag, so it doesn't leak into step exports/logs.
if (this.debug) {
result.ingest_url = endpoint.ingest_url;
}
return result;
},
};
Loading
Loading