@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.
- package/CHANGELOG.md +15 -0
- package/README.md +3 -1
- package/dist/routing/EnhancedRouter.js +63 -0
- package/dist/utilities/ai/client.js +276 -0
- package/dist/utilities/ai/index.js +6 -0
- package/dist/utilities/analytics/index.js +6 -0
- package/dist/utilities/analytics/writer.js +226 -0
- package/dist/utilities/bindings/client.js +283 -0
- package/dist/utilities/bindings/index.js +6 -0
- package/dist/utilities/cache/index.js +9 -0
- package/dist/utilities/cache/leaderboard.js +52 -0
- package/dist/utilities/cache/rate-limiter.js +57 -0
- package/dist/utilities/cache/session.js +69 -0
- package/dist/utilities/cache/upstash.js +200 -0
- package/dist/utilities/durable-objects/base.js +200 -0
- package/dist/utilities/durable-objects/counter.js +117 -0
- package/dist/utilities/durable-objects/index.js +10 -0
- package/dist/utilities/durable-objects/rate-limiter.js +80 -0
- package/dist/utilities/durable-objects/session-store.js +126 -0
- package/dist/utilities/durable-objects/websocket-room.js +223 -0
- package/dist/utilities/email/handler.js +359 -0
- package/dist/utilities/email/index.js +6 -0
- package/dist/utilities/index.js +65 -0
- package/dist/utilities/kv/index.js +6 -0
- package/dist/utilities/kv/storage.js +268 -0
- package/dist/utilities/queues/consumer.js +188 -0
- package/dist/utilities/queues/index.js +7 -0
- package/dist/utilities/queues/producer.js +74 -0
- package/dist/utilities/scheduled/handler.js +276 -0
- package/dist/utilities/scheduled/index.js +6 -0
- package/dist/utilities/storage/index.js +6 -0
- package/dist/utilities/storage/r2.js +314 -0
- package/dist/utilities/vectorize/index.js +6 -0
- package/dist/utilities/vectorize/store.js +273 -0
- package/dist/utils/config/environment-var-normalizer.js +233 -0
- package/docs/CHANGELOG.md +1877 -0
- package/docs/api-reference.md +153 -0
- 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,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';
|