Skip to content

Commit 39af41c

Browse files
khaliqgantclaude
andauthored
feat: add 5 end-to-end provider examples (#1)
* feat: add end-to-end provider examples Five runnable TypeScript examples covering the core provider patterns: - Nango GitHub proxy with optional baseUrl override - Composio proxy with toolkit/action resolution - Provider + Adapter webhook→writeback flow (core pattern) - Multi-provider registry in a single app - Health checks and connection listing Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix composio example field names * fix: address Devin review feedback --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 000010e commit 39af41c

11 files changed

Lines changed: 727 additions & 0 deletions

File tree

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# 01 — Nango GitHub Proxy
2+
3+
Proxy GitHub API requests through Nango with automatic OAuth token injection.
4+
5+
## Key concepts
6+
7+
- **`baseUrl` is optional** — the provider resolves it from the connection's provider-config-key.
8+
- Override `baseUrl` when targeting a non-default API host (e.g. `uploads.github.com`).
9+
- No `RelayFileClient` needed for Nango — just `secretKey`.
10+
11+
## Setup
12+
13+
```bash
14+
export NANGO_SECRET_KEY="your-nango-secret-key"
15+
export NANGO_CONNECTION_ID="conn_github_demo"
16+
```
17+
18+
## Run
19+
20+
```bash
21+
npx tsx examples/01-nango-github-proxy/index.ts
22+
```
23+
24+
## Mock testing
25+
26+
Without real credentials the Nango API will reject requests. To test the
27+
wiring locally, swap in a mock `fetch` via the provider config:
28+
29+
```ts
30+
const nango = new NangoProvider({
31+
secretKey: "mock",
32+
fetch: async () => new Response(JSON.stringify({ ok: true }), { status: 200 }),
33+
});
34+
```
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/**
2+
* Example 01 — Nango GitHub Proxy
3+
*
4+
* Demonstrates NangoProvider proxying GitHub API requests.
5+
* baseUrl is optional: the provider resolves it from the connection
6+
* when omitted, or you can override it for non-default API hosts
7+
* like uploads.github.com.
8+
*/
9+
10+
import { NangoProvider } from "@relayfile/provider-nango";
11+
12+
// ── Config ──────────────────────────────────────────────────────────
13+
// In production, pull from environment variables.
14+
// For local testing you can hardcode or use a .env loader.
15+
const NANGO_SECRET_KEY = process.env.NANGO_SECRET_KEY ?? "nango-mock-secret-key";
16+
const CONNECTION_ID = process.env.NANGO_CONNECTION_ID ?? "conn_github_demo";
17+
18+
async function main() {
19+
// Nango does not require a RelayFileClient — just the secret key.
20+
const nango = new NangoProvider({
21+
secretKey: NANGO_SECRET_KEY,
22+
// baseUrl is optional; defaults to https://api.nango.dev
23+
});
24+
25+
console.log("Provider:", nango.name);
26+
27+
// ── 1. Proxy without baseUrl ──────────────────────────────────────
28+
// The provider resolves the target API host from the connection's
29+
// provider-config-key (e.g. "github" → api.github.com).
30+
console.log("\n--- List pull requests (baseUrl omitted) ---");
31+
const pulls = await nango.proxy({
32+
method: "GET",
33+
endpoint: "/repos/acme/api/pulls",
34+
connectionId: CONNECTION_ID,
35+
query: { state: "open", per_page: "5" },
36+
});
37+
console.log("Status:", pulls.status);
38+
console.log("Data:", JSON.stringify(pulls.data, null, 2));
39+
40+
// ── 2. Proxy WITH baseUrl override ────────────────────────────────
41+
// uploads.github.com is a different host from the default
42+
// api.github.com — pass it explicitly.
43+
console.log("\n--- List release assets (baseUrl override) ---");
44+
const assets = await nango.proxy({
45+
method: "GET",
46+
baseUrl: "https://uploads.github.com",
47+
endpoint: "/repos/acme/api/releases/1/assets",
48+
connectionId: CONNECTION_ID,
49+
});
50+
console.log("Status:", assets.status);
51+
console.log("Data:", JSON.stringify(assets.data, null, 2));
52+
53+
// ── 3. POST example ──────────────────────────────────────────────
54+
console.log("\n--- Create issue comment ---");
55+
const comment = await nango.proxy({
56+
method: "POST",
57+
endpoint: "/repos/acme/api/issues/42/comments",
58+
connectionId: CONNECTION_ID,
59+
body: { body: "Automated comment from relayfile provider" },
60+
});
61+
console.log("Status:", comment.status);
62+
console.log("Data:", JSON.stringify(comment.data, null, 2));
63+
}
64+
65+
main().catch((err) => {
66+
console.error("Error:", err);
67+
process.exit(1);
68+
});
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# 02 — Composio Proxy
2+
3+
Proxy API calls and execute actions through Composio connected accounts.
4+
5+
## Key concepts
6+
7+
- **Toolkit resolution**`lookupAction()` maps a proxy request to the right Composio action/toolkit.
8+
- **`baseUrl` is optional** — resolved from the connected account.
9+
- **Action execution** — call Composio actions directly with `executeAction()`.
10+
- No `RelayFileClient` needed for Composio — just `apiKey`.
11+
12+
## Setup
13+
14+
```bash
15+
export COMPOSIO_API_KEY="your-composio-api-key"
16+
export COMPOSIO_ENTITY_ID="entity_demo"
17+
export COMPOSIO_CONNECTION_ID="conn_composio_demo"
18+
```
19+
20+
## Run
21+
22+
```bash
23+
npx tsx examples/02-composio-proxy/index.ts
24+
```
25+
26+
## Mock testing
27+
28+
Swap in a mock `fetch` to test locally without real credentials:
29+
30+
```ts
31+
const composio = new ComposioProvider({
32+
apiKey: "mock",
33+
fetch: async () => new Response(JSON.stringify({ items: [] }), { status: 200 }),
34+
});
35+
```
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/**
2+
* Example 02 — Composio Proxy
3+
*
4+
* Demonstrates ComposioProvider proxy and toolkit resolution.
5+
* The provider resolves which action/toolkit to invoke from the
6+
* connected account when baseUrl is omitted.
7+
*/
8+
9+
import { ComposioProvider } from "@relayfile/provider-composio";
10+
11+
// ── Config ──────────────────────────────────────────────────────────
12+
const COMPOSIO_API_KEY = process.env.COMPOSIO_API_KEY ?? "composio-mock-api-key";
13+
const ENTITY_ID = process.env.COMPOSIO_ENTITY_ID ?? "entity_demo";
14+
const CONNECTION_ID = process.env.COMPOSIO_CONNECTION_ID ?? "conn_composio_demo";
15+
16+
async function main() {
17+
// Composio does not require a RelayFileClient — just the API key.
18+
const composio = new ComposioProvider({
19+
apiKey: COMPOSIO_API_KEY,
20+
// baseUrl defaults to https://backend.composio.dev/api/v3
21+
});
22+
23+
console.log("Provider:", composio.name);
24+
25+
// ── 1. Proxy through connected account ────────────────────────────
26+
// baseUrl is optional — the provider resolves it from the account.
27+
console.log("\n--- Proxy: list GitHub repos via Composio ---");
28+
const repos = await composio.proxy({
29+
method: "GET",
30+
endpoint: "/user/repos",
31+
connectionId: CONNECTION_ID,
32+
query: { per_page: "5" },
33+
});
34+
console.log("Status:", repos.status);
35+
console.log("Data:", JSON.stringify(repos.data, null, 2));
36+
37+
// ── 2. Toolkit / action resolution ────────────────────────────────
38+
// lookupAction resolves which Composio action maps to a proxy request.
39+
console.log("\n--- Action lookup ---");
40+
const lookup = await composio.lookupAction({
41+
method: "POST",
42+
endpoint: "/repos/acme/api/issues",
43+
connectionId: CONNECTION_ID,
44+
body: { title: "Bug report", body: "Details here" },
45+
});
46+
console.log("Matched by:", lookup.matchedBy);
47+
console.log("Toolkit:", lookup.toolkitSlug ?? "(none)");
48+
console.log("Action:", lookup.toolSlug ?? "(none)");
49+
50+
// ── 3. Execute an action directly ─────────────────────────────────
51+
console.log("\n--- Execute action: GITHUB_CREATE_ISSUE ---");
52+
const result = await composio.executeAction(
53+
"GITHUB_CREATE_ISSUE",
54+
ENTITY_ID,
55+
{ owner: "acme", repo: "api", title: "Filed by relayfile" },
56+
);
57+
console.log("Execution successful:", result.successful);
58+
console.log("Result:", JSON.stringify(result.data, null, 2));
59+
60+
// ── 4. List connected accounts ────────────────────────────────────
61+
console.log("\n--- Connected accounts ---");
62+
const accounts = await composio.listConnectedAccounts();
63+
console.log("Total:", accounts.total);
64+
for (const account of accounts.items) {
65+
console.log(` - ${account.id} (${account.status})`);
66+
}
67+
}
68+
69+
main().catch((err) => {
70+
console.error("Error:", err);
71+
process.exit(1);
72+
});
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# 03 — Provider + Adapter
2+
3+
**This is the most important example.** It demonstrates the core relayfile pattern: providers handle auth while adapters normalize data and write back.
4+
5+
## Flow
6+
7+
```
8+
Webhook (GitHub event)
9+
→ NangoProvider.handleWebhook() — normalizes into NormalizedWebhook
10+
→ GitHubAdapter.mapWebhookToPath() — maps to VFS path
11+
→ GitHubAdapter.writeback() — calls provider.proxy() to post back
12+
→ NangoProvider.proxy() — injects OAuth token, forwards to GitHub
13+
```
14+
15+
## Key concepts
16+
17+
- **Provider** = auth layer. Handles OAuth tokens, proxies API calls.
18+
- **Adapter** = data layer. Normalizes webhooks, maps to VFS paths, writes back through the provider.
19+
- Agents never see the provider or adapter — they just read/write files.
20+
- `baseUrl` is optional on writeback — the provider resolves it.
21+
22+
## Setup
23+
24+
```bash
25+
export NANGO_SECRET_KEY="your-nango-secret-key"
26+
export NANGO_CONNECTION_ID="conn_github_demo"
27+
```
28+
29+
## Run
30+
31+
```bash
32+
npx tsx examples/03-provider-with-adapter/index.ts
33+
```
34+
35+
The example uses mock webhook data so it runs without real GitHub events. The writeback call will fail without valid credentials — this is expected and handled.
36+
37+
## In production
38+
39+
Replace `GitHubAdapterStub` with the real `@relayfile/adapter-github`:
40+
41+
```ts
42+
import { GitHubAdapter } from "@relayfile/adapter-github";
43+
import { NangoProvider } from "@relayfile/provider-nango";
44+
45+
const provider = new NangoProvider({ secretKey: process.env.NANGO_SECRET_KEY! });
46+
const adapter = new GitHubAdapter({ provider });
47+
48+
await adapter.writeback({
49+
provider,
50+
connectionId: "conn_abc",
51+
path: "/github/repos/acme/api/pulls/42/comments",
52+
payload: { body: "Looks good!" },
53+
});
54+
```
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/**
2+
* Example 03 — Provider + Adapter (MOST IMPORTANT)
3+
*
4+
* Shows the core relayfile pattern: a Provider handles auth while an
5+
* Adapter normalizes data and writes back through the provider.
6+
*
7+
* Flow:
8+
* Webhook → NangoProvider.handleWebhook() normalizes the event
9+
* → GitHubAdapter maps it to a VFS path
10+
* → Adapter writes back through NangoProvider.proxy()
11+
*
12+
* This example simulates the full cycle with mock webhook data.
13+
*/
14+
15+
import { NangoProvider } from "@relayfile/provider-nango";
16+
import type { NormalizedWebhook, ProxyResponse } from "@relayfile/provider-nango";
17+
18+
// ── Config ──────────────────────────────────────────────────────────
19+
const NANGO_SECRET_KEY = process.env.NANGO_SECRET_KEY ?? "nango-mock-secret-key";
20+
const CONNECTION_ID = process.env.NANGO_CONNECTION_ID ?? "conn_github_demo";
21+
22+
// ── Mock adapter ────────────────────────────────────────────────────
23+
// In production you would import GitHubAdapter from @relayfile/adapter-github.
24+
// This minimal adapter demonstrates the contract between adapter and provider.
25+
26+
interface AdapterWritebackRequest {
27+
connectionId: string;
28+
path: string;
29+
payload: Record<string, unknown>;
30+
}
31+
32+
class GitHubAdapterStub {
33+
constructor(private provider: NangoProvider) {}
34+
35+
/**
36+
* Convert a normalized webhook into a VFS path.
37+
* Real adapters do richer mapping; this shows the shape.
38+
*/
39+
mapWebhookToPath(webhook: NormalizedWebhook): string {
40+
const { provider, objectType, objectId, eventType } = webhook;
41+
// Example: /github/repos/acme/api/issues/42
42+
return `/${provider}/${objectType}/${objectId}`;
43+
}
44+
45+
/**
46+
* Write back to the external API through the provider.
47+
* The provider handles OAuth — the adapter just describes the request.
48+
*/
49+
async writeback(request: AdapterWritebackRequest): Promise<ProxyResponse> {
50+
console.log(`[Adapter] Writing back to ${request.path}`);
51+
return this.provider.proxy({
52+
method: "POST",
53+
endpoint: request.path,
54+
connectionId: request.connectionId,
55+
body: request.payload,
56+
// baseUrl omitted — provider resolves from connection
57+
});
58+
}
59+
}
60+
61+
// ── Mock webhook payload ────────────────────────────────────────────
62+
// Simulates a Nango forward-webhook for a GitHub pull_request event.
63+
const mockWebhookPayload = {
64+
type: "forward",
65+
connectionId: CONNECTION_ID,
66+
providerConfigKey: "github",
67+
payload: {
68+
action: "opened",
69+
pull_request: {
70+
number: 99,
71+
title: "Add relayfile examples",
72+
html_url: "https://github.com/acme/api/pull/99",
73+
user: { login: "agent-bot" },
74+
},
75+
repository: {
76+
full_name: "acme/api",
77+
},
78+
},
79+
};
80+
81+
async function main() {
82+
const nango = new NangoProvider({
83+
secretKey: NANGO_SECRET_KEY,
84+
});
85+
const adapter = new GitHubAdapterStub(nango);
86+
87+
console.log("Provider:", nango.name);
88+
89+
// ── Step 1: Normalize the incoming webhook ────────────────────────
90+
console.log("\n--- Step 1: Normalize webhook ---");
91+
const webhook = await nango.handleWebhook(mockWebhookPayload);
92+
console.log("Provider:", webhook.provider);
93+
console.log("Event:", webhook.eventType);
94+
console.log("Object:", webhook.objectType, webhook.objectId);
95+
console.log("Connection:", webhook.connectionId);
96+
97+
// ── Step 2: Adapter maps webhook to VFS path ─────────────────────
98+
console.log("\n--- Step 2: Map to VFS path ---");
99+
const vfsPath = adapter.mapWebhookToPath(webhook);
100+
console.log("VFS path:", vfsPath);
101+
102+
// ── Step 3: Adapter writes back through provider ──────────────────
103+
console.log("\n--- Step 3: Writeback through provider ---");
104+
try {
105+
const response = await adapter.writeback({
106+
connectionId: CONNECTION_ID,
107+
path: "/repos/acme/api/pulls/99/comments",
108+
payload: { body: "Thanks for the PR! Reviewing now." },
109+
});
110+
console.log("Writeback status:", response.status);
111+
console.log("Writeback data:", JSON.stringify(response.data, null, 2));
112+
} catch (err) {
113+
// Expected when running without real credentials
114+
console.log("Writeback failed (expected without real credentials):", (err as Error).message);
115+
}
116+
117+
// ── Step 4: Show the full flow ────────────────────────────────────
118+
console.log("\n--- Full flow summary ---");
119+
console.log("1. Webhook received from Nango (GitHub push event)");
120+
console.log("2. Provider normalized it into a NormalizedWebhook");
121+
console.log("3. Adapter mapped it to VFS path:", vfsPath);
122+
console.log("4. Adapter wrote back through provider.proxy()");
123+
console.log("5. Provider injected OAuth token and forwarded to GitHub API");
124+
}
125+
126+
main().catch((err) => {
127+
console.error("Error:", err);
128+
process.exit(1);
129+
});

0 commit comments

Comments
 (0)