Skip to content
Draft
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
5 changes: 5 additions & 0 deletions .changeset/sendblue-native-channel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eve": patch
---

Add a first-class Sendblue channel with managed Vercel Connect and portable credential setup.
45 changes: 7 additions & 38 deletions apps/docs/lib/integrations/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -661,48 +661,17 @@ export default channel;
See the [Velt adapter documentation](https://chat-sdk.dev/adapters/vendor-official/velt) for supported events, capabilities, and credentials.`,
configure: `Create a Velt bot user and webhook, set \`VELT_API_KEY\` and \`VELT_WEBHOOK_SECRET\`, then send comment events to \`/eve/v1/velt\`. The adapter maps documents to channels, annotations to threads, and comments to messages. See the [Chat SDK channel docs](/docs/channels/chat-sdk) for eve session dispatch, state, streaming, and human-in-the-loop behavior.`,
},
"chat-sdk-sendblue": {
sendblue: {
logo: "sendblue",
docsHref: "/docs/channels/chat-sdk",
badge: "Provider official",
keywords: ["chat sdk", "sendblue", "imessage", "sms", "rcs", "tapbacks", "phone"],
install: `Add this Chat SDK channel from eve's registry. This writes \`agent/channels/sendblue.ts\` and installs Chat SDK and its adapter dependencies:
docsHref: "/docs/channels/sendblue",
keywords: ["sendblue", "imessage", "sms", "rcs", "tapbacks", "phone"],
install: `Add Sendblue from eve's registry, then choose Vercel Connect or portable credentials:

\`\`\`bash
eve add channel/chat-sdk-sendblue
eve add channel/sendblue
\`\`\``,
quickStart: `Create \`agent/channels/sendblue.ts\`:

\`\`\`ts
// agent/channels/sendblue.ts
import { createSendblueAdapter } from "chat-adapter-sendblue";
import { createMemoryState } from "@chat-adapter/state-memory";
import type { Message, Thread } from "chat";
import { chatSdkChannel } from "eve/channels/chat-sdk";

export const { bot, channel, send } = chatSdkChannel({
userName: "My Agent",
adapters: {
sendblue: createSendblueAdapter(),
},
state: createMemoryState(),
streaming: false,
});

bot.onNewMention(async (thread: Thread, message: Message) => {
await thread.subscribe();
await send(message.text, { thread });
});

bot.onSubscribedMessage(async (thread: Thread, message: Message) => {
await send(message.text, { thread });
});

export default channel;
\`\`\`

See the [Sendblue adapter documentation](https://chat-sdk.dev/adapters/vendor-official/sendblue) for supported events, capabilities, and credentials.`,
configure: `Set \`SENDBLUE_API_KEY\`, \`SENDBLUE_API_SECRET\`, and \`SENDBLUE_FROM_NUMBER\`, then point Sendblue webhooks at \`/eve/v1/sendblue\`. The adapter also supports tapbacks, typing indicators, delivery callbacks, and number lookup. See the [Chat SDK channel docs](/docs/channels/chat-sdk) for eve session dispatch, state, streaming, and human-in-the-loop behavior.`,
quickStart: `The guided setup writes \`agent/channels/sendblue.ts\`. With Vercel Connect, it provisions a managed Sendblue account and phone number, routes verified webhooks to \`/eve/v1/sendblue\`, and resolves short-lived credentials at runtime.`,
configure: `For a host without Vercel Connect, choose **Use portable credentials**. After deploying, configure Sendblue webhooks for \`/eve/v1/sendblue\` and set \`SENDBLUE_API_KEY\`, \`SENDBLUE_API_SECRET\`, \`SENDBLUE_FROM_NUMBER\`, and \`SENDBLUE_WEBHOOK_SECRET\` as encrypted environment variables.`,
},
"chat-sdk-novu": {
logo: "novu",
Expand Down
29 changes: 12 additions & 17 deletions apps/docs/registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -1800,27 +1800,22 @@
}
},
{
"name": "channel/chat-sdk-sendblue",
"name": "channel/sendblue",
"type": "registry:item",
"title": "Sendblue",
"description": "Send and receive iMessage, SMS, and RCS through Sendblue.",
"dependencies": ["chat", "chat-adapter-sendblue", "@chat-adapter/state-memory"],
"envVars": {
"SENDBLUE_API_KEY": "",
"SENDBLUE_API_SECRET": "",
"SENDBLUE_FROM_NUMBER": ""
},
"files": [
{
"path": "registry/channels/chat-sdk-sendblue.ts",
"type": "registry:file",
"target": "agent/channels/sendblue.ts"
}
],
"description": "Connect an eve agent to iMessage, SMS, and RCS through Sendblue with guided setup.",
"dependencies": ["@vercel/connect@>=0.8.0"],
"meta": {
"eve": {
"docs": "/docs/channels/chat-sdk",
"implementation": "chat-sdk"
"setup": {
"command": "eve",
"package": "eve",
"bin": "eve",
"args": ["integration", "setup", "sendblue"]
},
"requires": ">=0.31.4",
"docs": "/docs/channels/sendblue",
"implementation": "native"
}
}
},
Expand Down
25 changes: 1 addition & 24 deletions apps/docs/registry/channels/chat-sdk-sendblue.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1 @@
import { createSendblueAdapter } from "chat-adapter-sendblue";
import { createMemoryState } from "@chat-adapter/state-memory";
import type { Message, Thread } from "chat";
import { chatSdkChannel } from "eve/channels/chat-sdk";

