Repository navigation
Expand file tree
/
Copy pathworker.js
More file actions
168 lines (140 loc) · 4.62 KB
/
Copy pathworker.js
File metadata and controls
168 lines (140 loc) · 4.62 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
/*
* Muse AI relay worker with optional image attachments.
*
* Bind a KV namespace named MESSAGES to persist relay messages.
* The existing deployment's authentication and storage logic should be
* merged into this handler if your relay already has additional behavior.
*/
const MAX_IMAGE_BYTES = 2 * 1024 * 1024;
const MAX_MESSAGES = 1000;
const MESSAGE_KEY = "messages";
export default {
async fetch(request, env) {
const url = new URL(request.url);
if (request.method === "OPTIONS") {
return new Response(null, { status: 204, headers: corsHeaders() });
}
try {
if (request.method === "POST" && url.pathname === "/send") {
return withCors(await sendMessage(request, env));
}
if (request.method === "GET" && url.pathname === "/inbox") {
return withCors(await getInbox(request, env));
}
return withCors(json({ error: "Not found" }, 404));
} catch (error) {
console.error(error);
return withCors(json({ error: "Internal server error" }, 500));
}
},
};
async function sendMessage(request, env) {
let payload;
try {
payload = await request.json();
} catch {
return json({ error: "Request body must be valid JSON" }, 400);
}
if (!payload || typeof payload !== "object") {
return json({ error: "Request body must be a JSON object" }, 400);
}
const message = {
...payload,
image: validateImage(payload.image),
timestamp: payload.timestamp || new Date().toISOString(),
};
const messages = await readMessages(env);
messages.push(message);
const trimmed = messages.slice(-MAX_MESSAGES);
await writeMessages(env, trimmed);
return json({ ok: true, message }, 200);
}
async function getInbox(request, env) {
const url = new URL(request.url);
const messages = await readMessages(env);
const since = url.searchParams.get("since");
const filtered = since
? messages.filter((message) => String(message.timestamp || "") > since)
: messages;
return json({ messages: filtered });
}
function validateImage(image) {
if (image === undefined || image === null || image === "") {
return undefined;
}
if (typeof image !== "string") {
throw httpError("image must be a string", 400);
}
if (image.startsWith("data:")) {
if (!/^data:image\/[a-z0-9.+-]+;base64,[A-Za-z0-9+/=]+$/i.test(image)) {
throw httpError("image must be a base64 image data URL", 400);
}
const comma = image.indexOf(",");
const base64 = image.slice(comma + 1);
const byteLength = Math.floor((base64.length * 3) / 4) -
(base64.endsWith("==") ? 2 : base64.endsWith("=") ? 1 : 0);
if (byteLength > MAX_IMAGE_BYTES) {
throw httpError("image exceeds the 2 MB limit", 413);
}
return image;
}
let parsed;
try {
parsed = new URL(image);
} catch {
throw httpError("image must be a data URL or HTTPS URL", 400);
}
if (parsed.protocol !== "https:") {
throw httpError("image URLs must use HTTPS", 400);
}
// A remote URL is not downloaded by the worker. Its referenced content
// size cannot be measured here, so clients should enforce the local cap.
return image;
}
async function readMessages(env) {
if (!env.MESSAGES || typeof env.MESSAGES.get !== "function") {
return [];
}
return (await env.MESSAGES.get(MESSAGE_KEY, "json")) || [];
}
async function writeMessages(env, messages) {
if (!env.MESSAGES || typeof env.MESSAGES.put !== "function") {
throw new Error("Missing MESSAGES KV binding");
}
await env.MESSAGES.put(MESSAGE_KEY, JSON.stringify(messages));
}
function httpError(message, status) {
const error = new Error(message);
error.status = status;
return error;
}
function json(body, status = 200) {
return new Response(JSON.stringify(body), {
status,
headers: { "content-type": "application/json; charset=utf-8" },
});
}
function withCors(response) {
const headers = new Headers(response.headers);
for (const [key, value] of Object.entries(corsHeaders())) headers.set(key, value);
return new Response(response.body, { status: response.status, headers });
}
function corsHeaders() {
return {
"access-control-allow-origin": "*",
"access-control-allow-methods": "GET,POST,OPTIONS",
"access-control-allow-headers": "content-type,authorization",
};
}
// Convert validation failures into their intended HTTP status.
const originalSendMessage = sendMessage;
sendMessage = async function wrappedSendMessage(request, env) {
try {
return await originalSendMessage(request, env);
} catch (error) {
if (error && Number.isInteger(error.status)) {
return json({ error: error.message }, error.status);
}
throw error;
}
};