@relaymessenger/chat-sdk-adapter 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +121 -0
- package/dist/src/adapter.d.ts +125 -0
- package/dist/src/adapter.js +724 -0
- package/dist/src/chunk.d.ts +25 -0
- package/dist/src/chunk.js +108 -0
- package/dist/src/client.d.ts +112 -0
- package/dist/src/client.js +175 -0
- package/dist/src/format.d.ts +43 -0
- package/dist/src/format.js +266 -0
- package/dist/src/idempotency.d.ts +50 -0
- package/dist/src/idempotency.js +75 -0
- package/dist/src/index.d.ts +14 -0
- package/dist/src/index.js +8 -0
- package/dist/src/reactions.d.ts +12 -0
- package/dist/src/reactions.js +98 -0
- package/dist/src/signature.d.ts +46 -0
- package/dist/src/signature.js +95 -0
- package/dist/src/threadId.d.ts +12 -0
- package/dist/src/threadId.js +28 -0
- package/dist/src/turn.d.ts +38 -0
- package/dist/src/turn.js +15 -0
- package/dist/src/types.d.ts +177 -0
- package/dist/src/types.js +7 -0
- package/package.json +59 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Companion Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# @relaymessenger/chat-sdk-adapter
|
|
2
|
+
|
|
3
|
+
Relay adapter for the [Vercel Chat SDK](https://chat-sdk.dev). Relay is a
|
|
4
|
+
consumer messenger where people talk to agents the way they talk to contacts;
|
|
5
|
+
this package makes a Relay conversation a Chat SDK thread, so anything built on
|
|
6
|
+
the Chat SDK can reach Relay users. Raw HTTPS remains the canonical contract;
|
|
7
|
+
this is a thin, dependency-free binding of it.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { createMemoryState } from "@chat-adapter/state-memory";
|
|
11
|
+
import { createRelayAdapter } from "@relaymessenger/chat-sdk-adapter";
|
|
12
|
+
import { Chat } from "chat";
|
|
13
|
+
|
|
14
|
+
const chat = new Chat({
|
|
15
|
+
userName: "My Agent",
|
|
16
|
+
adapters: {
|
|
17
|
+
relay: createRelayAdapter({
|
|
18
|
+
token: process.env.RELAY_AGENT_TOKEN!,
|
|
19
|
+
webhookSecret: process.env.RELAY_WEBHOOK_SECRET!,
|
|
20
|
+
}),
|
|
21
|
+
},
|
|
22
|
+
state: createMemoryState(),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
chat.onNewMention(async (thread, message) => {
|
|
26
|
+
await thread.subscribe();
|
|
27
|
+
await thread.post({ markdown: `You said: ${message.text}` });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Mount as a POST route.
|
|
31
|
+
export const POST = (request: Request) => chat.webhooks.relay(request);
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`chat` is a peer dependency: install it alongside this package.
|
|
35
|
+
|
|
36
|
+
## With eve
|
|
37
|
+
|
|
38
|
+
eve's Chat SDK channel bridges any adapter to an agent. See
|
|
39
|
+
[`examples/eve`](../../examples/eve) in this repository for the channel file and
|
|
40
|
+
the environment it needs.
|
|
41
|
+
|
|
42
|
+
## What the adapter enforces
|
|
43
|
+
|
|
44
|
+
- Standard Webhooks signature verification over the exact raw body, then
|
|
45
|
+
`event_id` deduplication, because Relay delivers at least once. The event id
|
|
46
|
+
is claimed before the handler runs, so two redeliveries racing each other
|
|
47
|
+
cannot both dispatch, and released again if the handler throws, so Relay's
|
|
48
|
+
retry of a failed turn is still handled. **This window is a bounded set in
|
|
49
|
+
memory, in one process.** A restart, or a second instance behind the same
|
|
50
|
+
webhook URL, has no claim to lose and will dispatch the event again. The
|
|
51
|
+
idempotency key below is what makes that second dispatch harmless.
|
|
52
|
+
- A deterministic `Idempotency-Key` on every `POST /v1/messages`, derived from
|
|
53
|
+
the inbound event id and the send's position in the turn, and from nothing
|
|
54
|
+
else. Relay hashes the request body server side and stores it beside the key,
|
|
55
|
+
so a retry carrying the same body replays the first response and a retry
|
|
56
|
+
carrying a different body is refused with 409 `idempotency_conflict`. Keeping
|
|
57
|
+
the content out of the key is what leaves both of those reachable: a key that
|
|
58
|
+
moved with the body would make every retry a new send, and a handler backed
|
|
59
|
+
by a model rarely writes the same words twice.
|
|
60
|
+
- Group `invocation_id` threading. Relay delivers a group message to an agent
|
|
61
|
+
only when that agent was invoked, and the reply is scoped to that single-use
|
|
62
|
+
invocation, so the first send of a turn carries it and a second raises
|
|
63
|
+
`RelayInvocationSpentError` rather than going out bare and taking a 403.
|
|
64
|
+
- Chunking rather than truncation. Relay caps a text part at 8 KB and a message
|
|
65
|
+
at 32 parts; a longer reply becomes more parts, and then more messages. Each
|
|
66
|
+
text part draws as its own balloon in the app, so a long reply arrives as a
|
|
67
|
+
stack of bubbles rather than one tall one. A split consumes the whitespace it
|
|
68
|
+
lands on, and nothing else.
|
|
69
|
+
|
|
70
|
+
## Formatting
|
|
71
|
+
|
|
72
|
+
Relay does not render Markdown. A text part carries canonical plain text plus
|
|
73
|
+
`styles` runs with UTF-16 offsets, and clients draw the runs. So `{ markdown }`
|
|
74
|
+
and `{ ast }` are flattened to the text a person reads, with emphasis carried
|
|
75
|
+
across as style ranges: `strong` becomes `bold`, `emphasis` becomes `italic`,
|
|
76
|
+
`delete` becomes `strikethrough`, and inline or fenced code becomes `monospace`.
|
|
77
|
+
|
|
78
|
+
Constructs Relay has no style for keep their information in the text rather than
|
|
79
|
+
losing it. A link whose label differs from its target renders as `label (url)`,
|
|
80
|
+
a blockquote keeps its `> ` line prefix, a list keeps its markers, and a table is
|
|
81
|
+
drawn as a monospace ASCII grid. A plain string is sent verbatim with an empty
|
|
82
|
+
`styles` array, which is Relay's marker for structured plain text rather than a
|
|
83
|
+
legacy Markdown body.
|
|
84
|
+
|
|
85
|
+
## Capabilities
|
|
86
|
+
|
|
87
|
+
| Chat SDK operation | Relay |
|
|
88
|
+
| --------------------------------- | ---------------------------------------------------------------- |
|
|
89
|
+
| `postMessage`, `editMessage`, `deleteMessage` | `POST /v1/messages`, `PATCH` and `DELETE /v1/messages/{id}` |
|
|
90
|
+
| `addReaction`, `removeReaction` | `POST /v1/messages/{id}/reactions` |
|
|
91
|
+
| `startTyping` | `POST /v1/conversations/{id}/typing`, ephemeral, 80-char label |
|
|
92
|
+
| `markAsRead` | `POST /v1/conversations/{id}/read` |
|
|
93
|
+
| `fetchMessages`, `fetchThread` | `GET /v1/conversations/{id}/messages` and `/v1/conversations/{id}` |
|
|
94
|
+
| `getUser` | `GET /v1/users/{id}`, scoped to a shared conversation |
|
|
95
|
+
| `stream` | Buffered, then committed as one message |
|
|
96
|
+
|
|
97
|
+
Things Relay does not do, stated rather than faked:
|
|
98
|
+
|
|
99
|
+
- **No streaming bubbles.** A turn commits exactly one canonical message, so
|
|
100
|
+
`stream` buffers the whole reply and posts it once. Nothing partial reaches a
|
|
101
|
+
recipient. With eve's Chat SDK channel, set `streaming: false`.
|
|
102
|
+
- **No forward history cursor.** `GET /v1/conversations/{id}/messages` pages
|
|
103
|
+
backwards with `before_sequence`, so `fetchMessages({ direction: "forward" })`
|
|
104
|
+
throws `NotImplementedError` instead of walking the whole conversation.
|
|
105
|
+
- **No agent-initiated DM.** `POST /v1/conversations/direct` accepts a user
|
|
106
|
+
session, not an Agent Token, so `openDM` is not implemented. A conversation
|
|
107
|
+
starts when a person adds the agent.
|
|
108
|
+
- **No single-message read.** The API has no `GET /v1/messages/{id}`, so
|
|
109
|
+
`fetchMessage` is not implemented and the Chat SDK returns `null`.
|
|
110
|
+
- **No cards.** Relay removed interactive components. A card is delivered as its
|
|
111
|
+
fallback text, so the words arrive but buttons do not render and a
|
|
112
|
+
human-in-the-loop prompt cannot be answered in the app.
|
|
113
|
+
- **No edits carrying media.** Relay requires an edit to stay text-bearing and
|
|
114
|
+
rejects attachment parts.
|
|
115
|
+
|
|
116
|
+
Attachments work in both directions. Inbound media and voice memo parts arrive as
|
|
117
|
+
Chat SDK attachments; outbound attachments with a public HTTPS URL are sent as
|
|
118
|
+
media parts, and outbound bytes are uploaded through `POST /v1/attachments`
|
|
119
|
+
first.
|
|
120
|
+
|
|
121
|
+
Docs: <https://docs.relayapp.im>.
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { Message } from "chat";
|
|
2
|
+
import type { Adapter, AdapterPostableMessage, ChatInstance, EmojiValue, FetchOptions, FetchResult, FormattedContent, RawMessage, StreamChunk, ThreadInfo, UserInfo, WebhookOptions } from "chat";
|
|
3
|
+
import { RelayClient } from "./client.js";
|
|
4
|
+
import type { RelayClientOptions } from "./client.js";
|
|
5
|
+
import type { RelayMessage, RelayRawMessage, RelayThreadId } from "./types.js";
|
|
6
|
+
export declare const RELAY_ADAPTER_NAME = "relay";
|
|
7
|
+
export interface RelayAdapterOptions extends Omit<RelayClientOptions, "token"> {
|
|
8
|
+
/** Agent Token. Defaults to `RELAY_AGENT_TOKEN`. */
|
|
9
|
+
token?: string;
|
|
10
|
+
/** Webhook signing secret. Defaults to `RELAY_WEBHOOK_SECRET`. */
|
|
11
|
+
webhookSecret?: string;
|
|
12
|
+
/** Display name for the agent. Defaults to `Relay Agent`. */
|
|
13
|
+
userName?: string;
|
|
14
|
+
/** This agent's `agt_` id, when the caller knows it. */
|
|
15
|
+
agentId?: string;
|
|
16
|
+
/** Clock tolerance for signature verification, in seconds. */
|
|
17
|
+
toleranceSeconds?: number;
|
|
18
|
+
/** How many handled `event_id` values to remember. */
|
|
19
|
+
dedupeWindow?: number;
|
|
20
|
+
/** Override the client, mainly for tests. */
|
|
21
|
+
client?: RelayClient;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* A group turn tried to commit a second message. Relay's invocation is single
|
|
25
|
+
* use, so there is nothing valid for the second message to cite and the server
|
|
26
|
+
* would answer 403.
|
|
27
|
+
*/
|
|
28
|
+
export declare class RelayInvocationSpentError extends Error {
|
|
29
|
+
constructor(message: string);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Relay adapter for the Vercel Chat SDK.
|
|
33
|
+
*
|
|
34
|
+
* Relay is a consumer messenger where people talk to agents as contacts, so
|
|
35
|
+
* this adapter maps the Chat SDK's thread model onto Relay conversations one
|
|
36
|
+
* to one: a Relay conversation has no enclosing channel, and a thread id is
|
|
37
|
+
* `relay:{conversation_id}`.
|
|
38
|
+
*
|
|
39
|
+
* Two Relay rules shape the surface. A streamed turn commits exactly one
|
|
40
|
+
* canonical message, so `stream` buffers and posts once rather than editing a
|
|
41
|
+
* draft bubble into place. And a group reply is scoped to the single-use
|
|
42
|
+
* invocation that produced the inbound event, so the first send of a turn
|
|
43
|
+
* carries it and a second cannot.
|
|
44
|
+
*/
|
|
45
|
+
export declare class RelayAdapter implements Adapter<RelayThreadId, RelayRawMessage> {
|
|
46
|
+
readonly name = "relay";
|
|
47
|
+
readonly userName: string;
|
|
48
|
+
readonly botUserId?: string;
|
|
49
|
+
readonly lockScope: "thread";
|
|
50
|
+
/** Relay serves history from `GET /v1/conversations/{id}/messages`. */
|
|
51
|
+
readonly persistThreadHistory = false;
|
|
52
|
+
private readonly client;
|
|
53
|
+
private readonly webhookSecret?;
|
|
54
|
+
private readonly toleranceSeconds?;
|
|
55
|
+
private readonly dedupe;
|
|
56
|
+
private chat?;
|
|
57
|
+
constructor(options?: RelayAdapterOptions);
|
|
58
|
+
initialize(chat: ChatInstance): Promise<void>;
|
|
59
|
+
encodeThreadId(platformData: RelayThreadId): string;
|
|
60
|
+
decodeThreadId(threadId: string): RelayThreadId;
|
|
61
|
+
channelIdFromThreadId(threadId: string): string;
|
|
62
|
+
renderFormatted(content: FormattedContent): string;
|
|
63
|
+
parseMessage(raw: RelayRawMessage): Message<RelayRawMessage>;
|
|
64
|
+
postMessage(threadId: string, message: AdapterPostableMessage): Promise<RawMessage<RelayRawMessage>>;
|
|
65
|
+
editMessage(threadId: string, messageId: string, message: AdapterPostableMessage): Promise<RawMessage<RelayRawMessage>>;
|
|
66
|
+
deleteMessage(threadId: string, messageId: string): Promise<void>;
|
|
67
|
+
addReaction(threadId: string, messageId: string, emoji: EmojiValue | string): Promise<void>;
|
|
68
|
+
removeReaction(threadId: string, messageId: string, emoji: EmojiValue | string): Promise<void>;
|
|
69
|
+
/**
|
|
70
|
+
* Relay's typing indicator is ephemeral and carries an optional label of up
|
|
71
|
+
* to 80 characters. The invocation is peeked rather than consumed: typing is
|
|
72
|
+
* not the group reply the invocation is spent on.
|
|
73
|
+
*
|
|
74
|
+
* Failures are swallowed. Group typing without a live pending invocation is a
|
|
75
|
+
* 403 (`Relay-Server/server/src/domain/typing.ts:70-73`), which is exactly
|
|
76
|
+
* what typing after the first send of a group turn looks like. The Chat SDK
|
|
77
|
+
* treats this call as best effort and has no `stopTyping` to strand, so a
|
|
78
|
+
* hint that cannot be shown must never take the reply down with it.
|
|
79
|
+
*/
|
|
80
|
+
startTyping(threadId: string, status?: string): Promise<void>;
|
|
81
|
+
markAsRead(threadId: string, messageId: string): Promise<void>;
|
|
82
|
+
getUser(userId: string): Promise<UserInfo | null>;
|
|
83
|
+
/**
|
|
84
|
+
* Relay pages history backwards with `before_sequence` and returns newest
|
|
85
|
+
* first; the Chat SDK wants each page in chronological order. There is no
|
|
86
|
+
* forward cursor on the route, so `direction: "forward"` has nothing to call.
|
|
87
|
+
*/
|
|
88
|
+
fetchMessages(threadId: string, options?: FetchOptions): Promise<FetchResult<RelayRawMessage>>;
|
|
89
|
+
fetchThread(threadId: string): Promise<ThreadInfo>;
|
|
90
|
+
/**
|
|
91
|
+
* Relay commits one canonical message per turn and has no draft bubble to
|
|
92
|
+
* edit, so the stream is buffered and posted once. Nothing partial ever
|
|
93
|
+
* reaches a recipient, and no cleanup is needed if the stream fails midway.
|
|
94
|
+
*/
|
|
95
|
+
stream(threadId: string, textStream: AsyncIterable<string | StreamChunk>): Promise<RawMessage<RelayRawMessage> | null>;
|
|
96
|
+
/**
|
|
97
|
+
* Verify the Standard Webhooks signature over the exact raw body, refuse a
|
|
98
|
+
* replay by `event_id`, and hand the event to the Chat SDK. Relay redelivers
|
|
99
|
+
* on 5xx, so a dispatch failure must not answer 2xx.
|
|
100
|
+
*/
|
|
101
|
+
handleWebhook(request: Request, options?: WebhookOptions): Promise<Response>;
|
|
102
|
+
private dispatch;
|
|
103
|
+
private buildParts;
|
|
104
|
+
private textParts;
|
|
105
|
+
/**
|
|
106
|
+
* Relay has no card surface: interactive components were removed from the
|
|
107
|
+
* app, so buttons cannot render and a human cannot answer one in Relay. The
|
|
108
|
+
* card's words are still delivered, as text, rather than dropped.
|
|
109
|
+
*/
|
|
110
|
+
private cardToText;
|
|
111
|
+
private attachmentPart;
|
|
112
|
+
private filePart;
|
|
113
|
+
/**
|
|
114
|
+
* Send the parts in one call when they fit, and as follow-up calls when
|
|
115
|
+
* they do not. The server splits each call at ingest into one or more
|
|
116
|
+
* messages, and the one call that carries the invocation owns every message
|
|
117
|
+
* it commits. A group turn cannot overflow, and cannot POST twice: Relay's
|
|
118
|
+
* invocation is single use per call, so a second POST has nothing valid to
|
|
119
|
+
* cite and the server answers 403.
|
|
120
|
+
*/
|
|
121
|
+
private sendParts;
|
|
122
|
+
}
|
|
123
|
+
/** Build a Relay adapter for the Vercel Chat SDK. */
|
|
124
|
+
export declare function createRelayAdapter(options?: RelayAdapterOptions): RelayAdapter;
|
|
125
|
+
export type { RelayMessage };
|