@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.
- package/README.md +41 -7
- package/dist/index.cjs +415 -3085
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +12 -828
- package/dist/index.d.ts +12 -828
- package/dist/index.js +416 -3056
- package/dist/index.js.map +1 -1
- package/package.json +18 -5
- package/src/client.ts +23 -135
- package/src/index.ts +28 -81
- package/src/index.ts.old +145 -0
- package/src/lib/jwt.ts +45 -13
- package/src/lib/{message-listener.test.ts → message-listener.test.ts.old} +1 -1
- package/src/lib/subjects.ts +6 -5
- package/src/plugin.filters.test.ts +158 -0
- package/src/plugin.ts +456 -23
- package/src/resolver/address-resolver.ts +217 -211
- package/src/resolver/basename-resolver.ts +6 -5
- package/src/resolver/ens-resolver.ts +15 -14
- package/src/resolver/resolver.ts +3 -2
- package/src/resolver/xmtp-resolver.ts +10 -9
- package/src/{service-client.ts → service-client.ts.old} +26 -3
- package/src/types.ts +9 -157
- package/src/types.ts.old +157 -0
- package/.cache/tsbuildinfo.json +0 -1
- package/.turbo/turbo-build.log +0 -45
- package/.turbo/turbo-lint$colon$fix.log +0 -6
- package/.turbo/turbo-typecheck.log +0 -5
- package/biome.jsonc +0 -4
- package/scripts/generate-keys.ts +0 -25
- package/scripts/refresh-identity.ts +0 -119
- package/scripts/register-wallet.ts +0 -95
- package/scripts/revoke-all-installations.ts +0 -91
- package/scripts/revoke-installations.ts +0 -94
- package/src/endpoints.ts +0 -306
- package/tsconfig.json +0 -9
- package/tsup.config.ts +0 -14
- /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
|
|
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.
|
|
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
|
-
"
|
|
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
|
-
|
|
86
|
+
logger.warn(
|
|
86
87
|
"⚠️ [SECURITY] Using fallback JWT secret for development. " +
|
|
87
|
-
"Set
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
package/src/lib/subjects.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
64
|
+
logger.debug(`✅ Resolved ${mentionedName} → ${resolvedAddress}`)
|
|
64
65
|
} else {
|
|
65
|
-
|
|
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
|
+
})
|