@hybrd/xmtp 1.3.2 → 1.4.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.
Files changed (38) hide show
  1. package/README.md +41 -7
  2. package/dist/index.cjs +415 -3085
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +12 -828
  5. package/dist/index.d.ts +12 -828
  6. package/dist/index.js +416 -3056
  7. package/dist/index.js.map +1 -1
  8. package/package.json +18 -5
  9. package/src/client.ts +23 -135
  10. package/src/index.ts +28 -81
  11. package/src/index.ts.old +145 -0
  12. package/src/lib/jwt.ts +45 -13
  13. package/src/lib/{message-listener.test.ts → message-listener.test.ts.old} +1 -1
  14. package/src/lib/subjects.ts +6 -5
  15. package/src/plugin.filters.test.ts +158 -0
  16. package/src/plugin.ts +456 -23
  17. package/src/resolver/address-resolver.ts +217 -211
  18. package/src/resolver/basename-resolver.ts +6 -5
  19. package/src/resolver/ens-resolver.ts +15 -14
  20. package/src/resolver/resolver.ts +3 -2
  21. package/src/resolver/xmtp-resolver.ts +10 -9
  22. package/src/{service-client.ts → service-client.ts.old} +26 -3
  23. package/src/types.ts +9 -157
  24. package/src/types.ts.old +157 -0
  25. package/.cache/tsbuildinfo.json +0 -1
  26. package/.turbo/turbo-build.log +0 -45
  27. package/.turbo/turbo-lint$colon$fix.log +0 -6
  28. package/.turbo/turbo-typecheck.log +0 -5
  29. package/biome.jsonc +0 -4
  30. package/scripts/generate-keys.ts +0 -25
  31. package/scripts/refresh-identity.ts +0 -119
  32. package/scripts/register-wallet.ts +0 -95
  33. package/scripts/revoke-all-installations.ts +0 -91
  34. package/scripts/revoke-installations.ts +0 -94
  35. package/src/endpoints.ts +0 -306
  36. package/tsconfig.json +0 -9
  37. package/tsup.config.ts +0 -14
  38. /package/src/lib/{message-listener.ts → message-listener.ts.old} +0 -0
package/src/lib/jwt.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Context } from "hono"
2
2
  import jwt from "jsonwebtoken"
3
+ import { logger } from "@hybrd/utils"
3
4
 
