@hybrd/channels 2.0.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.
@@ -0,0 +1,167 @@
1
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs"
2
+ import { join } from "node:path"
3
+ import { afterEach, beforeEach, describe, expect, it } from "vitest"
4
+
5
+ const TEST_DIR = join("/tmp", "hybrid-xmtp-acl-test", Date.now().toString())
6
+
7
+ beforeEach(() => {
8
+ if (existsSync(TEST_DIR)) {
9
+ rmSync(TEST_DIR, { recursive: true })
10
+ }
11
+ mkdirSync(TEST_DIR, { recursive: true })
12
+ })
13
+
14
+ afterEach(() => {
15
+ if (existsSync(TEST_DIR)) {
16
+ rmSync(TEST_DIR, { recursive: true })
17
+ }
18
+ })
19
+
20
+ describe("ACL Filtering Logic", () => {
21
+ describe("allowlist reading", () => {
22
+ it("returns empty array when file doesn't exist", async () => {
23
+ const { readACLAllowFrom } = await import("@hybrid/memory")
24
+ const allowFrom = await readACLAllowFrom(TEST_DIR)
25
+ expect(allowFrom).toEqual([])
26
+ })
27
+
28
+ it("returns allowlist from file", async () => {
29
+ const credentialsDir = join(TEST_DIR, "credentials")
30
+ mkdirSync(credentialsDir, { recursive: true })
31
+
32
+ writeFileSync(
33
+ join(credentialsDir, "xmtp-allowFrom.json"),
34
+ JSON.stringify({
35
+ version: 1,
36
+ allowFrom: ["0xallowed1", "0xallowed2"]
37
+ })
38
+ )
39
+
40
+ const { readACLAllowFrom } = await import("@hybrid/memory")
41
+ const allowFrom = await readACLAllowFrom(TEST_DIR)
42
+ expect(allowFrom).toEqual(["0xallowed1", "0xallowed2"])
43
+ })
44
+ })
45
+
46
+ describe("allowlist checking", () => {
47
+ it("allows all when allowlist is empty", async () => {
48
+ const credentialsDir = join(TEST_DIR, "credentials")
49
+ mkdirSync(credentialsDir, { recursive: true })
50
+
51
+ writeFileSync(
52
+ join(credentialsDir, "xmtp-allowFrom.json"),
53
+ JSON.stringify({ version: 1, allowFrom: [] })
54
+ )
55
+
56
+ const { readACLAllowFrom } = await import("@hybrid/memory")
57
+ const allowFrom = await readACLAllowFrom(TEST_DIR)
58
+
59
+ // Empty allowlist = open to all
60
+ expect(allowFrom.length).toBe(0)
61
+ })
62
+
63
+ it("allows addresses on the list", async () => {
64
+ const credentialsDir = join(TEST_DIR, "credentials")
65
+ mkdirSync(credentialsDir, { recursive: true })
66
+
67
+ writeFileSync(
68
+ join(credentialsDir, "xmtp-allowFrom.json"),
69
+ JSON.stringify({
70
+ version: 1,
71
+ allowFrom: ["0xabc", "0xdef"]
72
+ })
73
+ )
74
+
75
+ const { readACLAllowFrom } = await import("@hybrid/memory")
76
+ const allowFrom = await readACLAllowFrom(TEST_DIR)
77
+
78
+ // Check membership
79
+ expect(allowFrom.includes("0xabc")).toBe(true)
80
+ expect(allowFrom.includes("0xdef")).toBe(true)
81
+ expect(allowFrom.includes("0xxyz")).toBe(false)
82
+ })
83
+
84
+ it("normalizes addresses to lowercase", async () => {
85
+ const credentialsDir = join(TEST_DIR, "credentials")
86
+ mkdirSync(credentialsDir, { recursive: true })
87
+
88
+ writeFileSync(
89
+ join(credentialsDir, "xmtp-allowFrom.json"),
90
+ JSON.stringify({
91
+ version: 1,
92
+ allowFrom: ["0xABC123"]
93
+ })
94
+ )
95
+
96
+ const { readACLAllowFrom } = await import("@hybrid/memory")
97
+ const allowFrom = await readACLAllowFrom(TEST_DIR)
98
+
99
+ // Should be normalized to lowercase
100
+ expect(allowFrom).toContain("0xabc123")
101
+ expect(allowFrom).not.toContain("0xABC123")
102
+ })
103
+ })
104
+ })
105
+
106
+ describe("XMTP Adapter Integration", () => {
107
+ it("blocks message from non-allowed sender", async () => {
108
+ const credentialsDir = join(TEST_DIR, "credentials")
109
+ mkdirSync(credentialsDir, { recursive: true })
110
+
111
+ writeFileSync(
112
+ join(credentialsDir, "xmtp-allowFrom.json"),
113
+ JSON.stringify({
114
+ version: 1,
115
+ allowFrom: ["0xallowed"]
116
+ })
117
+ )
118
+
119
+ const { readACLAllowFrom } = await import("@hybrid/memory")
120
+ const allowFrom = await readACLAllowFrom(TEST_DIR)
121
+
122
+ // Simulate the check the adapter would do
123
+ const senderAddress = "0xblocked"
124
+ const isAllowed =
125
+ allowFrom.length === 0 || allowFrom.includes(senderAddress.toLowerCase())
126
+
127
+ expect(isAllowed).toBe(false)
128
+ })
129
+
130
+ it("allows message from allowed sender", async () => {
131
+ const credentialsDir = join(TEST_DIR, "credentials")
132
+ mkdirSync(credentialsDir, { recursive: true })
133
+
134
+ writeFileSync(
135
+ join(credentialsDir, "xmtp-allowFrom.json"),
136
+ JSON.stringify({
137
+ version: 1,
138
+ allowFrom: ["0xallowed"]
139
+ })
140
+ )
141
+
142
+ const { readACLAllowFrom } = await import("@hybrid/memory")
143
+ const allowFrom = await readACLAllowFrom(TEST_DIR)
144
+
145
+ // Simulate the check the adapter would do
146
+ const senderAddress = "0xallowed"
147
+ const isAllowed =
148
+ allowFrom.length === 0 || allowFrom.includes(senderAddress.toLowerCase())
149
+
150
+ expect(isAllowed).toBe(true)
151
+ })
152
+
153
+ it("allows all messages when no allowlist configured", async () => {
154
+ const { readACLAllowFrom } = await import("@hybrid/memory")
155
+ const allowFrom = await readACLAllowFrom(TEST_DIR)
156
+
157
+ // No allowlist file = open
158
+ expect(allowFrom.length).toBe(0)
159
+
160
+ // Any sender should be allowed
161
+ const senderAddress = "0xanyone"
162
+ const isAllowed =
163
+ allowFrom.length === 0 || allowFrom.includes(senderAddress.toLowerCase())
164
+
165
+ expect(isAllowed).toBe(true)
166
+ })
167
+ })
@@ -0,0 +1,307 @@
1
+ import { randomUUID } from "node:crypto"
2
+ import type { Server } from "node:http"
3
+ import type {
4
+ ChannelAdapter,
5
+ ChannelId,
6
+ TriggerRequest,
7
+ TriggerResponse
8
+ } from "@hybrd/types"
9
+ import { readACLAllowFrom } from "@hybrid/memory"
10
+ import { Agent } from "@xmtp/agent-sdk"
11
+ import type { Conversation } from "@xmtp/node-sdk"
12
+ import express from "express"
13
+ import pc from "picocolors"
14
+
15
+ const log = {
16
+ info: (msg: string) => console.log(`${pc.magenta("[xmtp]")} ${msg}`),
17
+ error: (msg: string) => console.error(`${pc.red("[xmtp]")} ${msg}`),
18
+ warn: (msg: string) => console.log(`${pc.yellow("[xmtp]")} ${msg}`),
19
+ success: (msg: string) => console.log(`${pc.green("[xmtp]")} ${msg}`)
20
+ }
21
+
22
+ export interface XMTPAdapterConfig {
23
+ port: number
24
+ agentUrl: string
25
+ xmtpEnv?: "dev" | "production"
26
+ walletKey: `0x${string}`
27
+ dbEncryptionKey: Uint8Array
28
+ dbPath: string
29
+ workspaceDir?: string
30
+ }
31
+
32
+ interface TextMessage {
33
+ id: string
34
+ content: string
35
+ senderInboxId: string
36
+ }
37
+
38
+ export class XMTPAdapter implements ChannelAdapter {
39
+ readonly channel: ChannelId = "xmtp"
40
+ readonly port: number
41
+
42
+ private config: XMTPAdapterConfig
43
+ private agent: Agent | null = null
44
+ private server: Server | null = null
45
+ private app: express.Application
46
+ private botInboxId: string | null = null
47
+ private processedMessages: Set<string> = new Set()
48
+ private addressCache: Map<string, string> = new Map()
49
+
50
+ constructor(config: XMTPAdapterConfig) {
51
+ this.port = config.port
52
+ this.config = config
53
+ this.app = express()
54
+ this.app.use(express.json())
55
+ }
56
+
57
+ async start(): Promise<void> {
58
+ await this.startXMTPClient()
59
+ this.startTriggerServer()
60
+ }
61
+
62
+ async stop(): Promise<void> {
63
+ this.server?.close()
64
+ }
65
+
66
+ private async startXMTPClient(): Promise<void> {
67
+ const { createUser } = await import("@xmtp/agent-sdk")
68
+ const { toBytes } = await import("viem")
69
+
70
+ const user = createUser(this.config.walletKey)
71
+ const identifier = {
72
+ identifier: user.account.address.toLowerCase(),
73
+ identifierKind: 0
74
+ }
75
+
76
+ const signer = {
77
+ type: "EOA" as const,
78
+ getIdentifier: () => identifier,
79
+ getChainId: async () => BigInt(1),
80
+ signMessage: async (message: string) => {
81
+ const sig = await user.account.signMessage({ message })
82
+ return toBytes(sig)
83
+ }
84
+ }
85
+
86
+ this.agent = await Agent.create(
87
+ signer as unknown as Parameters<typeof Agent.create>[0],
88
+ {
89
+ env: this.config.xmtpEnv ?? "dev",
90
+ dbEncryptionKey: this.config.dbEncryptionKey,
91
+ dbPath: this.config.dbPath
92
+ }
93
+ )
94
+
95
+ log.success("connected to XMTP network")
96
+
97
+ this.botInboxId = this.agent.client.inboxId
98
+
99
+ this.agent.on("text", async (ctx) => {
100
+ const { conversation, message } = ctx
101
+ await this.handleInbound(conversation, message as TextMessage)
102
+ })
103
+
104
+ await this.agent.start()
105
+ log.success("listening for XMTP messages")
106
+ }
107
+
108
+ private startTriggerServer(): void {
109
+ this.app.post("/api/trigger", async (req, res) => {
110
+ const result = await this.handleTrigger(req.body)
111
+ res.json(result)
112
+ })
113
+
114
+ this.server = this.app.listen(this.port, "127.0.0.1", () => {
115
+ log.success(`trigger server listening on 127.0.0.1:${this.port}`)
116
+ })
117
+ }
118
+
119
+ private async resolveSenderAddress(
120
+ inboxId: string,
121
+ conversation: Conversation
122
+ ): Promise<string> {
123
+ const cached = this.addressCache.get(inboxId)
124
+ if (cached) return cached
125
+
126
+ try {
127
+ const members = await conversation.members()
128
+ const sender = members.find(
129
+ (m: any) => m.inboxId.toLowerCase() === inboxId.toLowerCase()
130
+ )
131
+
132
+ if (sender) {
133
+ const ethIdentifier = sender.accountIdentifiers.find(
134
+ (id: any) => id.identifierKind === 0
135
+ )
136
+ if (ethIdentifier) {
137
+ const address = ethIdentifier.identifier.toLowerCase()
138
+ this.addressCache.set(inboxId, address)
139
+ return address
140
+ }
141
+ }
142
+
143
+ const inboxState =
144
+ await this.agent?.client.preferences.inboxStateFromInboxIds([inboxId])
145
+ const firstState = inboxState?.[0]
146
+ if (firstState?.identifiers?.[0]?.identifier) {
147
+ const address = firstState.identifiers[0].identifier.toLowerCase()
148
+ this.addressCache.set(inboxId, address)
149
+ return address
150
+ }
151
+ } catch (err) {
152
+ log.warn(`failed to resolve address for ${inboxId.slice(0, 16)}...`)
153
+ }
154
+
155
+ return inboxId
156
+ }
157
+
158
+ private async isSenderAllowed(senderAddress: string): Promise<boolean> {
159
+ if (!this.config.workspaceDir) {
160
+ return true
161
+ }
162
+
163
+ try {
164
+ const allowFrom = await readACLAllowFrom(this.config.workspaceDir)
165
+ if (allowFrom.length === 0) {
166
+ return true
167
+ }
168
+ const normalized = senderAddress.toLowerCase()
169
+ return allowFrom.includes(normalized)
170
+ } catch (err) {
171
+ log.warn(`failed to read ACL, allowing sender: ${(err as Error).message}`)
172
+ return true
173
+ }
174
+ }
175
+
176
+ private async handleInbound(
177
+ conversation: Conversation,
178
+ message: TextMessage
179
+ ): Promise<void> {
180
+ log.info(`message ${pc.gray(message.id.slice(0, 8))}`)
181
+
182
+ if (this.processedMessages.has(message.id)) {
183
+ log.warn(`skipping duplicate: ${message.id.slice(0, 8)}`)
184
+ return
185
+ }
186
+ this.processedMessages.add(message.id)
187
+
188
+ if (this.processedMessages.size > 1000) {
189
+ const arr = Array.from(this.processedMessages)
190
+ arr.slice(0, 500).forEach((id) => this.processedMessages.delete(id))
191
+ }
192
+
193
+ // Check if sender is on the allowlist
194
+ const senderAddress = await this.resolveSenderAddress(
195
+ message.senderInboxId,
196
+ conversation
197
+ )
198
+ const isAllowed = await this.isSenderAllowed(senderAddress)
199
+
200
+ if (!isAllowed) {
201
+ log.warn(
202
+ `blocked message from ${senderAddress.slice(0, 10)}... (not on allowlist)`
203
+ )
204
+ return
205
+ }
206
+
207
+ await this.runAgentAndReply({
208
+ conversationId: conversation.id,
209
+ message: message.content,
210
+ conversation
211
+ })
212
+ }
213
+
214
+ async trigger(req: TriggerRequest): Promise<TriggerResponse> {
215
+ return this.handleTrigger(req)
216
+ }
217
+
218
+ private async handleTrigger(req: TriggerRequest): Promise<TriggerResponse> {
219
+ if (!this.agent) {
220
+ return { delivered: false, error: "XMTP client not initialized" }
221
+ }
222
+
223
+ const conversations = await this.agent.client.conversations.list()
224
+ const conversation = conversations.find(
225
+ (c: Conversation) => c.id === req.to
226
+ )
227
+
228
+ if (!conversation) {
229
+ return { delivered: false, error: "Conversation not found" }
230
+ }
231
+
232
+ return this.runAgentAndReply({
233
+ conversationId: req.to,
234
+ message: req.message,
235
+ conversation
236
+ })
237
+ }
238
+
239
+ private async runAgentAndReply(params: {
240
+ conversationId: string
241
+ message: string
242
+ conversation: Conversation
243
+ }): Promise<TriggerResponse> {
244
+ const { conversation, message, conversationId } = params
245
+
246
+ try {
247
+ const res = await fetch(`${this.config.agentUrl}/api/chat`, {
248
+ method: "POST",
249
+ headers: {
250
+ "Content-Type": "application/json",
251
+ "X-Request-ID": randomUUID(),
252
+ "X-Source": "xmtp-adapter"
253
+ },
254
+ body: JSON.stringify({
255
+ messages: [{ id: randomUUID(), role: "user", content: message }],
256
+ chatId: conversationId
257
+ })
258
+ })
259
+
260
+ if (!res.ok) {
261
+ log.error(`HTTP ${res.status}`)
262
+ return { delivered: false, error: `HTTP ${res.status}` }
263
+ }
264
+
265
+ const reader = res.body?.getReader()
266
+ if (!reader) {
267
+ return { delivered: false, error: "No response body" }
268
+ }
269
+
270
+ const decoder = new TextDecoder()
271
+ let reply = ""
272
+
273
+ while (true) {
274
+ const { done, value } = await reader.read()
275
+ if (done) break
276
+
277
+ for (const line of decoder.decode(value).split("\n")) {
278
+ if (line.startsWith("data: ") && line !== "data: [DONE]") {
279
+ try {
280
+ const p = JSON.parse(line.slice(6))
281
+ if (p.type === "text" && p.content) reply += p.content
282
+ } catch {}
283
+ }
284
+ }
285
+ }
286
+
287
+ if (reply) {
288
+ await conversation.send(reply)
289
+ log.success(`replied (${reply.length} chars)`)
290
+ return { delivered: true }
291
+ }
292
+
293
+ return { delivered: false, error: "No reply generated" }
294
+ } catch (err) {
295
+ log.error((err as Error).message)
296
+ return { delivered: false, error: (err as Error).message }
297
+ }
298
+ }
299
+ }
300
+
301
+ export async function createXMTPAdapter(
302
+ config: XMTPAdapterConfig
303
+ ): Promise<XMTPAdapter> {
304
+ const adapter = new XMTPAdapter(config)
305
+ await adapter.start()
306
+ return adapter
307
+ }
@@ -0,0 +1,138 @@
1
+ import fs from "node:fs"
2
+ import path from "node:path"
3
+ import { resolveAgentSecret } from "@hybrd/xmtp"
4
+ import { readACLAllowFrom } from "@hybrid/memory"
5
+ import { createUser } from "@xmtp/agent-sdk"
6
+ import pc from "picocolors"
7
+ import { type XMTPAdapterConfig } from "./adapter.js"
8
+
9
+ const log = {
10
+ info: (msg: string) => console.log(`${pc.magenta("[xmtp]")} ${msg}`),
11
+ error: (msg: string) => console.error(`${pc.red("[xmtp]")} ${msg}`),
12
+ warn: (msg: string) => console.log(`${pc.yellow("[xmtp]")} ${msg}`),
13
+ success: (msg: string) => console.log(`${pc.green("[xmtp]")} ${msg}`)
14
+ }
15
+
16
+ const AGENT_PORT = process.env.AGENT_PORT || "8454"
17
+ const XMTP_ENV = (process.env.XMTP_ENV || "dev") as "dev" | "production"
18
+ const XMTP_ADAPTER_PORT = Number.parseInt(
19
+ process.env.XMTP_ADAPTER_PORT || "8455",
20
+ 10
21
+ )
22
+
23
+ process.on("uncaughtException", (err) => {
24
+ log.error(`FATAL: ${err.message}`)
25
+ process.exit(1)
26
+ })
27
+
28
+ process.on("unhandledRejection", (reason) => {
29
+ log.error(`FATAL: ${reason}`)
30
+ process.exit(1)
31
+ })
32
+
33
+ function printBanner(walletAddress?: string, aclCount?: number) {
34
+ const isHotReload = process.env.TSX_WATCH === "true"
35
+
36
+ console.log("")
37
+ console.log(
38
+ pc.magenta(" ╭───────────────────────────────────────────────────╮")
39
+ )
40
+ console.log(
41
+ pc.magenta(" │") +
42
+ pc.bold(pc.white(" XMTP Channel Adapter")) +
43
+ pc.magenta(" │")
44
+ )
45
+ console.log(
46
+ pc.magenta(" ╰───────────────────────────────────────────────────╯")
47
+ )
48
+ console.log("")
49
+ console.log(
50
+ ` ${pc.bold("Network")} ${XMTP_ENV === "production" ? pc.green("production") : pc.cyan("dev")}`
51
+ )
52
+ console.log(
53
+ ` ${pc.bold("Wallet")} ${walletAddress ? pc.cyan(walletAddress) : pc.gray("(not configured)")}`
54
+ )
55
+ console.log(` ${pc.bold("Agent")} http://localhost:${AGENT_PORT}`)
56
+ console.log(
57
+ ` ${pc.bold("Trigger")} http://127.0.0.1:${XMTP_ADAPTER_PORT}/api/trigger`
58
+ )
59
+ if (aclCount !== undefined) {
60
+ const aclStatus =
61
+ aclCount > 0
62
+ ? pc.green(`${aclCount} allowed`)
63
+ : pc.yellow("open (no allowlist)")
64
+ console.log(` ${pc.bold("ACL")} ${aclStatus}`)
65
+ }
66
+ console.log("")
67
+
68
+ if (isHotReload) {
69
+ console.log(
70
+ ` ${pc.yellow("⚡")} Hot reload enabled - watching for changes...`
71
+ )
72
+ } else {
73
+ console.log(` ${pc.green("✓")} Listening for messages...`)
74
+ }
75
+ console.log("")
76
+ }
77
+
78
+ async function start() {
79
+ const key = process.env.AGENT_WALLET_KEY
80
+
81
+ if (!key) {
82
+ log.warn("AGENT_WALLET_KEY not set")
83
+ printBanner()
84
+ await new Promise(() => {})
85
+ return
86
+ }
87
+
88
+ const user = createUser(key as `0x${string}`)
89
+
90
+ // Read ACL count for banner
91
+ let aclCount: number | undefined
92
+ try {
93
+ const allowFrom = await readACLAllowFrom(process.cwd())
94
+ aclCount = allowFrom.length
95
+ } catch {
96
+ // ACL doesn't exist yet, that's fine
97
+ aclCount = 0
98
+ }
99
+
100
+ printBanner(user.account.address, aclCount)
101
+
102
+ const secret = resolveAgentSecret(key)
103
+ const dbEncryptionKey = new Uint8Array(Buffer.from(secret, "hex"))
104
+
105
+ const dbDir = path.join(process.cwd(), ".hybrid", ".xmtp")
106
+ if (!fs.existsSync(dbDir)) {
107
+ fs.mkdirSync(dbDir, { recursive: true })
108
+ }
109
+ const dbPath = path.join(
110
+ dbDir,
111
+ `xmtp-${XMTP_ENV}-${user.account.address.toLowerCase().slice(0, 8)}.db3`
112
+ )
113
+
114
+ const config: XMTPAdapterConfig = {
115
+ port: XMTP_ADAPTER_PORT,
116
+ agentUrl: `http://localhost:${AGENT_PORT}`,
117
+ xmtpEnv: XMTP_ENV,
118
+ walletKey: key as `0x${string}`,
119
+ dbEncryptionKey,
120
+ dbPath,
121
+ workspaceDir: process.cwd()
122
+ }
123
+
124
+ await import("./adapter.js").then(({ createXMTPAdapter }) =>
125
+ createXMTPAdapter(config)
126
+ )
127
+ }
128
+
129
+ start().catch((e) => {
130
+ log.error(e.message)
131
+ process.exit(1)
132
+ })
133
+
134
+ export {
135
+ XMTPAdapter,
136
+ type XMTPAdapterConfig,
137
+ createXMTPAdapter
138
+ } from "./adapter.js"
@@ -0,0 +1,51 @@
1
+ import type { ChannelId, TriggerRequest, TriggerResponse } from "@hybrd/types"
2
+
3
+ export const DEFAULT_ADAPTER_PORTS: Record<string, number> = {
4
+ xmtp: 8455
5
+ // telegram: 8456, // future
6
+ // slack: 8457, // future
7
+ }
8
+
9
+ export async function dispatchToChannel(params: {
10
+ channel: ChannelId
11
+ to: string
12
+ message: string
13
+ metadata?: TriggerRequest["metadata"]
14
+ }): Promise<TriggerResponse> {
15
+ const port = DEFAULT_ADAPTER_PORTS[params.channel]
16
+
17
+ if (!port) {
18
+ return { delivered: false, error: `Unknown channel: ${params.channel}` }
19
+ }
20
+
21
+ const url = `http://127.0.0.1:${port}/api/trigger`
22
+
23
+ try {
24
+ const res = await fetch(url, {
25
+ method: "POST",
26
+ headers: { "Content-Type": "application/json" },
27
+ body: JSON.stringify({
28
+ to: params.to,
29
+ message: params.message,
30
+ metadata: params.metadata
31
+ })
32
+ })
33
+
34
+ if (!res.ok) {
35
+ return {
36
+ delivered: false,
37
+ error: `Channel adapter returned ${res.status}: ${res.statusText}`
38
+ }
39
+ }
40
+
41
+ return (await res.json()) as TriggerResponse
42
+ } catch (err) {
43
+ return {
44
+ delivered: false,
45
+ error:
46
+ err instanceof Error
47
+ ? err.message
48
+ : "Failed to connect to channel adapter"
49
+ }
50
+ }
51
+ }
package/src/index.ts ADDED
@@ -0,0 +1,11 @@
1
+ export type {
2
+ ChannelId,
3
+ CronDeliveryMode,
4
+ CronDelivery,
5
+ TriggerRequest,
6
+ TriggerResponse,
7
+ ChannelAdapter,
8
+ ChannelDispatcher
9
+ } from "@hybrd/types"
10
+
11
+ export { dispatchToChannel, DEFAULT_ADAPTER_PORTS } from "./dispatcher.js"