@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/.cache/tsbuildinfo.json +1 -1
- package/.turbo/turbo-build.log +12 -12
- package/.turbo/turbo-lint$colon$fix.log +2 -2
- package/.turbo/turbo-typecheck.log +5 -0
- package/dist/index.cjs +348 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +64 -1
- package/dist/index.d.ts +64 -1
- package/dist/index.js +345 -8
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
- package/src/endpoints.ts +306 -0
- package/src/index.ts +15 -3
- package/src/lib/jwt.ts +220 -0
- package/src/plugin.ts +39 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hybrd/xmtp",
|
|
3
|
-
"version": "1.
|
|
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/
|
|
30
|
-
"@config/
|
|
30
|
+
"@config/biome": "0.0.0",
|
|
31
|
+
"@config/tsconfig": "0.0.0"
|
|
31
32
|
},
|
|
32
33
|
"scripts": {
|
|
33
34
|
"build": "tsup",
|
package/src/endpoints.ts
ADDED
|
@@ -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
|
-
|
|
34
|
-
|
|
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
|
+
}
|