@tamyla/clodo-framework 4.3.5 → 4.4.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.
Files changed (38) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +3 -1
  3. package/dist/routing/EnhancedRouter.js +63 -0
  4. package/dist/utilities/ai/client.js +276 -0
  5. package/dist/utilities/ai/index.js +6 -0
  6. package/dist/utilities/analytics/index.js +6 -0
  7. package/dist/utilities/analytics/writer.js +226 -0
  8. package/dist/utilities/bindings/client.js +283 -0
  9. package/dist/utilities/bindings/index.js +6 -0
  10. package/dist/utilities/cache/index.js +9 -0
  11. package/dist/utilities/cache/leaderboard.js +52 -0
  12. package/dist/utilities/cache/rate-limiter.js +57 -0
  13. package/dist/utilities/cache/session.js +69 -0
  14. package/dist/utilities/cache/upstash.js +200 -0
  15. package/dist/utilities/durable-objects/base.js +200 -0
  16. package/dist/utilities/durable-objects/counter.js +117 -0
  17. package/dist/utilities/durable-objects/index.js +10 -0
  18. package/dist/utilities/durable-objects/rate-limiter.js +80 -0
  19. package/dist/utilities/durable-objects/session-store.js +126 -0
  20. package/dist/utilities/durable-objects/websocket-room.js +223 -0
  21. package/dist/utilities/email/handler.js +359 -0
  22. package/dist/utilities/email/index.js +6 -0
  23. package/dist/utilities/index.js +65 -0
  24. package/dist/utilities/kv/index.js +6 -0
  25. package/dist/utilities/kv/storage.js +268 -0
  26. package/dist/utilities/queues/consumer.js +188 -0
  27. package/dist/utilities/queues/index.js +7 -0
  28. package/dist/utilities/queues/producer.js +74 -0
  29. package/dist/utilities/scheduled/handler.js +276 -0
  30. package/dist/utilities/scheduled/index.js +6 -0
  31. package/dist/utilities/storage/index.js +6 -0
  32. package/dist/utilities/storage/r2.js +314 -0
  33. package/dist/utilities/vectorize/index.js +6 -0
  34. package/dist/utilities/vectorize/store.js +273 -0
  35. package/dist/utils/config/environment-var-normalizer.js +233 -0
  36. package/docs/CHANGELOG.md +1877 -0
  37. package/docs/api-reference.md +153 -0
  38. package/package.json +14 -2