4
5
  export interface XMTPToolsPayload {
5
6
  action: "send" | "reply" | "react" | "transaction" | "blockchain-event"
@@ -65,26 +66,26 @@ export function getValidatedPayload(c: Context): XMTPToolsPayload | null {
65
66
 
66
67
  /**
67
68
  * Gets the JWT secret for token signing, with lazy initialization
68
- * Uses XMTP_ENCRYPTION_KEY environment variable for consistency
69
+ * Uses XMTP_DB_ENCRYPTION_KEY environment variable for consistency
69
70
  * Only falls back to development secret in development/test environments
70
71
  */
71
72
  function getJwtSecret(): string {
72
- const secret = process.env.XMTP_ENCRYPTION_KEY
73
+ const secret = process.env.XMTP_DB_ENCRYPTION_KEY
73
74
  const nodeEnv = process.env.NODE_ENV || "development"
74
75
 
75
76
  // In production, require a real JWT secret
76
77
  if (nodeEnv === "production" && !secret) {
77
78
  throw new Error(
78
- "XMTP_ENCRYPTION_KEY environment variable is required in production. " +
79
+ "XMTP_DB_ENCRYPTION_KEY environment variable is required in production. " +
79
80
  "Generate a secure random secret for JWT token signing."
80
81
  )
81
82
  }
82
83
 
83
84
  // In development/test, allow fallback but warn only when actually used
84
85
  if (!secret) {
85
- console.warn(
86
+ logger.warn(
86
87
  "⚠️ [SECURITY] Using fallback JWT secret for development. " +
87
- "Set XMTP_ENCRYPTION_KEY environment variable for production."
88
+ "Set XMTP_DB_ENCRYPTION_KEY environment variable for production."
88
89
  )
89
90
  return "fallback-secret-for-dev-only"
90
91
  }
@@ -111,7 +112,7 @@ function getApiKey(): string {
111
112
 
112
113
  // In development/test, allow fallback but warn only when actually used
113
114
  if (!apiKey) {
114
- console.warn(
115
+ logger.warn(
115
116
  "⚠️ [SECURITY] Using fallback API key for development. " +
116
117
  "Set XMTP_API_KEY environment variable for production."
117
118
  )
@@ -148,6 +149,9 @@ const JWT_EXPIRY = 5 * 60 // 5 minutes in seconds
148
149
  export function generateXMTPToolsToken(
149
150
  payload: Omit<XMTPToolsPayload, "issued" | "expires">
150
151
  ): string {
152
+ const startTime = performance.now()
153
+ logger.debug("🔐 [JWT] Starting token generation...")
154
+
151
155
  const now = Math.floor(Date.now() / 1000)
152
156
  const fullPayload: XMTPToolsPayload = {
153
157
  ...payload,
@@ -155,9 +159,16 @@ export function generateXMTPToolsToken(
155
159
  expires: now + JWT_EXPIRY
156
160
  }
157
161
 
158
- return jwt.sign(fullPayload, getJwtSecret(), {
162
+ const token = jwt.sign(fullPayload, getJwtSecret(), {
159
163
  expiresIn: JWT_EXPIRY
160
164
  })
165
+
166
+ const endTime = performance.now()
167
+ logger.debug(
168
+ `🔐 [JWT] Token generation completed in ${(endTime - startTime).toFixed(2)}ms`
169
+ )
170
+
171
+ return token
161
172
  }
162
173
 
163
174
  /**
@@ -184,37 +195,58 @@ export function generateXMTPToolsToken(
184
195
  * ```
185
196
  */
186
197
  export function validateXMTPToolsToken(token: string): XMTPToolsPayload | null {
198
+ const startTime = performance.now()
199
+ logger.debug("🔐 [JWT] Starting token validation...")
200
+
187
201
  // First try API key authentication
188
202
  if (token === getApiKey()) {
189
- console.log("🔑 [Auth] Using API key authentication")
203
+ logger.debug("🔑 [Auth] Using API key authentication")
190
204
  // Return a valid payload for API key auth
191
205
  const now = Math.floor(Date.now() / 1000)
192
- return {
193
- action: "send", // Default action
206
+ const result = {
207
+ action: "send" as const, // Default action
194
208
  conversationId: "", // Will be filled by endpoint
195
209
  issued: now,
196
210
  expires: now + 3600 // API keys are valid for 1 hour
197
211
  }
212
+
213
+ const endTime = performance.now()
214
+ logger.debug(
215
+ `🔐 [JWT] API key validation completed in ${(endTime - startTime).toFixed(2)}ms`
216
+ )
217
+ return result
198
218
  }
199
219
 
200
220
  // Then try JWT token authentication
201
221
  try {
202
222
  const decoded = jwt.verify(token, getJwtSecret()) as XMTPToolsPayload
203
- console.log("🔑 [Auth] Using JWT token authentication")
223
+ logger.debug("🔑 [Auth] Using JWT token authentication")
204
224
 
205
225
  // Additional expiry check
206
226
  const now = Math.floor(Date.now() / 1000)
207
227
  if (decoded.expires < now) {
208
- console.warn("🔒 XMTP tools token has expired")
228
+ console.log("🔒 XMTP tools token has expired")
229
+ const endTime = performance.now()
230
+ logger.debug(
231
+ `🔐 [JWT] Token validation failed (expired) in ${(endTime - startTime).toFixed(2)}ms`
232
+ )
209
233
  return null
210
234
  }
211
235
 
236
+ const endTime = performance.now()
237
+ logger.debug(
238
+ `🔐 [JWT] JWT validation completed in ${(endTime - startTime).toFixed(2)}ms`
239
+ )
212
240
  return decoded
213
241
  } catch (error) {
214
- console.error(
242
+ logger.error(
215
243
  "🔒 Invalid XMTP tools token and not matching API key:",
216
244
  error
217
245
  )
246
+ const endTime = performance.now()
247
+ logger.debug(
248
+ `🔐 [JWT] Token validation failed in ${(endTime - startTime).toFixed(2)}ms`
249
+ )
218
250
  return null
219
251
  }
220
252
  }
@@ -1,6 +1,6 @@
1
1
  import { EventEmitter } from "node:events"
2
2
  import { beforeEach, describe, expect, it, vi } from "vitest"
3
- import { MessageListener } from "./message-listener"
3
+ import { MessageListener } from "./message-listener.ts.old"
4
4
 
5
5
  /**
6
6
  * Check if content contains any of the supported agent mention patterns
@@ -1,5 +1,6 @@
1
1
  import type { BasenameResolver } from "../resolver/basename-resolver"
2
2
  import type { ENSResolver } from "../resolver/ens-resolver"
3
+ import { logger } from "@hybrd/utils"
3
4
 
4
5
  /**
5
6
  * Extract basenames/ENS names from message content using @mention pattern
@@ -38,7 +39,7 @@ export async function resolveSubjects(
38
39
  return subjects
39
40
  }
40
41
 
41
- console.log(
42
+ logger.debug(
42
43
  `🔍 Found ${mentionedNames.length} name mentions:`,
43
44
  mentionedNames
44
45
  )
@@ -49,20 +50,20 @@ export async function resolveSubjects(
49
50
 
50
51
  // Check if it's an ENS name (.eth but not .base.eth)
51
52
  if (ensResolver.isENSName(mentionedName)) {
52
- console.log(`🔍 Resolving ENS name: ${mentionedName}`)
53
+ logger.debug(`🔍 Resolving ENS name: ${mentionedName}`)
53
54
  resolvedAddress = await ensResolver.resolveENSName(mentionedName)
54
55
  } else {
55
56
  // It's a basename (.base.eth or other format)
56
- console.log(`🔍 Resolving basename: ${mentionedName}`)
57
+ logger.debug(`🔍 Resolving basename: ${mentionedName}`)
57
58
  resolvedAddress =
58
59
  await basenameResolver.getBasenameAddress(mentionedName)
59
60
  }
60
61
 
61
62
  if (resolvedAddress) {
62
63
  subjects[mentionedName] = resolvedAddress as `0x${string}`
63
- console.log(`✅ Resolved ${mentionedName} → ${resolvedAddress}`)
64
+ logger.debug(`✅ Resolved ${mentionedName} → ${resolvedAddress}`)
64
65
  } else {
65
- console.log(`❌ Could not resolve address for: ${mentionedName}`)
66
+ logger.debug(`❌ Could not resolve address for: ${mentionedName}`)
66
67
  }
67
68
  } catch (error) {
68
69
  console.error(`❌ Error resolving ${mentionedName}:`, error)
@@ -0,0 +1,158 @@
1
+ import type { HonoVariables, PluginContext } from "@hybrd/types"
2
+ import { Hono } from "hono"
3
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
4
+ import { XMTPPlugin } from "./plugin"
5
+
6
+ // Mock @xmtp/agent-sdk and ./client used by the plugin
7
+ vi.mock("@xmtp/agent-sdk", () => {
8
+ const handlers: Record<string, Array<(payload: unknown) => unknown>> = {}
9
+ const fakeXmtp = {
10
+ on: (event: string, cb: (payload: unknown) => unknown) => {
11
+ if (!handlers[event]) handlers[event] = []
12
+ handlers[event].push(cb)
13
+ },
14
+ emit: async (event: string, payload: unknown) => {
15
+ for (const cb of handlers[event] || []) {
16
+ // eslint-disable-next-line @typescript-eslint/await-thenable
17
+ await cb(payload)
18
+ }
19
+ },
20
+ start: vi.fn(async () => {})
21
+ }
22
+
23
+ return {
24
+ Agent: { create: vi.fn(async () => fakeXmtp) },
25
+ createUser: vi.fn(() => ({ account: { address: "0xabc" } })),
26
+ createSigner: vi.fn(() => ({})),
27
+ getTestUrl: vi.fn(() => "http://test"),
28
+ XmtpEnv: {},
29
+ __fakeXmtp: fakeXmtp
30
+ }
31
+ })
32
+
33
+ vi.mock("./client", () => {
34
+ const fakeConversation = {
35
+ id: "conv1",
36
+ send: vi.fn(async (_text: string) => {})
37
+ }
38
+
39
+ const fakeClient = {
40
+ inboxId: "inbox-1",
41
+ accountIdentifier: { identifier: "0xabc" },
42
+ conversations: {
43
+ list: vi.fn(async () => [] as unknown[]),
44
+ getConversationById: vi.fn(async (_id: string) => fakeConversation),
45
+ streamAllMessages: vi.fn(async function* () {
46
+ // Yield one message and finish
47
+ yield {
48
+ id: "msg1",
49
+ senderInboxId: "other-inbox",
50
+ content: "hello",
51
+ contentType: { sameAs: () => true },
52
+ conversationId: "conv1"
53
+ }
54
+ })
55
+ }
56
+ }
57
+
58
+ async function getDbPath(_name: string): Promise<string> {
59
+ return "/tmp/xmtp-test-db"
60
+ }
61
+
62
+ return {
63
+ createXMTPClient: vi.fn(async () => fakeClient),
64
+ getDbPath
65
+ }
66
+ })
67
+
68
+ type MockAgentSdk = {
69
+ __fakeXmtp: {
70
+ emit: (event: string, payload: unknown) => Promise<void>
71
+ }
72
+ }
73
+
74
+ function createTestAgent() {
75
+ return {
76
+ name: "test-agent",
77
+ plugins: { applyAll: vi.fn(async () => {}) },
78
+ createRuntimeContext: vi.fn(
79
+ async (base: unknown) => base as Record<string, unknown>
80
+ ),
81
+ generate: vi.fn(async () => ({ text: "ok" }))
82
+ }
83
+ }
84
+
85
+ beforeEach(() => {
86
+ vi.resetModules()
87
+ vi.clearAllMocks()
88
+ process.env.XMTP_WALLET_KEY = "0xabc"
89
+ process.env.XMTP_DB_ENCRYPTION_KEY = "secret"
90
+ process.env.XMTP_ENV = "dev"
91
+ process.env.XMTP_ENABLE_NODE_STREAM = undefined
92
+ })
93
+
94
+ afterEach(() => {
95
+ process.env.XMTP_WALLET_KEY = undefined
96
+ process.env.XMTP_DB_ENCRYPTION_KEY = undefined
97
+ process.env.XMTP_ENV = undefined
98
+ process.env.XMTP_ENABLE_NODE_STREAM = undefined
99
+ })
100
+
101
+ describe("XMTPPlugin behaviors", () => {
102
+ it("blocks node stream messages when behaviors filter out", async () => {
103
+ const app = new Hono<{ Variables: HonoVariables }>()
104
+ const agent = createTestAgent()
105
+
106
+ // Mock behaviors to filter out messages
107
+ const mockBehaviors = {
108
+ executeBefore: vi.fn(async (context: any) => {
109
+ context.sendOptions = { filtered: true }
110
+ }),
111
+ executeAfter: vi.fn(async () => {})
112
+ }
113
+
114
+ const context = {
115
+ agent,
116
+ behaviors: mockBehaviors
117
+ } as unknown as PluginContext
118
+
119
+ const plugin = XMTPPlugin()
120
+ await plugin.apply(app, context)
121
+
122
+ // Allow async stream to tick
123
+ await new Promise((r) => setTimeout(r, 10))
124
+
125
+ // No generation or sending should have occurred
126
+ expect(agent.generate).not.toHaveBeenCalled()
127
+ })
128
+
129
+ it("allows text handler when behaviors don't filter", async () => {
130
+ const app = new Hono<{ Variables: HonoVariables }>()
131
+ const agent = createTestAgent()
132
+
133
+ // Mock behaviors to not filter messages
134
+ const mockBehaviors = {
135
+ executeBefore: vi.fn(async () => {}),
136
+ executeAfter: vi.fn(async () => {})
137
+ }
138
+
139
+ const context = {
140
+ agent,
141
+ behaviors: mockBehaviors
142
+ } as unknown as PluginContext
143
+
144
+ process.env.XMTP_ENABLE_NODE_STREAM = "false"
145
+ const plugin = XMTPPlugin()
146
+ await plugin.apply(app, context)
147
+
148
+ const mocked = (await import("@xmtp/agent-sdk")) as unknown as MockAgentSdk
149
+
150
+ // Emit a text event
151
+ await mocked.__fakeXmtp.emit("text", {
152
+ conversation: { id: "conv1", send: vi.fn(async () => {}) },
153
+ message: { content: "hello" }
154
+ })
155
+
156
+ expect(agent.generate).toHaveBeenCalledTimes(1)
157
+ })
158
+ })