@hybrd/xmtp 1.2.8 → 1.3.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hybrd/xmtp",
3
- "version": "1.2.8",
3
+ "version": "1.3.1",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -17,6 +17,7 @@
17
17
  "@xmtp/content-type-transaction-reference": "2.0.2",
18
18
  "@xmtp/content-type-wallet-send-calls": "2.0.0",
19
19
  "@xmtp/node-sdk": "^4.0.1",
20
+ "hono": "^4.7.4",
20
21
  "jsonwebtoken": "^9.0.2",
21
22
  "uint8arrays": "^5.1.0",
22
23
  "viem": "^2.22.17"
@@ -26,8 +27,8 @@
26
27
  "@types/node": "22.8.6",
27
28
  "tsup": "^8.5.0",
28
29
  "vitest": "^3.2.4",
29
- "@config/tsconfig": "0.0.0",
30
- "@config/biome": "0.0.0"
30
+ "@config/biome": "0.0.0",
31
+ "@config/tsconfig": "0.0.0"
31
32
  },
32
33
  "scripts": {
33
34
  "build": "tsup",
@@ -0,0 +1,306 @@
1
+ import { ContentTypeReaction } from "@xmtp/content-type-reaction"
2
+ import { ContentTypeReply, Reply } from "@xmtp/content-type-reply"
3
+ import { ContentTypeText } from "@xmtp/content-type-text"
4
+ import { ContentTypeWalletSendCalls, WalletSendCallsParams } from "@xmtp/content-type-wallet-send-calls"
5
+ import { Hono } from "hono"
6
+ import { getValidatedPayload, validateXMTPToolsToken } from "./lib/jwt"
7
+ import type { HonoVariables, SendMessageParams, SendReactionParams, SendReplyParams, SendTransactionParams } from "./types"
8
+
9
+ const app = new Hono<{ Variables: HonoVariables }>()
10
+
11
+ app.get("/messages/:messageId", async (c) => {
12
+ const xmtpClient = c.get("xmtpClient")
13
+
14
+ if (!xmtpClient) {
15
+ return c.json({ error: "XMTP client not initialized" }, 500)
16
+ }
17
+
18
+ const token = c.req.query("token")
19
+
20
+ if (!token) {
21
+ return c.json({ error: "Token required" }, 401)
22
+ }
23
+
24
+ const payload = validateXMTPToolsToken(token)
25
+ if (!payload) {
26
+ return c.json({ error: "Invalid or expired token" }, 401)
27
+ }
28
+
29
+ const messageId = c.req.param("messageId")
30
+
31
+ try {
32
+ const message = await xmtpClient.conversations.getMessageById(messageId)
33
+ if (!message) {
34
+ return c.json({ error: "Message not found" }, 404)
35
+ }
36
+
37
+ console.log(`✅ Retrieved message ${messageId}`)
38
+
39
+ const transformedMessage = {
40
+ id: message.id,
41
+ senderInboxId: message.senderInboxId,
42
+ sentAt: message.sentAt.toISOString(),
43
+ content:
44
+ typeof message.content === "object"
45
+ ? JSON.stringify(message.content)
46
+ : message.content,
47
+ contentType: message.contentType?.typeId || "text",
48
+ conversationId: message.conversationId,
49
+ parameters: (message.contentType as any)?.parameters || {}
50
+ }
51
+
52
+ return c.json(transformedMessage)
53
+ } catch (error) {
54
+ console.error("❌ Error fetching message:", error)
55
+ return c.json({ error: "Failed to fetch message" }, 500)
56
+ }
57
+ })
58
+
59
+ // XMTP Tools endpoints
60
+ app.post("/send", async (c) => {
61
+ const xmtpClient = c.get("xmtpClient")
62
+
63
+ if (!xmtpClient) {
64
+ return c.json({ error: "XMTP client not initialized" }, 500)
65
+ }
66
+
67
+ const payload = getValidatedPayload(c)
68
+ if (!payload) {
69
+ return c.json({ error: "Invalid or expired token" }, 401)
70
+ }
71
+
72
+ // Get request body data
73
+ const body = await c.req.json<SendMessageParams>()
74
+
75
+ // Content can come from JWT payload or request body
76
+ const content = body.content || payload.content
77
+ if (!content) {
78
+ return c.json({ error: "Content required for send action" }, 400)
79
+ }
80
+
81
+ const conversationId = payload.conversationId
82
+
83
+ // Conversation ID can come from JWT payload or request body (for API key auth)
84
+ // const conversationId = payload.conversationId || body.conversationId
85
+ if (!conversationId) {
86
+ return c.json({ error: "Conversation ID required" }, 400)
87
+ }
88
+
89
+ try {
90
+ const conversation =
91
+ await xmtpClient.conversations.getConversationById(conversationId)
92
+ if (!conversation) {
93
+ return c.json({ error: "Conversation not found" }, 404)
94
+ }
95
+
96
+ await conversation.send(content)
97
+ console.log(`➡ Sent message to conversation ${conversationId}`)
98
+
99
+ return c.json({
100
+ success: true,
101
+ action: "send",
102
+ conversationId: payload.conversationId
103
+ })
104
+ } catch (error) {
105
+ console.error("❌ Error sending message:", error)
106
+ return c.json({ error: "Failed to send message" }, 500)
107
+ }
108
+ })
109
+
110
+ app.post("/reply", async (c) => {
111
+ const xmtpClient = c.get("xmtpClient")
112
+
113
+ if (!xmtpClient) {
114
+ return c.json({ error: "XMTP client not initialized" }, 500)
115
+ }
116
+
117
+ const payload = getValidatedPayload(c)
118
+ if (!payload) {
119
+ return c.json({ error: "Invalid or expired token" }, 401)
120
+ }
121
+
122
+ // Get request body data
123
+ const body = await c.req.json<SendReplyParams>()
124
+
125
+ // Content can come from JWT payload or request body
126
+ const content = body.content || payload.content
127
+ if (!content) {
128
+ return c.json({ error: "Content required for reply action" }, 400)
129
+ }
130
+
131
+ // Reference message ID can come from JWT payload or request body
132
+ const messageId = body.messageId
133
+
134
+ if (!messageId) {
135
+ return c.json(
136
+ { error: "Reference message ID required for reply action" },
137
+ 400
138
+ )
139
+ }
140
+
141
+ try {
142
+ const conversation = await xmtpClient.conversations.getConversationById(
143
+ payload.conversationId
144
+ )
145
+ if (!conversation) {
146
+ return c.json({ error: "Conversation not found" }, 404)
147
+ }
148
+
149
+ // Create proper XMTP reply structure
150
+ const reply: Reply = {
151
+ reference: messageId,
152
+ contentType: ContentTypeText,
153
+ content: content
154
+ }
155
+
156
+ // Send as a proper threaded reply
157
+ await conversation.send(reply, ContentTypeReply)
158
+ console.log(
159
+ `➡ Sent reply "${content}" to conversation ${payload.conversationId}`
160
+ )
161
+
162
+ return c.json({
163
+ success: true,
164
+ action: "reply",
165
+ conversationId: payload.conversationId
166
+ })
167
+ } catch (error) {
168
+ console.error("❌ Error sending reply:", error)
169
+ return c.json({ error: "Failed to send reply" }, 500)
170
+ }
171
+ })
172
+
173
+ app.post("/react", async (c) => {
174
+ const xmtpClient = c.get("xmtpClient")
175
+
176
+ if (!xmtpClient) {
177
+ return c.json({ error: "XMTP client not initialized" }, 500)
178
+ }
179
+
180
+ const payload = getValidatedPayload(c)
181
+ if (!payload) {
182
+ return c.json({ error: "Invalid or expired token" }, 401)
183
+ }
184
+
185
+ // Get request body data
186
+ const body = await c.req.json<SendReactionParams>()
187
+
188
+ if (!body.emoji) {
189
+ return c.json({ error: "Emoji required for react action" }, 400)
190
+ }
191
+
192
+ try {
193
+ const conversation = await xmtpClient.conversations.getConversationById(
194
+ payload.conversationId
195
+ )
196
+ if (!conversation) {
197
+ return c.json({ error: "Conversation not found" }, 404)
198
+ }
199
+
200
+ const reaction = {
201
+ schema: "unicode",
202
+ reference: body.messageId,
203
+ action: body.action,
204
+ contentType: ContentTypeReaction,
205
+ content: body.emoji
206
+ }
207
+
208
+ // For now, send the reaction content as a simple text message
209
+ // This will send "eyes" as text content to indicate message was seen
210
+ await conversation.send(reaction, ContentTypeReaction)
211
+
212
+ console.log(
213
+ `➡ Sent reaction ${body.emoji} to message ${body.messageId} in conversation ${payload.conversationId}`
214
+ )
215
+
216
+ return c.json({
217
+ success: true,
218
+ action: "react",
219
+ conversationId: payload.conversationId
220
+ })
221
+ } catch (error) {
222
+ console.error("❌ Error sending reaction:", error)
223
+ return c.json({ error: "Failed to send reaction" }, 500)
224
+ }
225
+ })
226
+
227
+ app.post("/transaction", async (c) => {
228
+ const xmtpClient = c.get("xmtpClient")
229
+
230
+ if (!xmtpClient) {
231
+ return c.json({ error: "XMTP client not initialized" }, 500)
232
+ }
233
+
234
+ const payload = getValidatedPayload(c)
235
+ if (!payload) {
236
+ return c.json({ error: "Invalid or expired token" }, 401)
237
+ }
238
+
239
+ // Get request body data for backward compatibility
240
+ let body: any = {}
241
+ try {
242
+ body = await c.req.json<SendTransactionParams>()
243
+ } catch (error) {
244
+ body = {}
245
+ }
246
+
247
+ // Transaction data can come from JWT payload (preferred) or request body (fallback)
248
+ const fromAddress = payload.fromAddress || body.fromAddress
249
+ const chainId = payload.chainId || body.chainId
250
+ const calls = payload.calls || body.calls
251
+
252
+ if (!calls || !fromAddress || !chainId) {
253
+ return c.json(
254
+ { error: "Transaction data required for transaction action" },
255
+ 400
256
+ )
257
+ }
258
+
259
+ // CRITICAL: Detect data truncation that can cause transaction failures
260
+ calls.forEach((call: any, index: number) => {
261
+ if (call.data && typeof call.data === "string") {
262
+ const actualStart = call.data.substring(0, 10)
263
+
264
+ if (actualStart === "0x010f2e2e") {
265
+ console.error("🚨 CRITICAL: Transaction data truncation detected!")
266
+ console.error(" Function selector corrupted - transaction will fail")
267
+ console.error(
268
+ " This indicates a bug in wallet software or XMTP transmission"
269
+ )
270
+ }
271
+ }
272
+ })
273
+
274
+ try {
275
+ const conversation = await xmtpClient.conversations.getConversationById(
276
+ payload.conversationId
277
+ )
278
+ if (!conversation) {
279
+ return c.json({ error: "Conversation not found" }, 404)
280
+ }
281
+
282
+ const params: WalletSendCallsParams = {
283
+ version: "1",
284
+ chainId,
285
+ from: fromAddress,
286
+ calls
287
+ }
288
+
289
+ await conversation.send(params, ContentTypeWalletSendCalls)
290
+
291
+ console.log(
292
+ `✅ Sent transaction request to conversation ${payload.conversationId}`
293
+ )
294
+
295
+ return c.json({
296
+ success: true,
297
+ action: "transaction",
298
+ conversationId: payload.conversationId
299
+ })
300
+ } catch (error) {
301
+ console.error("❌ Error sending transaction:", error)
302
+ return c.json({ error: "Failed to send transaction" }, 500)
303
+ }
304
+ })
305
+
306
+ export default app
package/src/index.ts CHANGED
@@ -15,13 +15,25 @@ export * from "./resolver/xmtp-resolver"
15
15
  export * from "./service-client"
16
16
  export * from "./types"
17
17
 
18
+ // ===================================================================
19
+ // XMTP Plugin for Agent Integration
20
+ // ===================================================================
21
+ export { XMTPPlugin } from "./plugin"
22
+ export type { Plugin, XMTPPluginContext } from "./plugin"
23
+
24
+ // ===================================================================
25
+ // JWT Utilities for XMTP Tools
26
+ // ===================================================================
27
+ export { generateXMTPToolsToken } from "./lib/jwt"
28
+ export type { XMTPToolsPayload } from "./lib/jwt"
29
+
18
30
  // ===================================================================
19
31
  // Enhanced XMTP Client & Connection Management
20
32
  // ===================================================================
21
33
  export {
22
- createXMTPConnectionManager,
23
34
  // Enhanced connection management
24
35
  XMTPConnectionManager,
36
+ createXMTPConnectionManager,
25
37
  type XMTPConnectionConfig,
26
38
  type XMTPConnectionHealth
27
39
  } from "./client"
@@ -30,8 +42,8 @@ export {
30
42
  // XMTP Service Client (for external service communication)
31
43
  // ===================================================================
32
44
  export {
33
- createXmtpServiceClient,
34
- XmtpServiceClient
45
+ XmtpServiceClient,
46
+ createXmtpServiceClient
35
47
  } from "./service-client"
36
48
 
37
49
  // Service Client Types
package/src/lib/jwt.ts ADDED
@@ -0,0 +1,220 @@
1
+ import { Context } from "hono"
2
+ import jwt from "jsonwebtoken"
3
+
4
+ export interface XMTPToolsPayload {
5
+ action: "send" | "reply" | "react" | "transaction" | "blockchain-event"
6
+ conversationId: string
7
+ // Action-specific data
8
+ content?: string
9
+ referenceMessageId?: string
10
+ emoji?: string
11
+ actionType?: "added" | "removed"
12
+ fromAddress?: string
13
+ chainId?: string
14
+ calls?: Array<{
15
+ to: string
16
+ data: string
17
+ metadata?: {
18
+ description: string
19
+ transactionType: string
20
+ }
21
+ }>
22
+ // Metadata
23
+ issued: number
24
+ expires: number
25
+ }
26
+
27
+ /**
28
+ * Validates token and returns payload for both GET and POST endpoints
29
+ *
30
+ * @param {Context} c - Hono context object containing request information
31
+ * @returns {XMTPToolsPayload | null} The validated payload or null if invalid
32
+ *
33
+ * @description
34
+ * Supports two authentication methods:
35
+ * - Authorization header with Bearer token (for POST endpoints)
36
+ * - Query parameter token (for GET endpoints)
37
+ *
38
+ * @example
39
+ * ```typescript
40
+ * app.post("/api/endpoint", async (c) => {
41
+ * const payload = getValidatedPayload(c);
42
+ * if (!payload) {
43
+ * return c.json({ error: "Invalid token" }, 401);
44
+ * }
45
+ * // Use payload data
46
+ * });
47
+ * ```
48
+ */
49
+ export function getValidatedPayload(c: Context): XMTPToolsPayload | null {
50
+ // Try Authorization header first (for POST endpoints)
51
+ const authHeader = c.req.header("Authorization")
52
+ if (authHeader?.startsWith("Bearer ")) {
53
+ const token = authHeader.substring(7) // Remove "Bearer " prefix
54
+ return validateXMTPToolsToken(token)
55
+ }
56
+
57
+ // Fall back to query parameter (for GET endpoints)
58
+ const token = c.req.query("token")
59
+ if (!token) {
60
+ return null
61
+ }
62
+
63
+ return validateXMTPToolsToken(token)
64
+ }
65
+
66
+ /**
67
+ * JWT secret key used for signing and verifying tokens
68
+ * Uses XMTP_ENCRYPTION_KEY environment variable for consistency
69
+ * Only falls back to development secret in development/test environments
70
+ */
71
+ const JWT_SECRET = (() => {
72
+ const secret = process.env.XMTP_ENCRYPTION_KEY
73
+ const nodeEnv = process.env.NODE_ENV || "development"
74
+
75
+ // In production, require a real JWT secret
76
+ if (nodeEnv === "production" && !secret) {
77
+ throw new Error(
78
+ "XMTP_ENCRYPTION_KEY environment variable is required in production. " +
79
+ "Generate a secure random secret for JWT token signing."
80
+ )
81
+ }
82
+
83
+ // In development/test, allow fallback but warn
84
+ if (!secret) {
85
+ console.warn(
86
+ "⚠️ [SECURITY] Using fallback JWT secret for development. " +
87
+ "Set XMTP_ENCRYPTION_KEY environment variable for production."
88
+ )
89
+ return "fallback-secret-for-dev-only"
90
+ }
91
+
92
+ return secret
93
+ })()
94
+
95
+ /**
96
+ * API key used for simple authentication bypass
97
+ * Requires XMTP_API_KEY environment variable in production
98
+ * Only falls back to development key in development/test environments
99
+ */
100
+ const API_KEY = (() => {
101
+ const apiKey = process.env.XMTP_API_KEY
102
+ const nodeEnv = process.env.NODE_ENV || "development"
103
+
104
+ // In production, require a real API key
105
+ if (nodeEnv === "production" && !apiKey) {
106
+ throw new Error(
107
+ "XMTP_API_KEY environment variable is required in production. " +
108
+ "Generate a secure random API key for authentication."
109
+ )
110
+ }
111
+
112
+ // In development/test, allow fallback but warn
113
+ if (!apiKey) {
114
+ console.warn(
115
+ "⚠️ [SECURITY] Using fallback API key for development. " +
116
+ "Set XMTP_API_KEY environment variable for production."
117
+ )
118
+ return "fallback-api-key-for-dev-only"
119
+ }
120
+
121
+ return apiKey
122
+ })()
123
+
124
+ /**
125
+ * JWT token expiry time in seconds (5 minutes)
126
+ */
127
+ const JWT_EXPIRY = 5 * 60 // 5 minutes in seconds
128
+
129
+ /**
130
+ * Generates a signed JWT token for XMTP tools authentication
131
+ *
132
+ * @param {Omit<XMTPToolsPayload, "issued" | "expires">} payload - Token payload without timestamp fields
133
+ * @returns {string} Signed JWT token
134
+ *
135
+ * @description
136
+ * Creates a JWT token with automatic timestamp fields:
137
+ * - issued: Current timestamp
138
+ * - expires: Current timestamp + JWT_EXPIRY
139
+ *
140
+ * @example
141
+ * ```typescript
142
+ * const token = generateXMTPToolsToken({
143
+ * action: "send",
144
+ * conversationId: "0x123..."
145
+ * });
146
+ * ```
147
+ */
148
+ export function generateXMTPToolsToken(
149
+ payload: Omit<XMTPToolsPayload, "issued" | "expires">
150
+ ): string {
151
+ const now = Math.floor(Date.now() / 1000)
152
+ const fullPayload: XMTPToolsPayload = {
153
+ ...payload,
154
+ issued: now,
155
+ expires: now + JWT_EXPIRY
156
+ }
157
+
158
+ return jwt.sign(fullPayload, JWT_SECRET, {
159
+ expiresIn: JWT_EXPIRY
160
+ })
161
+ }
162
+
163
+ /**
164
+ * Validates an XMTP tools token using either API key or JWT verification
165
+ *
166
+ * @param {string} token - Token to validate (either API key or JWT)
167
+ * @returns {XMTPToolsPayload | null} Validated payload or null if invalid
168
+ *
169
+ * @description
170
+ * Supports two authentication methods in order of precedence:
171
+ * 1. API key authentication - Direct comparison with XMTP_API_KEY
172
+ * 2. JWT token authentication - Signature verification and expiry check
173
+ *
174
+ * For API key authentication, returns a default payload with 1-hour expiry.
175
+ * For JWT authentication, validates signature and checks expiry timestamp.
176
+ *
177
+ * @example
178
+ * ```typescript
179
+ * const payload = validateXMTPToolsToken(userToken);
180
+ * if (payload) {
181
+ * console.log(`Action: ${payload.action}`);
182
+ * console.log(`Conversation: ${payload.conversationId}`);
183
+ * }
184
+ * ```
185
+ */
186
+ export function validateXMTPToolsToken(token: string): XMTPToolsPayload | null {
187
+ // First try API key authentication
188
+ if (token === API_KEY) {
189
+ console.log("🔑 [Auth] Using API key authentication")
190
+ // Return a valid payload for API key auth
191
+ const now = Math.floor(Date.now() / 1000)
192
+ return {
193
+ action: "send", // Default action
194
+ conversationId: "", // Will be filled by endpoint
195
+ issued: now,
196
+ expires: now + 3600 // API keys are valid for 1 hour
197
+ }
198
+ }
199
+
200
+ // Then try JWT token authentication
201
+ try {
202
+ const decoded = jwt.verify(token, JWT_SECRET) as XMTPToolsPayload
203
+ console.log("🔑 [Auth] Using JWT token authentication")
204
+
205
+ // Additional expiry check
206
+ const now = Math.floor(Date.now() / 1000)
207
+ if (decoded.expires < now) {
208
+ console.warn("🔒 XMTP tools token has expired")
209
+ return null
210
+ }
211
+
212
+ return decoded
213
+ } catch (error) {
214
+ console.error(
215
+ "🔒 Invalid XMTP tools token and not matching API key:",
216
+ error
217
+ )
218
+ return null
219
+ }
220
+ }
package/src/plugin.ts ADDED
@@ -0,0 +1,39 @@
1
+ import { Hono } from "hono"
2
+ import xmtpEndpoints from "./endpoints"
3
+ import type { MessageListenerConfig } from "./lib/message-listener"
4
+ import type { HonoVariables } from "./types"
5
+
6
+ export interface Plugin<TContext = unknown> {
7
+ name: string
8
+ description?: string
9
+ apply: (
10
+ app: Hono<{ Variables: HonoVariables }>,
11
+ context?: TContext
12
+ ) => void | Promise<void>
13
+ }
14
+
15
+ export interface XMTPPluginContext {
16
+ agent: unknown
17
+ }
18
+
19
+ /**
20
+ * XMTP Plugin that provides XMTP functionality to the agent
21
+ *
22
+ * @description
23
+ * This plugin integrates XMTP messaging capabilities into the agent's
24
+ * HTTP server. It mounts the XMTP endpoints for handling XMTP tools requests.
25
+ */
26
+ export function XMTPPlugin({
27
+ filter
28
+ }: {
29
+ filter?: MessageListenerConfig["filter"]
30
+ } = {}): Plugin<XMTPPluginContext> {
31
+ return {
32
+ name: "xmtp",
33
+ description: "Provides XMTP messaging functionality",
34
+ apply: (app, context) => {
35
+ // Mount the XMTP endpoints at /xmtp-tools
36
+ app.route("/xmtp-tools", xmtpEndpoints)
37
+ }
38
+ }
39
+ }