@hybrd/xmtp 1.4.4 → 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.
@@ -1,203 +0,0 @@
1
- import { existsSync, mkdirSync } from "node:fs";
2
- import * as fs from "node:fs/promises";
3
- import path from "node:path";
4
- import { validateEnvironment } from "./client";
5
- import type { WalletInfo, WalletStorage } from "./walletService";
6
-
7
- const { XMTP_NETWORK_ID } = validateEnvironment(["XMTP_NETWORK_ID"]);
8
- export const STORAGE_DIRS = {
9
- WALLET: ".data/wallet_data",
10
- XMTP: ".data/xmtp",
11
- };
12
-
13
- /**
14
- * Generic file-based storage service
15
- */
16
- export class FileStorage implements WalletStorage {
17
- private initialized = false;
18
-
19
- constructor(private baseDirs = STORAGE_DIRS) {
20
- this.initialize();
21
- }
22
-
23
- /**
24
- * Initialize storage directories
25
- */
26
- public initialize(): void {
27
- if (this.initialized) return;
28
-
29
- Object.values(this.baseDirs).forEach((dir) => {
30
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
31
- });
32
-
33
- this.initialized = true;
34
- }
35
-
36
- /**
37
- * File operations - save/read/delete
38
- */
39
- private async saveToFile(
40
- directory: string,
41
- identifier: string,
42
- data: string,
43
- ): Promise<boolean> {
44
- const key = `${identifier}-${XMTP_NETWORK_ID}`;
45
- try {
46
- await fs.writeFile(path.join(directory, `${key}.json`), data);
47
- return true;
48
- } catch (error) {
49
- console.error(`Error writing to file ${key}:`, error);
50
- return false;
51
- }
52
- }
53
-
54
- private async readFromFile<T>(
55
- directory: string,
56
- identifier: string,
57
- ): Promise<T | null> {
58
- try {
59
- const key = `${identifier}-${XMTP_NETWORK_ID}`;
60
- const data = await fs.readFile(
61
- path.join(directory, `${key}.json`),
62
- "utf-8",
63
- );
64
- return JSON.parse(data) as T;
65
- } catch (error) {
66
- if (
67
- error instanceof Error &&
68
- (error.message.includes("ENOENT") ||
69
- error.message.includes("no such file or directory"))
70
- ) {
71
- return null;
72
- }
73
- throw error;
74
- }
75
- }
76
-
77
- public async deleteFile(directory: string, key: string): Promise<boolean> {
78
- try {
79
- await fs.unlink(path.join(directory, `${key}.json`));
80
- return true;
81
- } catch (error) {
82
- console.error(`Error deleting file ${key}:`, error);
83
- return false;
84
- }
85
- }
86
-
87
- /**
88
- * Generic data operations
89
- */
90
- public async saveData(
91
- category: string,
92
- id: string,
93
- data: unknown,
94
- ): Promise<boolean> {
95
- if (!this.initialized) this.initialize();
96
-
97
- // Make sure the directory exists
98
- const directory = path.join(".data", category);
99
- if (!existsSync(directory)) mkdirSync(directory, { recursive: true });
100
-
101
- return await this.saveToFile(directory, id, JSON.stringify(data));
102
- }
103
-
104
- public async getData<T>(category: string, id: string): Promise<T | null> {
105
- if (!this.initialized) this.initialize();
106
-
107
- const directory = path.join(".data", category);
108
- return this.readFromFile<T>(directory, id);
109
- }
110
-
111
- public async listData<T>(category: string): Promise<T[]> {
112
- if (!this.initialized) this.initialize();
113
-
114
- try {
115
- const directory = path.join(".data", category);
116
- if (!existsSync(directory)) return [];
117
-
118
- const files = await fs.readdir(directory);
119
- const items: T[] = [];
120
-
121
- for (const file of files.filter((f) => f.endsWith(".json"))) {
122
- const id = file.replace(`-${XMTP_NETWORK_ID}.json`, "");
123
- const data = await this.getData<T>(category, id);
124
- if (data) items.push(data);
125
- }
126
-
127
- return items;
128
- } catch (error) {
129
- console.error(`Error listing data in ${category}:`, error);
130
- return [];
131
- }
132
- }
133
-
134
- public async deleteData(category: string, id: string): Promise<boolean> {
135
- if (!this.initialized) this.initialize();
136
-
137
- try {
138
- const directory = path.join(".data", category);
139
- const key = `${id}-${XMTP_NETWORK_ID}`;
140
- return await this.deleteFile(directory, key);
141
- } catch (error) {
142
- console.error(`Error deleting data ${id} from ${category}:`, error);
143
- return false;
144
- }
145
- }
146
-
147
- /**
148
- * Wallet Storage implementation
149
- */
150
- public async saveWallet(userId: string, walletData: string): Promise<void> {
151
- if (!this.initialized) this.initialize();
152
- await this.saveToFile(this.baseDirs.WALLET, userId, walletData);
153
- }
154
-
155
- public async getWallet(userId: string): Promise<WalletInfo | null> {
156
- if (!this.initialized) this.initialize();
157
- return this.readFromFile(this.baseDirs.WALLET, userId);
158
- }
159
-
160
- public async getWalletByAddress(address: string): Promise<WalletInfo | null> {
161
- if (!this.initialized) this.initialize();
162
- try {
163
- const directory = this.baseDirs.WALLET;
164
- if (!existsSync(directory)) return null;
165
-
166
- const files = await fs.readdir(directory);
167
-
168
- for (const file of files.filter((f) => f.endsWith(".json"))) {
169
- try {
170
- const data = await fs.readFile(path.join(directory, file), "utf-8");
171
- const walletData = JSON.parse(data) as WalletInfo;
172
-
173
- // Check if this wallet has the target address
174
- if (walletData.address.toLowerCase() === address.toLowerCase()) {
175
- return walletData;
176
- }
177
- } catch (err) {
178
- console.error(`Error parsing wallet data from ${file}:`, err);
179
- // Skip files with parsing errors
180
- continue;
181
- }
182
- }
183
-
184
- return null;
185
- } catch (error) {
186
- console.error(`Error finding wallet by address ${address}:`, error);
187
- return null;
188
- }
189
- }
190
-
191
- public async getWalletCount(): Promise<number> {
192
- try {
193
- const files = await fs.readdir(this.baseDirs.WALLET);
194
- return files.filter((file) => file.endsWith(".json")).length;
195
- } catch (error) {
196
- console.error("Error getting wallet count:", error);
197
- return 0;
198
- }
199
- }
200
- }
201
-
202
- // Export a single global instance
203
- export const storage = new FileStorage();
@@ -1,158 +0,0 @@
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
- })
@@ -1,309 +0,0 @@
1
- /**
2
- * @fileoverview XMTP Service Client Library
3
- *
4
- * Clean, reusable client for making HTTP calls to the XMTP listener service.
5
- * Handles authentication, request formatting, and error handling.
6
- *
7
- * This is different from the direct XMTP client - this is for external services
8
- * talking to our XMTP listener service.
9
- */
10
-
11
- import type {
12
- GetMessageParams,
13
- SendMessageParams,
14
- SendMessageResponse,
15
- SendReactionParams,
16
- SendReactionResponse,
17
- SendReplyParams,
18
- SendReplyResponse,
19
- SendTransactionParams,
20
- SendTransactionResponse,
21
- XmtpServiceClientConfig,
22
- XmtpServiceMessage,
23
- XmtpServiceResponse
24
- } from "./types"
25
- import { logger } from "./lib/logger"
26
-
27
- export class XmtpServiceClient {
28
- private config: XmtpServiceClientConfig
29
-
30
- constructor(config: XmtpServiceClientConfig) {
31
- this.config = config
32
- }
33
-
34
- private async request<T = unknown>(
35
- endpoint: string,
36
- body?: Record<string, unknown>,
37
- method: "GET" | "POST" = "POST"
38
- ): Promise<XmtpServiceResponse<T>> {
39
- const startTime = performance.now()
40
- logger.debug(`🌐 [HTTP] Starting ${method} request to ${endpoint}`)
41
-
42
- try {
43
- const baseUrl = this.config.serviceUrl.replace(/\/+$/, "")
44
-
45
- // Use Authorization header for xmtp-tools endpoints, query parameter for others
46
- const isXmtpToolsEndpoint = endpoint.startsWith("/xmtp-tools/")
47
- const url = `${baseUrl}${endpoint}?token=${this.config.serviceToken}`
48
-
49
- const headers: Record<string, string> = {
50
- "Content-Type": "application/json"
51
- }
52
-
53
- // Add Authorization header for xmtp-tools endpoints
54
- if (isXmtpToolsEndpoint) {
55
- headers.Authorization = `Bearer ${this.config.serviceToken}`
56
- }
57
-
58
- const fetchOptions: RequestInit = {
59
- method,
60
- headers
61
- }
62
-
63
- if (method === "POST" && body) {
64
- fetchOptions.body = JSON.stringify(body)
65
- }
66
-
67
- const sanitizedUrl = `${baseUrl}${endpoint}?token=***`
68
- logger.debug(`🌐 [HTTP] Making fetch request to ${sanitizedUrl}`)
69
- const fetchStartTime = performance.now()
70
-
71
- const response = await fetch(url, fetchOptions)
72
-
73
- const fetchEndTime = performance.now()
74
- logger.debug(`🌐 [HTTP] Fetch completed in ${(fetchEndTime - fetchStartTime).toFixed(2)}ms, status: ${response.status}`)
75
-
76
- if (!response.ok) {
77
- let errorMessage = `HTTP ${response.status}`
78
- try {
79
- const responseText = await response.text()
80
- try {
81
- const errorData = JSON.parse(responseText) as { error?: string }
82
- errorMessage = errorData.error || errorMessage
83
- } catch {
84
- errorMessage = responseText || errorMessage
85
- }
86
- } catch {
87
- // If we can't read the response at all, use the status
88
- }
89
-
90
- const endTime = performance.now()
91
- logger.debug(`🌐 [HTTP] Request failed in ${(endTime - startTime).toFixed(2)}ms: ${errorMessage}`)
92
- throw new Error(errorMessage)
93
- }
94
-
95
- const jsonStartTime = performance.now()
96
- const result = {
97
- success: true,
98
- data: (await response.json()) as T
99
- }
100
- const jsonEndTime = performance.now()
101
- logger.debug(`🌐 [HTTP] JSON parsing completed in ${(jsonEndTime - jsonStartTime).toFixed(2)}ms`)
102
-
103
- const endTime = performance.now()
104
- logger.debug(`🌐 [HTTP] Total request completed in ${(endTime - startTime).toFixed(2)}ms`)
105
-
106
- return result
107
- } catch (error) {
108
- const endTime = performance.now()
109
- logger.error(
110
- `❌ [XmtpServiceClient] Request to ${endpoint} failed in ${(endTime - startTime).toFixed(2)}ms:`,
111
- error
112
- )
113
- return {
114
- success: false,
115
- error: error instanceof Error ? error.message : "Unknown error"
116
- }
117
- }
118
- }
119
-
120
- async sendMessage(
121
- params: SendMessageParams
122
- ): Promise<XmtpServiceResponse<SendMessageResponse>> {
123
- return this.request<SendMessageResponse>("/xmtp-tools/send", {
124
- content: params.content
125
- })
126
- }
127
-
128
- async sendReply(
129
- params: SendReplyParams
130
- ): Promise<XmtpServiceResponse<SendReplyResponse>> {
131
- return this.request<SendReplyResponse>("/xmtp-tools/reply", {
132
- content: params.content,
133
- messageId: params.messageId
134
- })
135
- }
136
-
137
- async sendReaction(
138
- params: SendReactionParams
139
- ): Promise<XmtpServiceResponse<SendReactionResponse>> {
140
- return this.request<SendReactionResponse>("/xmtp-tools/react", {
141
- messageId: params.messageId,
142
- emoji: params.emoji,
143
- action: params.action
144
- })
145
- }
146
-
147
- async sendTransaction(
148
- params: SendTransactionParams
149
- ): Promise<XmtpServiceResponse<SendTransactionResponse>> {
150
- return this.request<SendTransactionResponse>("/xmtp-tools/transaction", {
151
- fromAddress: params.fromAddress,
152
- chainId: params.chainId,
153
- calls: params.calls.map((call) => ({
154
- to: call.to,
155
- data: call.data,
156
- ...(call.gas && { gas: call.gas }),
157
- value: call.value || "0x0",
158
- metadata: {
159
- ...call.metadata,
160
- chainId: params.chainId,
161
- from: params.fromAddress,
162
- version: "1"
163
- }
164
- }))
165
- })
166
- }
167
-
168
- /**
169
- * Get a single message by ID
170
- */
171
- async getMessage(
172
- params: GetMessageParams
173
- ): Promise<XmtpServiceResponse<XmtpServiceMessage>> {
174
- return this.request<XmtpServiceMessage>(
175
- `/xmtp-tools/messages/${params.messageId}`,
176
- undefined,
177
- "GET"
178
- )
179
- }
180
-
181
- // getConversationMessages removed - superseded by thread-based approach
182
- }
183
-
184
- /**
185
- * Create an XMTP service client from runtime context
186
- * Expects the runtime context to have xmtpServiceUrl and xmtpServiceToken
187
- */
188
- export function createXmtpServiceClient(
189
- serviceUrl: string,
190
- serviceToken: string
191
- ): XmtpServiceClient {
192
- if (!serviceUrl || !serviceToken) {
193
- throw new Error("Missing XMTP service URL or token from runtime context")
194
- }
195
-
196
- return new XmtpServiceClient({
197
- serviceUrl,
198
- serviceToken
199
- })
200
- }
201
-
202
- export interface XmtpAuthConfig {
203
- serviceUrl: string
204
- serviceToken: string
205
- source: "callback" | "environment"
206
- }
207
-
208
- /**
209
- * Get XMTP authentication configuration from multiple sources
210
- * Priority: callback credentials > environment credentials
211
- */
212
- export function getXmtpAuthConfig(
213
- callbackUrl?: string,
214
- callbackToken?: string
215
- ): XmtpAuthConfig | null {
216
- // Priority 1: Use callback credentials if available
217
- if (callbackUrl && callbackToken) {
218
- console.log("🔑 [XmtpAuth] Using callback-provided credentials")
219
- return {
220
- serviceUrl: callbackUrl,
221
- serviceToken: callbackToken,
222
- source: "callback"
223
- }
224
- }
225
-
226
- // Priority 2: Use environment credentials
227
- const envUrl = process.env.XMTP_HOST
228
- const envToken = process.env.XMTP_API_KEY
229
-
230
- if (envUrl && envToken) {
231
- console.log("🔑 [XmtpAuth] Using environment credentials")
232
- return {
233
- serviceUrl: envUrl,
234
- serviceToken: envToken,
235
- source: "environment"
236
- }
237
- }
238
-
239
- // No valid credentials found
240
- console.error(
241
- "❌ [XmtpAuth] No XMTP credentials found in callback or environment"
242
- )
243
- console.error(
244
- "💡 [XmtpAuth] Expected: XMTP_HOST + XMTP_API_KEY or callback credentials"
245
- )
246
- return null
247
- }
248
-
249
- /**
250
- * Create an authenticated XMTP service client
251
- * Handles both callback and environment credential sources
252
- */
253
- export function createAuthenticatedXmtpClient(
254
- callbackUrl?: string,
255
- callbackToken?: string
256
- ): XmtpServiceClient {
257
- const authConfig = getXmtpAuthConfig(callbackUrl, callbackToken)
258
-
259
- if (!authConfig) {
260
- throw new Error("No XMTP credentials found")
261
- }
262
-
263
- console.log(
264
- `🔗 [XmtpAuth] Creating XMTP client (${authConfig.source} credentials)`
265
- )
266
-
267
- return createXmtpServiceClient(authConfig.serviceUrl, authConfig.serviceToken)
268
- }
269
-
270
- /**
271
- * Constructs a URL for XMTP tools API endpoints with token authentication
272
- *
273
- * @param {string} baseUrl - The base URL of the XMTP service (e.g., "https://api.example.com")
274
- * @param {string} action - The specific action/endpoint to call (e.g., "send", "receive", "status")
275
- * @param {string} token - Authentication token (either JWT or API key)
276
- * @returns {string} Complete URL with token as query parameter
277
- *
278
- * @description
279
- * Builds URLs for XMTP tools endpoints using query parameter authentication.
280
- * The token is appended as a query parameter for GET request authentication,
281
- * following the pattern: `/xmtp-tools/{action}?token={token}`
282
- *
283
- * @example
284
- * ```typescript
285
- * const url = getXMTPToolsUrl(
286
- * "https://api.hybrid.dev",
287
- * "send",
288
- * "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
289
- * );
290
- * // Returns: "https://api.hybrid.dev/xmtp-tools/send?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
291
- * ```
292
- *
293
- * @example
294
- * ```typescript
295
- * // Using with API key
296
- * const url = getXMTPToolsUrl(
297
- * process.env.XMTP_BASE_URL,
298
- * "status",
299
- * process.env.XMTP_API_KEY
300
- * );
301
- * ```
302
- */
303
- export function getXMTPToolsUrl(
304
- baseUrl: string,
305
- action: string,
306
- token: string
307
- ): string {
308
- return `${baseUrl}/xmtp-tools/${action}?token=${token}`
309
- }