@@ -0,0 +1,223 @@
1
+ /**
2
+ * WebSocket Room Durable Object
3
+ * Manages real-time WebSocket connections for a room/channel
4
+ *
5
+ * @example
6
+ * const id = env.WEBSOCKET_ROOM.idFromName('room-123');
7
+ * const room = env.WEBSOCKET_ROOM.get(id);
8
+ * return room.fetch(request); // WebSocket upgrade
9
+ */
10
+
11
+ import { DurableObjectBase } from './base.js';
12
+ export class WebSocketRoom extends DurableObjectBase {
13
+ constructor(state, env) {
14
+ super(state, env);
15
+ this.sessions = new Map();
16
+ }
17
+ async fetch(request) {
18
+ await this.ensureInitialized();
19
+ const url = new URL(request.url);
20
+
21
+ // Handle WebSocket upgrade
22
+ if (request.headers.get('Upgrade') === 'websocket') {
23
+ return this.handleWebSocket(request);
24
+ }
25
+
26
+ // HTTP API endpoints
27
+ const action = url.pathname.split('/').pop();
28
+ switch (action) {
29
+ case 'broadcast':
30
+ return this.handleBroadcast(request);
31
+ case 'members':
32
+ return this.getMembers();
33
+ case 'state':
34
+ return this.getRoomState();
35
+ default:
36
+ return this.error('Use WebSocket connection or API endpoints', 400);
37
+ }
38
+ }
39
+ async handleWebSocket(request) {
40
+ const url = new URL(request.url);
41
+ const userId = url.searchParams.get('userId') || crypto.randomUUID();
42
+ const username = url.searchParams.get('username') || `User-${userId.slice(0, 8)}`;
43
+
44
+ // WebSocketPair is a Cloudflare Workers global
45
+ // eslint-disable-next-line no-undef
46
+ const pair = new WebSocketPair();
47
+ const [client, server] = Object.values(pair);
48
+
49
+ // Accept the WebSocket
50
+ this.state.acceptWebSocket(server, [userId]);
51
+
52
+ // Create session
53
+ const session = {
54
+ id: userId,
55
+ username,
56
+ connectedAt: Date.now(),
57
+ lastActivity: Date.now()
58
+ };
59
+ this.sessions.set(userId, session);
60
+
61
+ // Notify others
62
+ this.broadcast({
63
+ type: 'user_joined',
64
+ user: {
65
+ id: userId,
66
+ username
67
+ },
68
+ memberCount: this.sessions.size
69
+ }, userId);
70
+
71
+ // Send welcome message
72
+ server.send(JSON.stringify({
73
+ type: 'connected',
74
+ userId,
75
+ username,
76
+ memberCount: this.sessions.size
77
+ }));
78
+ return new Response(null, {
79
+ status: 101,
80
+ webSocket: client
81
+ });
82
+ }
83
+ async webSocketMessage(ws, message) {
84
+ const [userId] = this.state.getTags(ws);
85
+ const session = this.sessions.get(userId);
86
+ if (!session) return;
87
+ session.lastActivity = Date.now();
88
+ try {
89
+ const data = JSON.parse(message);
90
+ switch (data.type) {
91
+ case 'message':
92
+ this.broadcast({
93
+ type: 'message',
94
+ from: {
95
+ id: userId,
96
+ username: session.username
97
+ },
98
+ content: data.content,
99
+ timestamp: Date.now()
100
+ });
101
+ break;
102
+ case 'typing':
103
+ this.broadcast({
104
+ type: 'typing',
105
+ user: {
106
+ id: userId,
107
+ username: session.username
108
+ }
109
+ }, userId);
110
+ break;
111
+ case 'presence':
112
+ session.status = data.status;
113
+ this.broadcast({
114
+ type: 'presence_update',
115
+ user: {
116
+ id: userId,
117
+ username: session.username
118
+ },
119
+ status: data.status
120
+ }, userId);
121
+ break;
122
+ case 'sync_state':
123
+ // Sync shared state
124
+ if (data.state) {
125
+ await this.setState('sharedState', data.state);
126
+ this.broadcast({
127
+ type: 'state_sync',
128
+ state: data.state,
129
+ updatedBy: userId
130
+ }, userId);
131
+ }
132
+ break;
133
+ case 'get_state':
134
+ {
135
+ const state = await this.getState('sharedState', {});
136
+ ws.send(JSON.stringify({
137
+ type: 'state_sync',
138
+ state
139
+ }));
140
+ break;
141
+ }
142
+ default:
143
+ // Forward custom message types
144
+ this.broadcast({
145
+ ...data,
146
+ from: userId
147
+ }, userId);
148
+ }
149
+ } catch (error) {
150
+ ws.send(JSON.stringify({
151
+ type: 'error',
152
+ message: 'Invalid message format'
153
+ }));
154
+ }
155
+ }
156
+ async webSocketClose(ws, code, reason) {
157
+ const [userId] = this.state.getTags(ws);
158
+ const session = this.sessions.get(userId);
159
+ if (session) {
160
+ this.sessions.delete(userId);
161
+ this.broadcast({
162
+ type: 'user_left',
163
+ user: {
164
+ id: userId,
165
+ username: session.username
166
+ },
167
+ memberCount: this.sessions.size
168
+ });
169
+ }
170
+ }
171
+ async webSocketError(ws, error) {
172
+ const [userId] = this.state.getTags(ws);
173
+ console.error(`WebSocket error for ${userId}:`, error);
174
+ ws.close(1011, 'Internal error');
175
+ }
176
+
177
+ /**
178
+ * Broadcast message to all connected clients
179
+ * @param {Object} message - Message to broadcast
180
+ * @param {string} excludeUserId - Optional user ID to exclude
181
+ */
182
+ broadcast(message, excludeUserId = null) {
183
+ const messageStr = JSON.stringify(message);
184
+ for (const ws of this.state.getWebSockets()) {
185
+ const [userId] = this.state.getTags(ws);
186
+ if (excludeUserId && userId === excludeUserId) continue;
187
+ try {
188
+ ws.send(messageStr);
189
+ } catch (error) {
190
+ // Socket might be closed
191
+ }
192
+ }
193
+ }
194
+ async handleBroadcast(request) {
195
+ const body = await request.json();
196
+ this.broadcast(body);
197
+ return this.json({
198
+ success: true,
199
+ recipients: this.sessions.size
200
+ });
201
+ }
202
+ async getMembers() {
203
+ const members = Array.from(this.sessions.values()).map(s => ({
204
+ id: s.id,
205
+ username: s.username,
206
+ connectedAt: s.connectedAt,
207
+ status: s.status
208
+ }));
209
+ return this.json({
210
+ members,
211
+ count: members.length
212
+ });
213
+ }
214
+ async getRoomState() {
215
+ const sharedState = await this.getState('sharedState', {});
216
+ return this.json({
217
+ memberCount: this.sessions.size,
218
+ members: Array.from(this.sessions.keys()),
219
+ sharedState
220
+ });
221
+ }
222
+ }
223
+ export default WebSocketRoom;
@@ -0,0 +1,359 @@
1
+ /**
2
+ * Email Workers Utilities
3
+ * Handle incoming emails with Email Workers
4
+ *
5
+ * @example
6
+ * import { EmailHandler, EmailParser } from '@tamyla/clodo-framework/utilities/email';
7
+ *
8
+ * export default {
9
+ * async email(message, env) {
10
+ * const handler = new EmailHandler(message, env);
11
+ *
12
+ * // Parse email
13
+ * const parsed = await handler.parse();
14
+ * console.log('From:', parsed.from);
15
+ * console.log('Subject:', parsed.subject);
16
+ *
17
+ * // Forward with modifications
18
+ * await handler.forward('forward@example.com', {
19
+ * headers: { 'X-Processed': 'true' }
20
+ * });
21
+ * }
22
+ * }
23
+ */
24
+
25
+ /**
26
+ * Email Handler for Email Workers
27
+ */
28
+ export class EmailHandler {
29
+ /**
30
+ * @param {EmailMessage} message - Email message from worker
31
+ * @param {Object} env - Environment bindings
32
+ */
33
+ constructor(message, env) {
34
+ this.message = message;
35
+ this.env = env;
36
+ this._parsed = null;
37
+ }
38
+
39
+ /**
40
+ * Get sender address
41
+ */
42
+ get from() {
43
+ return this.message.from;
44
+ }
45
+
46
+ /**
47
+ * Get recipient address
48
+ */
49
+ get to() {
50
+ return this.message.to;
51
+ }
52
+
53
+ /**
54
+ * Get raw email size in bytes
55
+ */
56
+ get size() {
57
+ return this.message.rawSize;
58
+ }
59
+
60
+ /**
61
+ * Get email headers
62
+ */
63
+ get headers() {
64
+ return this.message.headers;
65
+ }
66
+
67
+ /**
68
+ * Parse email content
69
+ * @returns {Promise<Object>} Parsed email
70
+ */
71
+ async parse() {
72
+ if (this._parsed) return this._parsed;
73
+ const parser = new EmailParser();
74
+ this._parsed = await parser.parse(this.message);
75
+ return this._parsed;
76
+ }
77
+
78
+ /**
79
+ * Forward email to another address
80
+ * @param {string} to - Destination address
81
+ * @param {Object} options - Forward options
82
+ */
83
+ async forward(to, options = {}) {
84
+ const headers = new Headers(this.message.headers);
85
+
86
+ // Add custom headers
87
+ if (options.headers) {
88
+ Object.entries(options.headers).forEach(([key, value]) => {
89
+ headers.set(key, value);
90
+ });
91
+ }
92
+ await this.message.forward(to, headers);
93
+ }
94
+
95
+ /**
96
+ * Reply to the email (requires send capability)
97
+ * @param {Object} options - Reply options
98
+ */
99
+ async reply(options = {}) {
100
+ if (!this.env.SEND_EMAIL) {
101
+ throw new Error('SEND_EMAIL binding required for replies');
102
+ }
103
+ const parsed = await this.parse();
104
+ const email = new EmailBuilder().from(options.from || this.to).to(this.from).subject(`Re: ${parsed.subject}`).text(options.text || '').html(options.html);
105
+
106
+ // Add In-Reply-To header
107
+ if (parsed.messageId) {
108
+ email.header('In-Reply-To', parsed.messageId);
109
+ email.header('References', parsed.messageId);
110
+ }
111
+ await this.env.SEND_EMAIL.send(email.build());
112
+ }
113
+
114
+ /**
115
+ * Reject the email
116
+ * @param {string} reason - Rejection reason
117
+ */
118
+ async reject(reason = 'Message rejected') {
119
+ await this.message.setReject(reason);
120
+ }
121
+
122
+ /**
123
+ * Get raw email content
124
+ * @returns {Promise<ReadableStream>}
125
+ */
126
+ async raw() {
127
+ return this.message.raw;
128
+ }
129
+
130
+ /**
131
+ * Get raw email as text
132
+ * @returns {Promise<string>}
133
+ */
134
+ async rawText() {
135
+ const raw = await this.raw();
136
+ const reader = raw.getReader();
137
+ const chunks = [];
138
+
139
+ // eslint-disable-next-line no-constant-condition
140
+ while (true) {
141
+ const {
142
+ done,
143
+ value
144
+ } = await reader.read();
145
+ if (done) break;
146
+ chunks.push(value);
147
+ }
148
+ return new TextDecoder().decode(new Uint8Array(chunks.flatMap(c => [...c])));
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Email Parser
154
+ */
155
+ export class EmailParser {
156
+ /**
157
+ * Parse an email message
158
+ * @param {EmailMessage} message
159
+ * @returns {Promise<Object>}
160
+ */
161
+ async parse(message) {
162
+ const headers = message.headers;
163
+ const rawText = await this._getRawText(message);
164
+
165
+ // Parse headers
166
+ const subject = headers.get('subject') || '';
167
+ const from = this._parseAddress(headers.get('from') || message.from);
168
+ const to = this._parseAddresses(headers.get('to') || message.to);
169
+ const cc = this._parseAddresses(headers.get('cc') || '');
170
+ const date = new Date(headers.get('date') || Date.now());
171
+ const messageId = headers.get('message-id');
172
+
173
+ // Parse body
174
+ const {
175
+ text,
176
+ html,
177
+ attachments
178
+ } = this._parseBody(rawText, headers);
179
+ return {
180
+ messageId,
181
+ from,
182
+ to,
183
+ cc,
184
+ subject,
185
+ date,
186
+ text,
187
+ html,
188
+ attachments,
189
+ headers: Object.fromEntries(headers.entries())
190
+ };
191
+ }
192
+ async _getRawText(message) {
193
+ const raw = await message.raw;
194
+ const reader = raw.getReader();
195
+ const chunks = [];
196
+
197
+ // eslint-disable-next-line no-constant-condition
198
+ while (true) {
199
+ const {
200
+ done,
201
+ value
202
+ } = await reader.read();
203
+ if (done) break;
204
+ chunks.push(value);
205
+ }
206
+ return new TextDecoder().decode(new Uint8Array(chunks.flatMap(c => [...c])));
207
+ }
208
+ _parseAddress(address) {
209
+ const match = address.match(/(?:"?([^"]*)"?\s)?<?([^>]+@[^>]+)>?/);
210
+ if (match) {
211
+ return {
212
+ name: match[1]?.trim() || '',
213
+ email: match[2].trim()
214
+ };
215
+ }
216
+ return {
217
+ name: '',
218
+ email: address.trim()
219
+ };
220
+ }
221
+ _parseAddresses(addresses) {
222
+ if (!addresses) return [];
223
+ return addresses.split(',').map(a => this._parseAddress(a.trim()));
224
+ }
225
+ _parseBody(rawText, headers) {
226
+ const contentType = headers.get('content-type') || 'text/plain';
227
+ const result = {
228
+ text: '',
229
+ html: '',
230
+ attachments: []
231
+ };
232
+
233
+ // Simple parsing - for complex emails, use a proper MIME parser
234
+ if (contentType.includes('multipart')) {
235
+ const boundary = this._getBoundary(contentType);
236
+ if (boundary) {
237
+ const parts = rawText.split(`--${boundary}`);
238
+ for (const part of parts) {
239
+ if (part.includes('Content-Type: text/plain')) {
240
+ result.text = this._extractContent(part);
241
+ } else if (part.includes('Content-Type: text/html')) {
242
+ result.html = this._extractContent(part);
243
+ } else if (part.includes('Content-Disposition: attachment')) {
244
+ result.attachments.push(this._parseAttachment(part));
245
+ }
246
+ }
247
+ }
248
+ } else if (contentType.includes('text/html')) {
249
+ result.html = rawText.split('\r\n\r\n').slice(1).join('\r\n\r\n');
250
+ } else {
251
+ result.text = rawText.split('\r\n\r\n').slice(1).join('\r\n\r\n');
252
+ }
253
+ return result;
254
+ }
255
+ _getBoundary(contentType) {
256
+ const match = contentType.match(/boundary=["']?([^"'\s;]+)["']?/);
257
+ return match ? match[1] : null;
258
+ }
259
+ _extractContent(part) {
260
+ const lines = part.split('\r\n');
261
+ const bodyStart = lines.findIndex(l => l === '') + 1;
262
+ return lines.slice(bodyStart).join('\r\n').trim();
263
+ }
264
+ _parseAttachment(part) {
265
+ const filenameMatch = part.match(/filename=["']?([^"'\r\n]+)["']?/);
266
+ const contentTypeMatch = part.match(/Content-Type:\s*([^\r\n;]+)/);
267
+ return {
268
+ filename: filenameMatch ? filenameMatch[1] : 'attachment',
269
+ contentType: contentTypeMatch ? contentTypeMatch[1].trim() : 'application/octet-stream',
270
+ content: this._extractContent(part)
271
+ };
272
+ }
273
+ }
274
+
275
+ /**
276
+ * Email Builder for sending emails
277
+ */
278
+ export class EmailBuilder {
279
+ constructor() {
280
+ this._from = '';
281
+ this._to = [];
282
+ this._cc = [];
283
+ this._bcc = [];
284
+ this._subject = '';
285
+ this._text = '';
286
+ this._html = '';
287
+ this._headers = new Map();
288
+ this._attachments = [];
289
+ }
290
+ from(address) {
291
+ this._from = address;
292
+ return this;
293
+ }
294
+ to(address) {
295
+ this._to = Array.isArray(address) ? address : [address];
296
+ return this;
297
+ }
298
+ cc(address) {
299
+ this._cc = Array.isArray(address) ? address : [address];
300
+ return this;
301
+ }
302
+ bcc(address) {
303
+ this._bcc = Array.isArray(address) ? address : [address];
304
+ return this;
305
+ }
306
+ subject(subject) {
307
+ this._subject = subject;
308
+ return this;
309
+ }
310
+ text(text) {
311
+ this._text = text;
312
+ return this;
313
+ }
314
+ html(html) {
315
+ this._html = html;
316
+ return this;
317
+ }
318
+ header(key, value) {
319
+ this._headers.set(key, value);
320
+ return this;
321
+ }
322
+ attachment(filename, content, contentType = 'application/octet-stream') {
323
+ this._attachments.push({
324
+ filename,
325
+ content,
326
+ contentType
327
+ });
328
+ return this;
329
+ }
330
+ build() {
331
+ return {
332
+ personalizations: [{
333
+ to: this._to.map(email => ({
334
+ email
335
+ })),
336
+ cc: this._cc.length ? this._cc.map(email => ({
337
+ email
338
+ })) : undefined,
339
+ bcc: this._bcc.length ? this._bcc.map(email => ({
340
+ email
341
+ })) : undefined
342
+ }],
343
+ from: {
344
+ email: this._from
345
+ },
346
+ subject: this._subject,
347
+ content: [this._text ? {
348
+ type: 'text/plain',
349
+ value: this._text
350
+ } : null, this._html ? {
351
+ type: 'text/html',
352
+ value: this._html
353
+ } : null].filter(Boolean),
354
+ headers: Object.fromEntries(this._headers),
355
+ attachments: this._attachments.length ? this._attachments : undefined
356
+ };
357
+ }
358
+ }
359
+ export default EmailHandler;
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Email Utilities
3
+ * @module @tamyla/clodo-framework/utilities/email
4
+ */
5
+
6
+ export { EmailHandler, EmailParser, EmailBuilder } from './handler.js';
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Clodo Framework Utilities
3
+ *
4
+ * Runtime utilities for Cloudflare Workers services.
5
+ * All utilities are Worker-compatible (no Node.js dependencies).
6
+ *
7
+ * @module @tamyla/clodo-framework/utilities
8
+ */
9
+
10
+ // ============================================================
11
+ // STORAGE UTILITIES
12
+ // ============================================================
13
+
14
+ // R2 Storage
15
+ export { R2Storage, handleFileUpload, serveFile } from './storage/index.js';
16
+
17
+ // KV Storage
18
+ export { KVStorage, KVCache, KVWithMetadata } from './kv/index.js';
19
+
20
+ // ============================================================
21
+ // COMPUTE UTILITIES
22
+ // ============================================================
23
+
24
+ // Durable Objects
25
+ export { DurableObjectBase, RateLimiter, SessionStore, Counter, WebSocketRoom } from './durable-objects/index.js';
26
+
27
+ // Queues
28
+ export { QueueProducer, QueueConsumer, MessageBuilder, createMessage, MessageTypes } from './queues/index.js';
29
+
30
+ // Scheduled/Cron
31
+ export { ScheduledHandler, CronJob, JobScheduler, ScheduledJobRegistry } from './scheduled/index.js';
32
+
33
+ // ============================================================
34
+ // AI & DATA UTILITIES
35
+ // ============================================================
36
+
37
+ // Workers AI
38
+ export { AIClient, Models, createSSEStream, streamResponse } from './ai/index.js';
39
+
40
+ // Vectorize (Vector Database)
41
+ export { VectorStore, VectorSearch, EmbeddingHelper } from './vectorize/index.js';
42
+
43
+ // ============================================================
44
+ // CACHE & DATA UTILITIES
45
+ // ============================================================
46
+
47
+ // Upstash Redis
48
+ export { UpstashRedis, UpstashCache, SessionManager, RateLimiter as RedisRateLimiter, Leaderboard } from './cache/index.js';
49
+
50
+ // ============================================================
51
+ // COMMUNICATION UTILITIES
52
+ // ============================================================
53
+
54
+ // Email Workers
55
+ export { EmailHandler, EmailParser, EmailBuilder } from './email/index.js';
56
+
57
+ // Service Bindings (inter-service communication)
58
+ export { ServiceBindingClient, RPCClient, ServiceRouter } from './bindings/index.js';
59
+
60
+ // ============================================================
61
+ // OBSERVABILITY UTILITIES
62
+ // ============================================================
63
+
64
+ // Analytics Engine
65
+ export { AnalyticsWriter, EventTracker, MetricsCollector } from './analytics/index.js';
@@ -0,0 +1,6 @@
1
+ /**
2
+ * KV Storage Utilities
3
+ * @module @tamyla/clodo-framework/utilities/kv
4
+ */
5
+
6
+ export { KVStorage, KVCache, KVWithMetadata } from './storage.js';