export const { bot, channel, send } = chatSdkChannel({
userName: "My Agent",
adapters: {
sendblue: createSendblueAdapter(),
},
state: createMemoryState(),
streaming: false,
});

bot.onNewMention(async (thread: Thread, message: Message) => {
await thread.subscribe();
await send(message.text, { thread });
});

bot.onSubscribedMessage(async (thread: Thread, message: Message) => {
await send(message.text, { thread });
});

export default channel;
// Sendblue is scaffolded by `eve integration setup sendblue`.
9 changes: 5 additions & 4 deletions apps/docs/scripts/validate-channel-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const registrySlugsByCatalogSlug: Readonly<Record<string, string>> = {
eve: "web",
linq: "linq",
photon: "photon-imessage",
sendblue: "sendblue",
};

const setupKindsByCatalogSlug: Readonly<Record<string, string>> = {
Expand All @@ -48,6 +49,7 @@ const setupKindsByCatalogSlug: Readonly<Record<string, string>> = {
eve: "web",
linq: "linq",
photon: "photon",
sendblue: "sendblue",
};

const adapterDependenciesByCatalogSlug: Readonly<Record<string, string>> = {
Expand All @@ -57,7 +59,6 @@ const adapterDependenciesByCatalogSlug: Readonly<Record<string, string>> = {
"chat-sdk-messenger": "@chat-adapter/messenger",
"chat-sdk-zernio": "@zernio/chat-sdk-adapter",
"chat-sdk-velt": "@veltdev/chat-sdk-adapter",
"chat-sdk-sendblue": "chat-adapter-sendblue",
"chat-sdk-novu": "@novu/chat-sdk-adapter",
"chat-sdk-liveblocks": "@liveblocks/chat-sdk-adapter",
"chat-sdk-kapso": "@kapso/chat-adapter",
Expand All @@ -76,7 +77,6 @@ const targetSlugsByCatalogSlug: Readonly<Record<string, string>> = {
"chat-sdk-messenger": "messenger",
"chat-sdk-zernio": "zernio",
"chat-sdk-velt": "velt",
"chat-sdk-sendblue": "sendblue",
"chat-sdk-novu": "novu",
"chat-sdk-liveblocks": "liveblocks",
"chat-sdk-kapso": "kapso",
Expand All @@ -87,7 +87,7 @@ const targetSlugsByCatalogSlug: Readonly<Record<string, string>> = {
"chat-sdk-resend": "resend",
};

const nonStreamingCatalogSlugs = new Set(["chat-sdk-sendblue"]);
const nonStreamingCatalogSlugs = new Set<string>();

const docsRoot = join(import.meta.dirname, "..");
const registry = JSON.parse(await readFile(join(docsRoot, "registry.json"), "utf8")) as Registry;
Expand Down Expand Up @@ -152,7 +152,8 @@ for (const [index, item] of items.entries()) {
entry.slug === "linear-agent" ||
entry.slug === "eve" ||
entry.slug === "linq" ||
entry.slug === "photon"
entry.slug === "photon" ||
entry.slug === "sendblue"
) {
const expectedArgs = [
"integration",
Expand Down
1 change: 1 addition & 0 deletions docs/channels/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"slack",
"linq",
"photon",
"sendblue",
"discord",
"teams",
"telegram",
Expand Down
3 changes: 2 additions & 1 deletion docs/channels/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ agent/
intake.ts
```

Install a channel from the registry with `eve add channel/<name>`, such as `eve add channel/slack` or `eve add channel/web`. You can also author the file by hand.
Install a channel from the registry with `eve add channel/<name>`, such as `eve add channel/sendblue`, `eve add channel/slack`, or `eve add channel/web`. You can also author the file by hand.

## The eve HTTP channel (default)

Expand Down Expand Up @@ -75,6 +75,7 @@ eve's first-class channels use eve-owned runtimes for webhook handling, verifica
| Slack mentions, DMs, buttons | [Slack](./slack) |
| iMessage and SMS | [Linq](./linq) |
| iMessage | [Photon](./photon) |
| iMessage, SMS, or RCS | [Sendblue](./sendblue) |
| Discord slash commands, components | [Discord](./discord) |
| Microsoft Teams messages + Adaptive Cards | [Teams](./teams) |
| Telegram bot messages | [Telegram](./telegram) |
Expand Down
59 changes: 59 additions & 0 deletions docs/channels/sendblue.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
---
title: Sendblue
description: Connect an eve agent to iMessage, SMS, and RCS through Sendblue.
---

Use Sendblue to receive and reply to iMessage, SMS, and RCS conversations. Run
`eve add channel/sendblue` to add `agent/channels/sendblue.ts` through the guided
setup.

Choose **Set up Vercel Connect** to provision a managed Sendblue account and
phone number. Connect stores the permanent Sendblue credentials, configures its
verified trigger at `/eve/v1/sendblue`, and issues short-lived credentials to
the deployed agent:

```ts title="agent/channels/sendblue.ts"
import { connectSendblueCredentials } from "@vercel/connect/eve";
import { sendblueChannel } from "eve/channels/sendblue";

export default sendblueChannel({
credentials: connectSendblueCredentials("sendblue/my-agent"),
});
```

The channel resolves a fresh Connect bearer token and its sole provisioned
Sendblue line. That line limits inbound events, so unrelated account lines do
not reach the agent. For a connector with multiple lines, pass `fromNumber` to
`connectSendblueCredentials(...)` to select one. When no direct webhook secret
is configured, the channel verifies Connect-forwarded webhooks with Vercel OIDC.

## Other hosts

Choose **Use portable credentials** during `eve add channel/sendblue` when the
agent is not deployed through Vercel Connect. eve writes the API key, secret,
and sending number to `.env.local`, then scaffolds a channel that reads them
from the environment:

```ts title="agent/channels/sendblue.ts"
import { sendblueChannel } from "eve/channels/sendblue";

export default sendblueChannel({
credentials: {
apiKey: process.env.SENDBLUE_API_KEY!,
apiSecret: process.env.SENDBLUE_API_SECRET!,
fromNumber: process.env.SENDBLUE_FROM_NUMBER!,
webhookSecret: process.env.SENDBLUE_WEBHOOK_SECRET,
},
});
```

After deploying the agent:

1. Configure Sendblue webhooks for `https://<your-host>/eve/v1/sendblue`.
2. Configure a signing secret for the webhook in Sendblue.
3. Set `SENDBLUE_API_KEY`, `SENDBLUE_API_SECRET`, `SENDBLUE_FROM_NUMBER`, and
`SENDBLUE_WEBHOOK_SECRET` as encrypted environment variables on the host.

For direct Sendblue webhooks, `SENDBLUE_WEBHOOK_SECRET` authenticates the
`sb-signing-secret` header. Use a `webhookVerifier` only when a trusted proxy
verifies inbound Sendblue events instead.
4 changes: 2 additions & 2 deletions packages/eve-catalog/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,10 +193,10 @@ export const INTEGRATIONS: readonly IntegrationEntry[] = [
surfaces: { scaffoldable: false, registry: true, gallery: true },
},
{
slug: "chat-sdk-sendblue",
slug: "sendblue",
name: "Sendblue",
kind: "channel",
tagline: "Send and receive iMessage, SMS, and RCS through Sendblue.",
tagline: "Send and receive iMessage, SMS, and RCS through Sendblue, with guided setup.",
surfaces: { scaffoldable: false, registry: true, gallery: true },
},
{
Expand Down
6 changes: 6 additions & 0 deletions packages/eve/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,11 @@
"import": "./dist/src/public/channels/linq/index.js",
"default": "./dist/src/public/channels/linq/index.js"
},
"./channels/sendblue": {
"types": "./dist/src/public/channels/sendblue/index.d.ts",
"import": "./dist/src/public/channels/sendblue/index.js",
"default": "./dist/src/public/channels/sendblue/index.js"
},
"./channels/github": {
"types": "./dist/src/public/channels/github/index.d.ts",
"import": "./dist/src/public/channels/github/index.js",
Expand Down Expand Up @@ -370,6 +375,7 @@
"ai": "catalog:",
"autoevals": "0.0.132",
"chat": "4.34.0",
"chat-adapter-sendblue": "0.2.0",
"chokidar": "5.0.0",
"commander": "14.0.3",
"emulate": "0.6.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { createDeclarationCopier } from "./_shared.mjs";

export default {
packageName: "chat-adapter-sendblue",
compiledPath: "chat-adapter-sendblue",
copyDeclarations: createDeclarationCopier({
rewrites: { chat: { kind: "vendored", compiledPath: "chat" } },
}),
};
2 changes: 2 additions & 0 deletions packages/eve/scripts/vendor-compiled/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import provider from "./@ai-sdk/provider.mjs";
import providerUtils from "./@ai-sdk/provider-utils.mjs";

import chatAdapterSlack from "./@chat-adapter/slack.mjs";
import chatAdapterSendblue from "./chat-adapter-sendblue.mjs";
import chatAdapterStateMemory from "./@chat-adapter/state-memory.mjs";
import chatAdapterTwilio from "./@chat-adapter/twilio.mjs";
import photonChatAdapterIMessage from "./@photon-ai/chat-adapter-imessage.mjs";
Expand Down Expand Up @@ -57,6 +58,7 @@ export const MODULES = [
anthropic,
chat,
chatAdapterSlack,
chatAdapterSendblue,
chatAdapterStateMemory,
chatAdapterTwilio,
chokidar,
Expand Down
24 changes: 24 additions & 0 deletions packages/eve/src/public/channels/sendblue/inboundContent.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { describe, expect, it } from "vitest";

import { Message } from "#compiled/chat/index.js";
import { sendblueInboundContent } from "#public/channels/sendblue/inboundContent.js";

function message(text: string): Message {
return new Message({
author: { isBot: false, isMe: false, userId: "user", userName: "user" },
id: "message-id",
raw: {},
text,
threadId: "thread-id",
});
}

describe("sendblueInboundContent", () => {
it("returns plain text", () => {
expect(sendblueInboundContent(message("hello"))).toBe("hello");
});

it("drops blank messages", () => {
expect(sendblueInboundContent(message(" \n"))).toBeUndefined();
});
});
11 changes: 11 additions & 0 deletions packages/eve/src/public/channels/sendblue/inboundContent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { UserContent } from "ai";

import type { Message } from "#compiled/chat/index.js";
import { messageToUserContent } from "#public/channels/chat-sdk/index.js";

/** Returns model-visible Sendblue content, or `undefined` for blank messages. */
export function sendblueInboundContent(message: Message): string | UserContent | undefined {
const content = messageToUserContent(message);
if (typeof content === "string") return content.trim().length > 0 ? content : undefined;
return content.length > 0 ? content : undefined;
}
7 changes: 7 additions & 0 deletions packages/eve/src/public/channels/sendblue/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export {
sendblueChannel,
type SendblueChannel,
type SendblueChannelConfig,
type SendblueChannelCredentials,
type SendblueChannelCredentialsProvider,
} from "#public/channels/sendblue/sendblueChannel.js";
Loading
Loading