Skip to content

Commit 3f64ee2

Browse files
committed
feat: add @relayfile/webhook-server package
1 parent c57dd54 commit 3f64ee2

11 files changed

Lines changed: 982 additions & 1 deletion

File tree

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Each adapter has exactly 3 jobs:
1010
## Quick Start
1111

1212
```bash
13-
npm install @relayfile/sdk @relayfile/adapter-github @relayfile/provider-nango
13+
npm install @relayfile/sdk @relayfile/adapter-github @relayfile/provider-nango @relayfile/webhook-server
1414
```
1515

1616
### Getting started
@@ -149,6 +149,7 @@ GitHub/GitLab/Slack/...
149149
| `@relayfile/adapter-slack` | Slack (channels, messages, reactions) |
150150
| `@relayfile/adapter-linear` | Linear (issues, projects, cycles) |
151151
| `@relayfile/adapter-notion` | Notion (pages, databases, blocks, comments) |
152+
| `@relayfile/webhook-server` | Hono webhook receiver for adapter-driven relayfile ingestion |
152153

153154
## Mapping YAML Specification
154155

packages/webhook-server/README.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# @relayfile/webhook-server
2+
3+
Thin [Hono](https://hono.dev) server for receiving provider webhooks, verifying signatures, and persisting normalized events into relayfile via `RelayFileClient`.
4+
5+
## Install
6+
7+
```bash
8+
npm install @relayfile/webhook-server hono
9+
```
10+
11+
## Usage
12+
13+
```ts
14+
import { GitHubAdapter } from "@relayfile/adapter-github";
15+
import { RelayFileClient } from "@relayfile/sdk";
16+
import { createWebhookServer } from "@relayfile/webhook-server";
17+
18+
const client = new RelayFileClient({ token: process.env.RELAYFILE_TOKEN! });
19+
const provider = { name: "nango", proxy: async () => ({ status: 200, headers: {}, data: {} }), healthCheck: async () => true };
20+
const github = new GitHubAdapter(provider);
21+
const server = createWebhookServer({ client, port: 3456, secrets: { github: process.env.GITHUB_WEBHOOK_SECRET } });
22+
server.register("github", github);
23+
await server.start();
24+
```
25+
26+
`POST /github/webhook` now verifies `x-hub-signature-256`, normalizes the event, computes a VFS path from the registered adapter, and calls `client.ingestWebhook(...)`.
27+
28+
## Supported built-ins
29+
30+
- `github`: HMAC-SHA256 verification via `x-hub-signature-256`
31+
- `slack`: signing secret verification via `x-slack-signature` and `x-slack-request-timestamp`
32+
33+
Adapters can also provide their own `verifySignature` or `normalizeWebhook` hooks.
34+
35+
## API
36+
37+
### `createWebhookServer(options)`
38+
39+
- `client`: `RelayFileClient` used to persist webhook events
40+
- `workspaceId`: relayfile workspace ID, defaults to `"default"`
41+
- `port`: default port for `server.start()`, defaults to `3456`
42+
- `hostname`: default hostname for `server.start()`, defaults to `"0.0.0.0"`
43+
- `adapters`: optional initial adapter map
44+
- `secrets`: provider-to-secret map for signature verification
45+
46+
### `server.register(name, adapter)`
47+
48+
Registers an adapter by provider name. The adapter may expose:
49+
50+
- `computePath(objectType, objectId, payload?)`
51+
- `normalizeWebhook(payload, context)`
52+
- `verifySignature(context)`
53+
- `provider?: ConnectionProvider`
54+
55+
### Route
56+
57+
- `POST /:provider/webhook`
58+
59+
The route returns `404` for unknown providers, `401` for signature failures, `400` for invalid payloads, and `200` with the queued relayfile operation IDs when the webhook is accepted.
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"name": "@relayfile/webhook-server",
3+
"version": "0.1.0",
4+
"description": "Thin Hono webhook receiver for relayfile adapters",
5+
"type": "module",
6+
"sideEffects": false,
7+
"main": "dist/index.js",
8+
"types": "dist/index.d.ts",
9+
"exports": {
10+
".": {
11+
"types": "./dist/index.d.ts",
12+
"import": "./dist/index.js"
13+
}
14+
},
15+
"files": [
16+
"dist",
17+
"README.md"
18+
],
19+
"scripts": {
20+
"build": "tsc -p tsconfig.json",
21+
"typecheck": "tsc --noEmit -p tsconfig.json"
22+
},
23+
"peerDependencies": {
24+
"@relayfile/sdk": "^0.1.3"
25+
},
26+
"dependencies": {
27+
"@hono/node-server": "^1.19.4",
28+
"hono": "^4.9.9"
29+
},
30+
"devDependencies": {
31+
"@types/node": "^24.6.0",
32+
"typescript": "^5.9.3"
33+
},
34+
"engines": {
35+
"node": ">=20"
36+
},
37+
"license": "MIT"
38+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
export { AdapterRegistry, createAdapterRegistry } from "./registry.js";
2+
export { createWebhookServer } from "./server.js";
3+
export { headersToRecord, verifyWebhookSignature } from "./verify.js";
4+
export type {
5+
AdapterMap,
6+
AdapterRegistryLike,
7+
PersistedWebhook,
8+
RegisteredWebhookAdapter,
9+
StartedWebhookServer,
10+
WebhookEvent,
11+
WebhookNormalizationContext,
12+
WebhookSecretMap,
13+
WebhookServer,
14+
WebhookServerOptions,
15+
WebhookSignatureVerificationContext,
16+
WebhookStartOptions,
17+
WebhookVerificationFailure,
18+
WebhookVerificationResult,
19+
WebhookVerificationSuccess,
20+
} from "./types.js";
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import type { AdapterMap, AdapterRegistryLike, RegisteredWebhookAdapter } from "./types.js";
2+
3+
function normalizeProviderName(value: string): string {
4+
const normalized = value.trim().toLowerCase();
5+
if (!normalized) {
6+
throw new Error("Provider name must be a non-empty string.");
7+
}
8+
return normalized;
9+
}
10+
11+
export class AdapterRegistry implements AdapterRegistryLike {
12+
private readonly adapters = new Map<string, RegisteredWebhookAdapter>();
13+
14+
constructor(initialAdapters: AdapterMap = {}) {
15+
for (const [name, adapter] of Object.entries(initialAdapters)) {
16+
this.register(name, adapter);
17+
}
18+
}
19+
20+
register(name: string, adapter: RegisteredWebhookAdapter): void {
21+
this.adapters.set(normalizeProviderName(name), adapter);
22+
}
23+
24+
get(name: string): RegisteredWebhookAdapter | undefined {
25+
return this.adapters.get(normalizeProviderName(name));
26+
}
27+
28+
list(): string[] {
29+
return [...this.adapters.keys()].sort();
30+
}
31+
}
32+
33+
export function createAdapterRegistry(initialAdapters: AdapterMap = {}): AdapterRegistry {
34+
return new AdapterRegistry(initialAdapters);
35+
}

0 commit comments

Comments
 (0)