This repository was archived by the owner on Mar 18, 2026. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathchainhooks-do.ts
More file actions
219 lines (184 loc) · 6.03 KB
/
chainhooks-do.ts
File metadata and controls
219 lines (184 loc) · 6.03 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
import { DurableObject } from 'cloudflare:workers';
import { Env } from '../../worker-configuration';
import { AppConfig } from '../config';
import { handleRequest } from '../utils/request-handler-util';
import { ApiError } from '../utils/api-error-util';
import { ErrorCode } from '../utils/error-catalog-util';
import { Logger } from '../utils/logger-util';
export class ChainhooksDO extends DurableObject<Env> {
// Configuration constants
private readonly BASE_PATH: string = '/chainhooks';
private readonly CACHE_PREFIX: string = this.BASE_PATH.replaceAll('/', '');
private readonly SUPPORTED_ENDPOINTS: string[] = ['/post-event', '/events', '/events/:id'];
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
this.ctx = ctx;
this.env = env;
// Initialize AppConfig with environment
const config = AppConfig.getInstance(env).getConfig();
}
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
const path = url.pathname;
const method = request.method;
return handleRequest(
async () => {
if (!path.startsWith(this.BASE_PATH)) {
throw new ApiError(ErrorCode.NOT_FOUND, { resource: path });
}
// Remove base path to get the endpoint
const endpoint = path.replace(this.BASE_PATH, '');
// Handle root path
if (endpoint === '' || endpoint === '/') {
return {
message: `Supported endpoints: ${this.SUPPORTED_ENDPOINTS.join(', ')}`,
};
}
// Handle post-event endpoint
if (endpoint === '/post-event') {
if (method !== 'POST') {
throw new ApiError(ErrorCode.INVALID_REQUEST, {
reason: `Method ${method} not allowed for this endpoint. Use POST.`,
});
}
// Check authentication
if (!this.validateAuthToken(request)) {
throw new ApiError(ErrorCode.UNAUTHORIZED, {
reason: 'Invalid or missing authentication token',
});
}
return await this.handlePostEvent(request);
}
// Handle get all events endpoint
if (endpoint === '/events') {
if (method !== 'GET') {
throw new ApiError(ErrorCode.INVALID_REQUEST, {
reason: `Method ${method} not allowed for this endpoint. Use GET.`,
});
}
return await this.handleGetAllEvents();
}
// Handle get specific event endpoint
if (endpoint.startsWith('/events/')) {
if (method !== 'GET') {
throw new ApiError(ErrorCode.INVALID_REQUEST, {
reason: `Method ${method} not allowed for this endpoint. Use GET.`,
});
}
const eventId = endpoint.replace('/events/', '');
return await this.handleGetEvent(eventId);
}
// If we get here, the endpoint is not supported
throw new ApiError(ErrorCode.NOT_FOUND, {
resource: endpoint,
supportedEndpoints: this.SUPPORTED_ENDPOINTS,
});
},
this.env,
{
path,
method,
}
);
}
private async handlePostEvent(request: Request): Promise<any> {
const logger = Logger.getInstance(this.env);
try {
// Clone the request to read the body
const clonedRequest = request.clone();
// Try to parse as JSON first
let body;
try {
body = await clonedRequest.json();
} catch (e) {
// If JSON parsing fails, get the body as text
body = await request.text();
}
// Log the received event
logger.info('Received chainhook event', {
body,
headers: Object.fromEntries(request.headers.entries()),
});
// Store the event in Durable Object storage for later analysis
const eventId = crypto.randomUUID();
await this.ctx.storage.put(`event_${eventId}`, {
timestamp: new Date().toISOString(),
body,
headers: Object.fromEntries(request.headers.entries()),
});
return {
message: 'Event received and logged successfully',
eventId,
};
} catch (error) {
logger.error('Error processing chainhook event', error instanceof Error ? error : new Error(String(error)));
throw new ApiError(ErrorCode.INTERNAL_ERROR, {
reason: 'Failed to process chainhook event',
});
}
}
private async handleGetEvent(eventId: string): Promise<any> {
const logger = Logger.getInstance(this.env);
try {
// Retrieve the event from storage
const event = await this.ctx.storage.get(`event_${eventId}`);
if (!event) {
throw new ApiError(ErrorCode.NOT_FOUND, {
resource: `Event with ID ${eventId}`,
});
}
return {
event,
};
} catch (error) {
if (error instanceof ApiError) {
throw error;
}
logger.error(`Error retrieving event ${eventId}`, error instanceof Error ? error : new Error(String(error)));
throw new ApiError(ErrorCode.INTERNAL_ERROR, {
reason: `Failed to retrieve event ${eventId}`,
});
}
}
private async handleGetAllEvents(): Promise<any> {
const logger = Logger.getInstance(this.env);
try {
// Get all keys that start with "event_"
const eventKeys = await this.ctx.storage.list({ prefix: 'event_' });
// Create an array to hold all events
const events: Record<string, any> = {};
// Retrieve each event and add it to the array
for (const [key, value] of eventKeys) {
const eventId = key.replace('event_', '');
events[eventId] = value;
}
return {
events,
count: Object.keys(events).length,
};
} catch (error) {
logger.error('Error retrieving all events', error instanceof Error ? error : new Error(String(error)));
throw new ApiError(ErrorCode.INTERNAL_ERROR, {
reason: 'Failed to retrieve events',
});
}
}
/**
* Validates the authentication token from the request
*
* @param request - The incoming request
* @returns boolean indicating if the token is valid
*/
private validateAuthToken(request: Request): boolean {
// Extract the Authorization header
const authHeader = request.headers.get('Authorization');
// Check if the header exists and has the correct format
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return false;
}
// Extract the token
const token = authHeader.replace('Bearer ', '');
// Compare with the stored token
return token === this.env.CHAINHOOKS_AUTH_TOKEN;
}
}