emulate 0.3.0 → 0.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/README.md +324 -8
- package/dist/api.d.ts +1 -1
- package/dist/api.js +151 -6
- package/dist/api.js.map +1 -1
- package/dist/{dist-BKXG6HVH.js → dist-6EW7SSOZ.js} +1 -1
- package/dist/dist-6EW7SSOZ.js.map +1 -0
- package/dist/{dist-O4KFIBVU.js → dist-6JFNJPUU.js} +1 -1
- package/dist/dist-6JFNJPUU.js.map +1 -0
- package/dist/dist-B674PYKV.js +961 -0
- package/dist/dist-B674PYKV.js.map +1 -0
- package/dist/{dist-UZSUUE3Y.js → dist-G7WQPZ3Y.js} +1 -1
- package/dist/dist-G7WQPZ3Y.js.map +1 -0
- package/dist/{dist-OCDKIMRJ.js → dist-H6JYGQM4.js} +1 -1
- package/dist/dist-H6JYGQM4.js.map +1 -0
- package/dist/dist-OTJZRQ3Q.js +1956 -0
- package/dist/dist-OTJZRQ3Q.js.map +1 -0
- package/dist/dist-QMOJM6DV.js +774 -0
- package/dist/dist-QMOJM6DV.js.map +1 -0
- package/dist/{dist-JYDZIVC6.js → dist-RDFBZ5O6.js} +1 -1
- package/dist/dist-RDFBZ5O6.js.map +1 -0
- package/dist/{dist-DSSB3LYT.js → dist-RMK3BS5M.js} +56 -1
- package/dist/dist-RMK3BS5M.js.map +1 -0
- package/dist/dist-YOVM5HEY.js +932 -0
- package/dist/dist-YOVM5HEY.js.map +1 -0
- package/dist/index.js +154 -9
- package/dist/index.js.map +1 -1
- package/package.json +13 -9
- package/dist/dist-BKXG6HVH.js.map +0 -1
- package/dist/dist-DSSB3LYT.js.map +0 -1
- package/dist/dist-JYDZIVC6.js.map +0 -1
- package/dist/dist-O4KFIBVU.js.map +0 -1
- package/dist/dist-OCDKIMRJ.js.map +0 -1
- package/dist/dist-UZSUUE3Y.js.map +0 -1
|
@@ -0,0 +1,774 @@
|
|
|
1
|
+
import "./chunk-TEPNEZ63.js";
|
|
2
|
+
|
|
3
|
+
// ../@emulators/resend/dist/index.js
|
|
4
|
+
import { randomUUID } from "crypto";
|
|
5
|
+
import { randomBytes } from "crypto";
|
|
6
|
+
import { readFileSync } from "fs";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
8
|
+
import { dirname, join } from "path";
|
|
9
|
+
function getResendStore(store) {
|
|
10
|
+
return {
|
|
11
|
+
emails: store.collection("resend.emails", ["uuid"]),
|
|
12
|
+
domains: store.collection("resend.domains", ["uuid", "name"]),
|
|
13
|
+
apiKeys: store.collection("resend.api_keys", ["uuid"]),
|
|
14
|
+
audiences: store.collection("resend.audiences", ["uuid"]),
|
|
15
|
+
contacts: store.collection("resend.contacts", ["uuid", "audience_id"])
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function generateUuid() {
|
|
19
|
+
return randomUUID();
|
|
20
|
+
}
|
|
21
|
+
function resendError(c, statusCode, name, message) {
|
|
22
|
+
return c.json({ statusCode, name, message }, statusCode);
|
|
23
|
+
}
|
|
24
|
+
function resendList(data) {
|
|
25
|
+
return { object: "list", data };
|
|
26
|
+
}
|
|
27
|
+
async function parseResendBody(c) {
|
|
28
|
+
const contentType = c.req.header("content-type") ?? "";
|
|
29
|
+
if (contentType.includes("application/x-www-form-urlencoded")) {
|
|
30
|
+
const text = await c.req.text();
|
|
31
|
+
const params = new URLSearchParams(text);
|
|
32
|
+
const result = {};
|
|
33
|
+
for (const [key, value] of params.entries()) {
|
|
34
|
+
result[key] = value;
|
|
35
|
+
}
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
const body = await c.req.json();
|
|
40
|
+
if (body && typeof body === "object" && !Array.isArray(body)) {
|
|
41
|
+
return body;
|
|
42
|
+
}
|
|
43
|
+
return {};
|
|
44
|
+
} catch {
|
|
45
|
+
return {};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function emailRoutes(ctx) {
|
|
49
|
+
const { app, store, webhooks } = ctx;
|
|
50
|
+
const rs = () => getResendStore(store);
|
|
51
|
+
app.post("/emails/batch", async (c) => {
|
|
52
|
+
let emails;
|
|
53
|
+
try {
|
|
54
|
+
const raw = await c.req.json();
|
|
55
|
+
if (!Array.isArray(raw)) {
|
|
56
|
+
return resendError(c, 422, "validation_error", "Request body must be an array");
|
|
57
|
+
}
|
|
58
|
+
emails = raw;
|
|
59
|
+
} catch {
|
|
60
|
+
return resendError(c, 422, "validation_error", "Request body must be an array");
|
|
61
|
+
}
|
|
62
|
+
if (emails.length > 100) {
|
|
63
|
+
return resendError(c, 422, "validation_error", "Batch size cannot exceed 100 emails");
|
|
64
|
+
}
|
|
65
|
+
for (const emailData of emails) {
|
|
66
|
+
if (!emailData.from) return resendError(c, 422, "validation_error", "Missing required field: from");
|
|
67
|
+
if (!emailData.to) return resendError(c, 422, "validation_error", "Missing required field: to");
|
|
68
|
+
if (!emailData.subject) return resendError(c, 422, "validation_error", "Missing required field: subject");
|
|
69
|
+
}
|
|
70
|
+
const results = [];
|
|
71
|
+
for (const emailData of emails) {
|
|
72
|
+
const from = emailData.from;
|
|
73
|
+
const to = emailData.to;
|
|
74
|
+
const subject = emailData.subject;
|
|
75
|
+
const toArray = Array.isArray(to) ? to : [to];
|
|
76
|
+
const uuid = generateUuid();
|
|
77
|
+
const scheduledAt = emailData.scheduled_at;
|
|
78
|
+
const status = scheduledAt ? "scheduled" : "delivered";
|
|
79
|
+
rs().emails.insert({
|
|
80
|
+
uuid,
|
|
81
|
+
from,
|
|
82
|
+
to: toArray,
|
|
83
|
+
subject,
|
|
84
|
+
html: emailData.html ?? null,
|
|
85
|
+
text: emailData.text ?? null,
|
|
86
|
+
cc: normalizeStringArray(emailData.cc),
|
|
87
|
+
bcc: normalizeStringArray(emailData.bcc),
|
|
88
|
+
reply_to: normalizeStringArray(emailData.reply_to),
|
|
89
|
+
headers: emailData.headers ?? {},
|
|
90
|
+
tags: emailData.tags ?? [],
|
|
91
|
+
status,
|
|
92
|
+
scheduled_at: scheduledAt ?? null,
|
|
93
|
+
last_event: status === "scheduled" ? "email.scheduled" : "email.delivered"
|
|
94
|
+
});
|
|
95
|
+
if (!scheduledAt) {
|
|
96
|
+
await webhooks.dispatch("email.sent", void 0, { type: "email.sent", data: { email_id: uuid, to: toArray, from, subject } }, "resend");
|
|
97
|
+
await webhooks.dispatch("email.delivered", void 0, { type: "email.delivered", data: { email_id: uuid, to: toArray, from, subject } }, "resend");
|
|
98
|
+
}
|
|
99
|
+
results.push({ id: uuid });
|
|
100
|
+
}
|
|
101
|
+
return c.json({ data: results }, 200);
|
|
102
|
+
});
|
|
103
|
+
app.post("/emails", async (c) => {
|
|
104
|
+
const body = await parseResendBody(c);
|
|
105
|
+
const from = body.from;
|
|
106
|
+
const to = body.to;
|
|
107
|
+
const subject = body.subject;
|
|
108
|
+
if (!from) return resendError(c, 422, "validation_error", "Missing required field: from");
|
|
109
|
+
if (!to) return resendError(c, 422, "validation_error", "Missing required field: to");
|
|
110
|
+
if (!subject) return resendError(c, 422, "validation_error", "Missing required field: subject");
|
|
111
|
+
const toArray = Array.isArray(to) ? to : [to];
|
|
112
|
+
const uuid = generateUuid();
|
|
113
|
+
const scheduledAt = body.scheduled_at;
|
|
114
|
+
const status = scheduledAt ? "scheduled" : "delivered";
|
|
115
|
+
rs().emails.insert({
|
|
116
|
+
uuid,
|
|
117
|
+
from,
|
|
118
|
+
to: toArray,
|
|
119
|
+
subject,
|
|
120
|
+
html: body.html ?? null,
|
|
121
|
+
text: body.text ?? null,
|
|
122
|
+
cc: normalizeStringArray(body.cc),
|
|
123
|
+
bcc: normalizeStringArray(body.bcc),
|
|
124
|
+
reply_to: normalizeStringArray(body.reply_to),
|
|
125
|
+
headers: body.headers ?? {},
|
|
126
|
+
tags: body.tags ?? [],
|
|
127
|
+
status,
|
|
128
|
+
scheduled_at: scheduledAt ?? null,
|
|
129
|
+
last_event: status === "scheduled" ? "email.scheduled" : "email.delivered"
|
|
130
|
+
});
|
|
131
|
+
if (!scheduledAt) {
|
|
132
|
+
await webhooks.dispatch("email.sent", void 0, { type: "email.sent", data: { email_id: uuid, to: toArray, from, subject } }, "resend");
|
|
133
|
+
await webhooks.dispatch("email.delivered", void 0, { type: "email.delivered", data: { email_id: uuid, to: toArray, from, subject } }, "resend");
|
|
134
|
+
}
|
|
135
|
+
return c.json({ id: uuid }, 200);
|
|
136
|
+
});
|
|
137
|
+
app.get("/emails", (c) => {
|
|
138
|
+
const allEmails = rs().emails.all();
|
|
139
|
+
return c.json(resendList(allEmails.map(formatEmail)));
|
|
140
|
+
});
|
|
141
|
+
app.get("/emails/:id", (c) => {
|
|
142
|
+
const id = c.req.param("id");
|
|
143
|
+
const email = rs().emails.findOneBy("uuid", id);
|
|
144
|
+
if (!email) return resendError(c, 404, "not_found", "Email not found");
|
|
145
|
+
return c.json(formatEmail(email));
|
|
146
|
+
});
|
|
147
|
+
app.post("/emails/:id/cancel", (c) => {
|
|
148
|
+
const id = c.req.param("id");
|
|
149
|
+
const email = rs().emails.findOneBy("uuid", id);
|
|
150
|
+
if (!email) return resendError(c, 404, "not_found", "Email not found");
|
|
151
|
+
if (email.status !== "scheduled") {
|
|
152
|
+
return resendError(c, 422, "validation_error", "Only scheduled emails can be canceled");
|
|
153
|
+
}
|
|
154
|
+
rs().emails.update(email.id, {
|
|
155
|
+
status: "canceled",
|
|
156
|
+
last_event: "email.canceled"
|
|
157
|
+
});
|
|
158
|
+
return c.json({ id: email.uuid, object: "email", canceled: true });
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
function normalizeStringArray(value) {
|
|
162
|
+
if (!value) return [];
|
|
163
|
+
if (Array.isArray(value)) return value.map(String);
|
|
164
|
+
if (typeof value === "string") return [value];
|
|
165
|
+
return [];
|
|
166
|
+
}
|
|
167
|
+
function formatEmail(email) {
|
|
168
|
+
return {
|
|
169
|
+
id: email.uuid,
|
|
170
|
+
object: "email",
|
|
171
|
+
from: email.from,
|
|
172
|
+
to: email.to,
|
|
173
|
+
subject: email.subject,
|
|
174
|
+
html: email.html,
|
|
175
|
+
text: email.text,
|
|
176
|
+
cc: email.cc,
|
|
177
|
+
bcc: email.bcc,
|
|
178
|
+
reply_to: email.reply_to,
|
|
179
|
+
headers: email.headers,
|
|
180
|
+
tags: email.tags,
|
|
181
|
+
status: email.status,
|
|
182
|
+
scheduled_at: email.scheduled_at,
|
|
183
|
+
last_event: email.last_event,
|
|
184
|
+
created_at: email.created_at
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function domainRoutes(ctx) {
|
|
188
|
+
const { app, store, webhooks } = ctx;
|
|
189
|
+
const rs = () => getResendStore(store);
|
|
190
|
+
app.post("/domains", async (c) => {
|
|
191
|
+
const body = await parseResendBody(c);
|
|
192
|
+
const name = body.name;
|
|
193
|
+
if (!name) return resendError(c, 422, "validation_error", "Missing required field: name");
|
|
194
|
+
const region = body.region ?? "us-east-1";
|
|
195
|
+
const uuid = generateUuid();
|
|
196
|
+
const records = [
|
|
197
|
+
{
|
|
198
|
+
record: "SPF",
|
|
199
|
+
name,
|
|
200
|
+
type: "MX",
|
|
201
|
+
ttl: "Auto",
|
|
202
|
+
status: "pending",
|
|
203
|
+
value: `feedback-smtp.${region}.amazonses.com`,
|
|
204
|
+
priority: 10
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
record: "SPF",
|
|
208
|
+
name,
|
|
209
|
+
type: "TXT",
|
|
210
|
+
ttl: "Auto",
|
|
211
|
+
status: "pending",
|
|
212
|
+
value: "v=spf1 include:amazonses.com ~all"
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
record: "DKIM",
|
|
216
|
+
name: `resend._domainkey.${name}`,
|
|
217
|
+
type: "CNAME",
|
|
218
|
+
ttl: "Auto",
|
|
219
|
+
status: "pending",
|
|
220
|
+
value: `resend.domainkey.${region}.amazonses.com`
|
|
221
|
+
}
|
|
222
|
+
];
|
|
223
|
+
const domain = rs().domains.insert({
|
|
224
|
+
uuid,
|
|
225
|
+
name,
|
|
226
|
+
status: "pending",
|
|
227
|
+
region,
|
|
228
|
+
records
|
|
229
|
+
});
|
|
230
|
+
await webhooks.dispatch("domain.created", void 0, { type: "domain.created", data: { id: uuid, name } }, "resend");
|
|
231
|
+
return c.json(formatDomain(domain), 200);
|
|
232
|
+
});
|
|
233
|
+
app.get("/domains", (c) => {
|
|
234
|
+
const allDomains = rs().domains.all();
|
|
235
|
+
return c.json(resendList(allDomains.map(formatDomain)));
|
|
236
|
+
});
|
|
237
|
+
app.get("/domains/:id", (c) => {
|
|
238
|
+
const id = c.req.param("id");
|
|
239
|
+
const domain = rs().domains.findOneBy("uuid", id);
|
|
240
|
+
if (!domain) return resendError(c, 404, "not_found", "Domain not found");
|
|
241
|
+
return c.json(formatDomain(domain));
|
|
242
|
+
});
|
|
243
|
+
app.delete("/domains/:id", async (c) => {
|
|
244
|
+
const id = c.req.param("id");
|
|
245
|
+
const domain = rs().domains.findOneBy("uuid", id);
|
|
246
|
+
if (!domain) return resendError(c, 404, "not_found", "Domain not found");
|
|
247
|
+
rs().domains.delete(domain.id);
|
|
248
|
+
await webhooks.dispatch("domain.deleted", void 0, { type: "domain.deleted", data: { id: domain.uuid, name: domain.name } }, "resend");
|
|
249
|
+
return c.json({ object: "domain", id: domain.uuid, deleted: true });
|
|
250
|
+
});
|
|
251
|
+
app.post("/domains/:id/verify", (c) => {
|
|
252
|
+
const id = c.req.param("id");
|
|
253
|
+
const domain = rs().domains.findOneBy("uuid", id);
|
|
254
|
+
if (!domain) return resendError(c, 404, "not_found", "Domain not found");
|
|
255
|
+
const verifiedRecords = domain.records.map((r) => ({ ...r, status: "verified" }));
|
|
256
|
+
rs().domains.update(domain.id, {
|
|
257
|
+
status: "verified",
|
|
258
|
+
records: verifiedRecords
|
|
259
|
+
});
|
|
260
|
+
return c.json({ object: "domain", id: domain.uuid, status: "verified" });
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
function formatDomain(domain) {
|
|
264
|
+
return {
|
|
265
|
+
id: domain.uuid,
|
|
266
|
+
object: "domain",
|
|
267
|
+
name: domain.name,
|
|
268
|
+
status: domain.status,
|
|
269
|
+
region: domain.region,
|
|
270
|
+
records: domain.records,
|
|
271
|
+
created_at: domain.created_at
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
function apiKeyRoutes(ctx) {
|
|
275
|
+
const { app, store } = ctx;
|
|
276
|
+
const rs = () => getResendStore(store);
|
|
277
|
+
app.post("/api-keys", async (c) => {
|
|
278
|
+
const body = await parseResendBody(c);
|
|
279
|
+
const name = body.name;
|
|
280
|
+
if (!name) return resendError(c, 422, "validation_error", "Missing required field: name");
|
|
281
|
+
const uuid = generateUuid();
|
|
282
|
+
const token = `re_${randomBytes(16).toString("hex")}`;
|
|
283
|
+
const apiKey = rs().apiKeys.insert({
|
|
284
|
+
uuid,
|
|
285
|
+
name,
|
|
286
|
+
token
|
|
287
|
+
});
|
|
288
|
+
return c.json({
|
|
289
|
+
id: apiKey.uuid,
|
|
290
|
+
token: apiKey.token
|
|
291
|
+
}, 200);
|
|
292
|
+
});
|
|
293
|
+
app.get("/api-keys", (c) => {
|
|
294
|
+
const allKeys = rs().apiKeys.all();
|
|
295
|
+
return c.json(resendList(allKeys.map((key) => ({
|
|
296
|
+
id: key.uuid,
|
|
297
|
+
name: key.name,
|
|
298
|
+
created_at: key.created_at
|
|
299
|
+
}))));
|
|
300
|
+
});
|
|
301
|
+
app.delete("/api-keys/:id", (c) => {
|
|
302
|
+
const id = c.req.param("id");
|
|
303
|
+
const apiKey = rs().apiKeys.findOneBy("uuid", id);
|
|
304
|
+
if (!apiKey) return resendError(c, 404, "not_found", "API key not found");
|
|
305
|
+
rs().apiKeys.delete(apiKey.id);
|
|
306
|
+
return c.json({ deleted: true });
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
function contactRoutes(ctx) {
|
|
310
|
+
const { app, store, webhooks } = ctx;
|
|
311
|
+
const rs = () => getResendStore(store);
|
|
312
|
+
app.post("/audiences", async (c) => {
|
|
313
|
+
const body = await parseResendBody(c);
|
|
314
|
+
const name = body.name;
|
|
315
|
+
if (!name) return resendError(c, 422, "validation_error", "Missing required field: name");
|
|
316
|
+
const uuid = generateUuid();
|
|
317
|
+
const audience = rs().audiences.insert({ uuid, name });
|
|
318
|
+
return c.json({
|
|
319
|
+
id: audience.uuid,
|
|
320
|
+
object: "audience",
|
|
321
|
+
name: audience.name,
|
|
322
|
+
created_at: audience.created_at
|
|
323
|
+
}, 200);
|
|
324
|
+
});
|
|
325
|
+
app.get("/audiences", (c) => {
|
|
326
|
+
const allAudiences = rs().audiences.all();
|
|
327
|
+
return c.json(resendList(allAudiences.map((a) => ({
|
|
328
|
+
id: a.uuid,
|
|
329
|
+
object: "audience",
|
|
330
|
+
name: a.name,
|
|
331
|
+
created_at: a.created_at
|
|
332
|
+
}))));
|
|
333
|
+
});
|
|
334
|
+
app.delete("/audiences/:id", (c) => {
|
|
335
|
+
const id = c.req.param("id");
|
|
336
|
+
const audience = rs().audiences.findOneBy("uuid", id);
|
|
337
|
+
if (!audience) return resendError(c, 404, "not_found", "Audience not found");
|
|
338
|
+
rs().audiences.delete(audience.id);
|
|
339
|
+
return c.json({ object: "audience", id: audience.uuid, deleted: true });
|
|
340
|
+
});
|
|
341
|
+
app.post("/audiences/:audience_id/contacts", async (c) => {
|
|
342
|
+
const audienceId = c.req.param("audience_id");
|
|
343
|
+
const audience = rs().audiences.findOneBy("uuid", audienceId);
|
|
344
|
+
if (!audience) return resendError(c, 404, "not_found", "Audience not found");
|
|
345
|
+
const body = await parseResendBody(c);
|
|
346
|
+
const email = body.email;
|
|
347
|
+
if (!email) return resendError(c, 422, "validation_error", "Missing required field: email");
|
|
348
|
+
const uuid = generateUuid();
|
|
349
|
+
const contact = rs().contacts.insert({
|
|
350
|
+
uuid,
|
|
351
|
+
audience_id: audienceId,
|
|
352
|
+
email,
|
|
353
|
+
first_name: body.first_name ?? null,
|
|
354
|
+
last_name: body.last_name ?? null,
|
|
355
|
+
unsubscribed: body.unsubscribed ?? false
|
|
356
|
+
});
|
|
357
|
+
await webhooks.dispatch("contact.created", void 0, { type: "contact.created", data: { id: uuid, email, audience_id: audienceId } }, "resend");
|
|
358
|
+
return c.json({
|
|
359
|
+
id: contact.uuid,
|
|
360
|
+
object: "contact",
|
|
361
|
+
email: contact.email
|
|
362
|
+
}, 200);
|
|
363
|
+
});
|
|
364
|
+
app.get("/audiences/:audience_id/contacts", (c) => {
|
|
365
|
+
const audienceId = c.req.param("audience_id");
|
|
366
|
+
const audience = rs().audiences.findOneBy("uuid", audienceId);
|
|
367
|
+
if (!audience) return resendError(c, 404, "not_found", "Audience not found");
|
|
368
|
+
const contacts = rs().contacts.findBy("audience_id", audienceId);
|
|
369
|
+
return c.json(resendList(contacts.map((ct) => ({
|
|
370
|
+
id: ct.uuid,
|
|
371
|
+
object: "contact",
|
|
372
|
+
email: ct.email,
|
|
373
|
+
first_name: ct.first_name,
|
|
374
|
+
last_name: ct.last_name,
|
|
375
|
+
unsubscribed: ct.unsubscribed,
|
|
376
|
+
created_at: ct.created_at
|
|
377
|
+
}))));
|
|
378
|
+
});
|
|
379
|
+
app.delete("/audiences/:audience_id/contacts/:id", async (c) => {
|
|
380
|
+
const audienceId = c.req.param("audience_id");
|
|
381
|
+
const contactId = c.req.param("id");
|
|
382
|
+
const contact = rs().contacts.findOneBy("uuid", contactId);
|
|
383
|
+
if (!contact || contact.audience_id !== audienceId) {
|
|
384
|
+
return resendError(c, 404, "not_found", "Contact not found");
|
|
385
|
+
}
|
|
386
|
+
rs().contacts.delete(contact.id);
|
|
387
|
+
await webhooks.dispatch("contact.deleted", void 0, { type: "contact.deleted", data: { id: contact.uuid, email: contact.email, audience_id: audienceId } }, "resend");
|
|
388
|
+
return c.json({ object: "contact", id: contact.uuid, deleted: true });
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
function createErrorHandler(documentationUrl) {
|
|
392
|
+
return async (c, next) => {
|
|
393
|
+
if (documentationUrl) {
|
|
394
|
+
c.set("docsUrl", documentationUrl);
|
|
395
|
+
}
|
|
396
|
+
await next();
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
var errorHandler = createErrorHandler();
|
|
400
|
+
var isDebug = typeof process !== "undefined" && (process.env.DEBUG === "1" || process.env.DEBUG === "true" || process.env.EMULATE_DEBUG === "1");
|
|
401
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
402
|
+
var FONTS = {
|
|
403
|
+
"geist-sans.woff2": readFileSync(join(__dirname, "fonts", "geist-sans.woff2")),
|
|
404
|
+
"GeistPixel-Square.woff2": readFileSync(join(__dirname, "fonts", "GeistPixel-Square.woff2"))
|
|
405
|
+
};
|
|
406
|
+
function escapeHtml(s) {
|
|
407
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
408
|
+
}
|
|
409
|
+
function escapeAttr(s) {
|
|
410
|
+
return escapeHtml(s).replace(/'/g, "'");
|
|
411
|
+
}
|
|
412
|
+
var CSS = `
|
|
413
|
+
@font-face{
|
|
414
|
+
font-family:'Geist';font-style:normal;font-weight:100 900;font-display:swap;
|
|
415
|
+
src:url('/_emulate/fonts/geist-sans.woff2') format('woff2');
|
|
416
|
+
}
|
|
417
|
+
@font-face{
|
|
418
|
+
font-family:'Geist Pixel';font-style:normal;font-weight:400;font-display:swap;
|
|
419
|
+
src:url('/_emulate/fonts/GeistPixel-Square.woff2') format('woff2');
|
|
420
|
+
}
|
|
421
|
+
*{box-sizing:border-box;margin:0;padding:0}
|
|
422
|
+
body{
|
|
423
|
+
font-family:'Geist',-apple-system,BlinkMacSystemFont,sans-serif;
|
|
424
|
+
background:#000;color:#33ff00;min-height:100vh;
|
|
425
|
+
-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;
|
|
426
|
+
}
|
|
427
|
+
.emu-bar{
|
|
428
|
+
border-bottom:1px solid #0a3300;padding:10px 20px;
|
|
429
|
+
display:flex;align-items:center;gap:10px;font-size:.8125rem;color:#1a8c00;
|
|
430
|
+
}
|
|
431
|
+
.emu-bar-title{font-weight:600;color:#33ff00;font-family:'Geist Pixel',monospace;}
|
|
432
|
+
.emu-bar-links{margin-left:auto;display:flex;gap:16px;}
|
|
433
|
+
.emu-bar-links a{
|
|
434
|
+
color:#1a8c00;font-size:.75rem;text-decoration:none;transition:color .15s;
|
|
435
|
+
}
|
|
436
|
+
.emu-bar-links a:hover{color:#33ff00;}
|
|
437
|
+
.emu-bar-links a .full{display:inline;}
|
|
438
|
+
.emu-bar-links a .short{display:none;}
|
|
439
|
+
@media(max-width:600px){
|
|
440
|
+
.emu-bar-links a .full{display:none;}
|
|
441
|
+
.emu-bar-links a .short{display:inline;}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
.content{
|
|
445
|
+
display:flex;align-items:center;justify-content:center;
|
|
446
|
+
min-height:calc(100vh - 42px);padding:24px 16px;
|
|
447
|
+
}
|
|
448
|
+
.content-inner{width:100%;max-width:420px;}
|
|
449
|
+
.card-title{
|
|
450
|
+
font-family:'Geist Pixel',monospace;
|
|
451
|
+
font-size:1.125rem;font-weight:600;margin-bottom:4px;color:#33ff00;
|
|
452
|
+
}
|
|
453
|
+
.card-subtitle{color:#1a8c00;font-size:.8125rem;margin-bottom:18px;line-height:1.45;}
|
|
454
|
+
.powered-by{
|
|
455
|
+
position:fixed;bottom:0;left:0;right:0;
|
|
456
|
+
text-align:center;padding:12px;font-size:.6875rem;color:#0a3300;
|
|
457
|
+
font-family:'Geist Pixel',monospace;
|
|
458
|
+
}
|
|
459
|
+
.powered-by a{color:#1a8c00;text-decoration:none;transition:color .15s;}
|
|
460
|
+
.powered-by a:hover{color:#33ff00;}
|
|
461
|
+
|
|
462
|
+
.error-title{
|
|
463
|
+
font-family:'Geist Pixel',monospace;
|
|
464
|
+
color:#ff4444;font-size:1.125rem;font-weight:600;margin-bottom:8px;
|
|
465
|
+
}
|
|
466
|
+
.error-msg{color:#1a8c00;font-size:.875rem;line-height:1.5;}
|
|
467
|
+
.error-card{text-align:center;}
|
|
468
|
+
|
|
469
|
+
.user-form{margin-bottom:8px;}
|
|
470
|
+
.user-form:last-of-type{margin-bottom:0;}
|
|
471
|
+
.user-btn{
|
|
472
|
+
width:100%;display:flex;align-items:center;gap:12px;
|
|
473
|
+
padding:10px 12px;border:1px solid #0a3300;border-radius:8px;
|
|
474
|
+
background:#000;color:inherit;cursor:pointer;text-align:left;
|
|
475
|
+
font:inherit;transition:border-color .15s;
|
|
476
|
+
}
|
|
477
|
+
.user-btn:hover{border-color:#33ff00;}
|
|
478
|
+
.avatar{
|
|
479
|
+
width:36px;height:36px;border-radius:50%;
|
|
480
|
+
background:#0a3300;color:#33ff00;font-weight:600;font-size:.875rem;
|
|
481
|
+
display:flex;align-items:center;justify-content:center;flex-shrink:0;
|
|
482
|
+
font-family:'Geist Pixel',monospace;
|
|
483
|
+
}
|
|
484
|
+
.user-text{min-width:0;}
|
|
485
|
+
.user-login{font-weight:600;font-size:.875rem;display:block;color:#33ff00;}
|
|
486
|
+
.user-meta{color:#1a8c00;font-size:.75rem;margin-top:1px;}
|
|
487
|
+
.user-email{font-size:.6875rem;color:#116600;word-break:break-all;margin-top:1px;}
|
|
488
|
+
|
|
489
|
+
.settings-layout{
|
|
490
|
+
max-width:920px;margin:0 auto;padding:28px 20px;
|
|
491
|
+
display:flex;gap:28px;
|
|
492
|
+
}
|
|
493
|
+
.settings-sidebar{width:200px;flex-shrink:0;}
|
|
494
|
+
.settings-sidebar a{
|
|
495
|
+
display:block;padding:6px 10px;border-radius:6px;color:#1a8c00;
|
|
496
|
+
text-decoration:none;font-size:.8125rem;transition:color .15s;
|
|
497
|
+
}
|
|
498
|
+
.settings-sidebar a:hover{color:#33ff00;}
|
|
499
|
+
.settings-sidebar a.active{color:#33ff00;font-weight:600;}
|
|
500
|
+
.settings-main{flex:1;min-width:0;}
|
|
501
|
+
|
|
502
|
+
.s-card{
|
|
503
|
+
padding:18px 0;margin-bottom:14px;border-bottom:1px solid #0a3300;
|
|
504
|
+
}
|
|
505
|
+
.s-card:last-child{border-bottom:none;}
|
|
506
|
+
.s-card-header{display:flex;align-items:center;gap:14px;margin-bottom:14px;}
|
|
507
|
+
.s-icon{
|
|
508
|
+
width:42px;height:42px;border-radius:8px;
|
|
509
|
+
background:#0a3300;display:flex;align-items:center;justify-content:center;
|
|
510
|
+
font-size:1.125rem;font-weight:700;color:#116600;flex-shrink:0;
|
|
511
|
+
font-family:'Geist Pixel',monospace;
|
|
512
|
+
}
|
|
513
|
+
.s-title{
|
|
514
|
+
font-family:'Geist Pixel',monospace;
|
|
515
|
+
font-size:1.25rem;font-weight:600;color:#33ff00;
|
|
516
|
+
}
|
|
517
|
+
.s-subtitle{font-size:.75rem;color:#1a8c00;margin-top:2px;}
|
|
518
|
+
.section-heading{
|
|
519
|
+
font-size:.9375rem;font-weight:600;margin-bottom:10px;color:#33ff00;
|
|
520
|
+
display:flex;align-items:center;justify-content:space-between;
|
|
521
|
+
}
|
|
522
|
+
.perm-list{list-style:none;}
|
|
523
|
+
.perm-list li{padding:5px 0;font-size:.8125rem;display:flex;align-items:center;gap:6px;color:#1a8c00;}
|
|
524
|
+
.check{color:#33ff00;}
|
|
525
|
+
.org-row{
|
|
526
|
+
display:flex;align-items:center;gap:8px;padding:7px 0;
|
|
527
|
+
border-bottom:1px solid #0a3300;font-size:.8125rem;
|
|
528
|
+
}
|
|
529
|
+
.org-row:last-child{border-bottom:none;}
|
|
530
|
+
.org-icon{
|
|
531
|
+
width:22px;height:22px;border-radius:4px;background:#0a3300;
|
|
532
|
+
display:flex;align-items:center;justify-content:center;
|
|
533
|
+
font-size:.625rem;font-weight:700;color:#116600;flex-shrink:0;
|
|
534
|
+
font-family:'Geist Pixel',monospace;
|
|
535
|
+
}
|
|
536
|
+
.org-name{font-weight:600;color:#33ff00;}
|
|
537
|
+
.badge{font-size:.6875rem;padding:1px 7px;border-radius:999px;font-weight:500;}
|
|
538
|
+
.badge-granted{background:#0a3300;color:#33ff00;}
|
|
539
|
+
.badge-denied{background:#1a0a0a;color:#ff4444;}
|
|
540
|
+
.badge-requested{background:#0a3300;color:#1a8c00;}
|
|
541
|
+
.btn-revoke{
|
|
542
|
+
display:inline-block;padding:5px 14px;border-radius:6px;
|
|
543
|
+
border:1px solid #0a3300;background:transparent;color:#ff4444;
|
|
544
|
+
font-size:.75rem;font-weight:600;cursor:pointer;transition:border-color .15s;
|
|
545
|
+
}
|
|
546
|
+
.btn-revoke:hover{border-color:#ff4444;}
|
|
547
|
+
.info-text{color:#1a8c00;font-size:.75rem;line-height:1.5;margin-top:10px;}
|
|
548
|
+
.app-link{
|
|
549
|
+
display:flex;align-items:center;gap:12px;padding:12px;
|
|
550
|
+
border:1px solid #0a3300;border-radius:8px;background:#000;
|
|
551
|
+
text-decoration:none;color:inherit;margin-bottom:8px;transition:border-color .15s;
|
|
552
|
+
}
|
|
553
|
+
.app-link:hover{border-color:#33ff00;}
|
|
554
|
+
.app-link-name{font-weight:600;font-size:.875rem;color:#33ff00;}
|
|
555
|
+
.app-link-scopes{font-size:.6875rem;color:#1a8c00;margin-top:1px;}
|
|
556
|
+
.empty{color:#1a8c00;text-align:center;padding:28px 0;font-size:.875rem;}
|
|
557
|
+
`;
|
|
558
|
+
var POWERED_BY = `<div class="powered-by">Powered by <a href="https://emulate.dev" target="_blank" rel="noopener">emulate</a></div>`;
|
|
559
|
+
function emuBar(service) {
|
|
560
|
+
const title = service ? `${escapeHtml(service)} Emulator` : "Emulator";
|
|
561
|
+
return `<div class="emu-bar">
|
|
562
|
+
<span class="emu-bar-title">${title}</span>
|
|
563
|
+
<nav class="emu-bar-links">
|
|
564
|
+
<a href="https://github.com/vercel-labs/emulate/issues" target="_blank" rel="noopener"><span class="full">Report Issue</span><span class="short">Report</span></a>
|
|
565
|
+
<a href="https://github.com/vercel-labs/emulate" target="_blank" rel="noopener"><span class="full">Source Code</span><span class="short">Source</span></a>
|
|
566
|
+
<a href="https://emulate.dev" target="_blank" rel="noopener"><span class="full">Learn More</span><span class="short">Learn</span></a>
|
|
567
|
+
</nav>
|
|
568
|
+
</div>`;
|
|
569
|
+
}
|
|
570
|
+
function head(title) {
|
|
571
|
+
return `<!DOCTYPE html>
|
|
572
|
+
<html lang="en">
|
|
573
|
+
<head>
|
|
574
|
+
<meta charset="utf-8"/>
|
|
575
|
+
<meta name="viewport" content="width=device-width,initial-scale=1"/>
|
|
576
|
+
<title>${escapeHtml(title)} | emulate</title>
|
|
577
|
+
<style>${CSS}</style>
|
|
578
|
+
</head>`;
|
|
579
|
+
}
|
|
580
|
+
function renderCardPage(title, subtitle, body, service) {
|
|
581
|
+
return `${head(title)}
|
|
582
|
+
<body>
|
|
583
|
+
${emuBar(service)}
|
|
584
|
+
<div class="content">
|
|
585
|
+
<div class="content-inner">
|
|
586
|
+
<div class="card-title">${escapeHtml(title)}</div>
|
|
587
|
+
<div class="card-subtitle">${subtitle}</div>
|
|
588
|
+
${body}
|
|
589
|
+
</div>
|
|
590
|
+
</div>
|
|
591
|
+
${POWERED_BY}
|
|
592
|
+
</body></html>`;
|
|
593
|
+
}
|
|
594
|
+
var SERVICE_LABEL = "Resend";
|
|
595
|
+
function inboxRoutes(ctx) {
|
|
596
|
+
const { app, store } = ctx;
|
|
597
|
+
const rs = () => getResendStore(store);
|
|
598
|
+
app.get("/inbox", (c) => {
|
|
599
|
+
const emails = rs().emails.all().reverse();
|
|
600
|
+
let body = "";
|
|
601
|
+
if (emails.length === 0) {
|
|
602
|
+
body = `<div class="empty">No emails sent yet. Use POST /emails to send one.</div>`;
|
|
603
|
+
} else {
|
|
604
|
+
for (const email of emails) {
|
|
605
|
+
const letter = (email.from?.[0] ?? "?").toUpperCase();
|
|
606
|
+
const statusClass = email.status === "delivered" ? "badge-granted" : email.status === "bounced" ? "badge-denied" : "badge-requested";
|
|
607
|
+
body += `<a href="/inbox/${escapeAttr(email.uuid)}" class="app-link">
|
|
608
|
+
<span class="org-icon">${escapeHtml(letter)}</span>
|
|
609
|
+
<span class="user-text">
|
|
610
|
+
<span class="org-name">${escapeHtml(email.subject)}</span>
|
|
611
|
+
<span class="user-meta">${escapeHtml(email.from)} → ${escapeHtml(email.to.join(", "))}</span>
|
|
612
|
+
</span>
|
|
613
|
+
<span class="badge ${statusClass}">${escapeHtml(email.status)}</span>
|
|
614
|
+
</a>`;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
const html = renderCardPage(
|
|
618
|
+
"Inbox",
|
|
619
|
+
`${emails.length} email${emails.length !== 1 ? "s" : ""} sent`,
|
|
620
|
+
body,
|
|
621
|
+
SERVICE_LABEL
|
|
622
|
+
);
|
|
623
|
+
return c.html(html);
|
|
624
|
+
});
|
|
625
|
+
app.get("/inbox/:id", (c) => {
|
|
626
|
+
const id = c.req.param("id");
|
|
627
|
+
const email = rs().emails.findOneBy("uuid", id);
|
|
628
|
+
if (!email) {
|
|
629
|
+
const html2 = renderCardPage(
|
|
630
|
+
"Not Found",
|
|
631
|
+
"The requested email was not found.",
|
|
632
|
+
`<div class="empty">Email not found</div>`,
|
|
633
|
+
SERVICE_LABEL
|
|
634
|
+
);
|
|
635
|
+
return c.html(html2, 404);
|
|
636
|
+
}
|
|
637
|
+
const statusClass = email.status === "delivered" ? "badge-granted" : email.status === "bounced" ? "badge-denied" : "badge-requested";
|
|
638
|
+
let tagsHtml = "";
|
|
639
|
+
if (email.tags.length > 0) {
|
|
640
|
+
tagsHtml = `<div class="info-text">`;
|
|
641
|
+
for (const tag of email.tags) {
|
|
642
|
+
tagsHtml += `<span class="badge badge-requested">${escapeHtml(tag.name)}: ${escapeHtml(tag.value)}</span> `;
|
|
643
|
+
}
|
|
644
|
+
tagsHtml += `</div>`;
|
|
645
|
+
}
|
|
646
|
+
const recipientLines = [];
|
|
647
|
+
recipientLines.push(`<strong>To:</strong> ${escapeHtml(email.to.join(", "))}`);
|
|
648
|
+
if (email.cc.length > 0) {
|
|
649
|
+
recipientLines.push(`<strong>Cc:</strong> ${escapeHtml(email.cc.join(", "))}`);
|
|
650
|
+
}
|
|
651
|
+
if (email.bcc.length > 0) {
|
|
652
|
+
recipientLines.push(`<strong>Bcc:</strong> ${escapeHtml(email.bcc.join(", "))}`);
|
|
653
|
+
}
|
|
654
|
+
const previewContent = email.html ? `<iframe
|
|
655
|
+
sandbox=""
|
|
656
|
+
srcdoc="${escapeAttr(email.html)}"
|
|
657
|
+
class="s-card"
|
|
658
|
+
style="width:100%;min-height:300px;border:1px solid #0a3300;border-radius:8px;background:#fff;"
|
|
659
|
+
></iframe>` : email.text ? `<div class="s-card"><pre class="info-text">${escapeHtml(email.text)}</pre></div>` : `<div class="empty">No content</div>`;
|
|
660
|
+
const body = `
|
|
661
|
+
<div class="org-row">
|
|
662
|
+
<span class="badge ${statusClass}">${escapeHtml(email.status)}</span>
|
|
663
|
+
<span class="user-meta">${escapeHtml(email.created_at)}</span>
|
|
664
|
+
</div>
|
|
665
|
+
<div class="s-card">
|
|
666
|
+
<div class="perm-list">
|
|
667
|
+
<li><strong>From:</strong> ${escapeHtml(email.from)}</li>
|
|
668
|
+
${recipientLines.map((line) => `<li>${line}</li>`).join("\n ")}
|
|
669
|
+
</div>
|
|
670
|
+
</div>
|
|
671
|
+
${tagsHtml}
|
|
672
|
+
<div class="section-heading">Preview</div>
|
|
673
|
+
${previewContent}
|
|
674
|
+
<div class="info-text">
|
|
675
|
+
<strong>Last event:</strong> ${escapeHtml(email.last_event)}
|
|
676
|
+
${email.scheduled_at ? ` | <strong>Scheduled:</strong> ${escapeHtml(email.scheduled_at)}` : ""}
|
|
677
|
+
</div>`;
|
|
678
|
+
const html = renderCardPage(
|
|
679
|
+
email.subject,
|
|
680
|
+
`Email ${escapeHtml(email.uuid)}`,
|
|
681
|
+
body,
|
|
682
|
+
SERVICE_LABEL
|
|
683
|
+
);
|
|
684
|
+
return c.html(html);
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
function seedFromConfig(store, _baseUrl, config) {
|
|
688
|
+
const rs = getResendStore(store);
|
|
689
|
+
if (config.domains) {
|
|
690
|
+
for (const d of config.domains) {
|
|
691
|
+
const existing = rs.domains.findOneBy("name", d.name);
|
|
692
|
+
if (existing) continue;
|
|
693
|
+
const region = d.region ?? "us-east-1";
|
|
694
|
+
rs.domains.insert({
|
|
695
|
+
uuid: generateUuid(),
|
|
696
|
+
name: d.name,
|
|
697
|
+
status: "verified",
|
|
698
|
+
region,
|
|
699
|
+
records: [
|
|
700
|
+
{
|
|
701
|
+
record: "SPF",
|
|
702
|
+
name: d.name,
|
|
703
|
+
type: "MX",
|
|
704
|
+
ttl: "Auto",
|
|
705
|
+
status: "verified",
|
|
706
|
+
value: `feedback-smtp.${region}.amazonses.com`,
|
|
707
|
+
priority: 10
|
|
708
|
+
},
|
|
709
|
+
{
|
|
710
|
+
record: "SPF",
|
|
711
|
+
name: d.name,
|
|
712
|
+
type: "TXT",
|
|
713
|
+
ttl: "Auto",
|
|
714
|
+
status: "verified",
|
|
715
|
+
value: "v=spf1 include:amazonses.com ~all"
|
|
716
|
+
},
|
|
717
|
+
{
|
|
718
|
+
record: "DKIM",
|
|
719
|
+
name: `resend._domainkey.${d.name}`,
|
|
720
|
+
type: "CNAME",
|
|
721
|
+
ttl: "Auto",
|
|
722
|
+
status: "verified",
|
|
723
|
+
value: `resend.domainkey.${region}.amazonses.com`
|
|
724
|
+
}
|
|
725
|
+
]
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
if (config.contacts) {
|
|
730
|
+
let defaultAudience = rs.audiences.findOneBy("name", "Default");
|
|
731
|
+
if (!defaultAudience) {
|
|
732
|
+
defaultAudience = rs.audiences.insert({ uuid: generateUuid(), name: "Default" });
|
|
733
|
+
}
|
|
734
|
+
for (const ct of config.contacts) {
|
|
735
|
+
let audienceId = defaultAudience.uuid;
|
|
736
|
+
if (ct.audience) {
|
|
737
|
+
let audience = rs.audiences.findOneBy("name", ct.audience);
|
|
738
|
+
if (!audience) {
|
|
739
|
+
audience = rs.audiences.insert({ uuid: generateUuid(), name: ct.audience });
|
|
740
|
+
}
|
|
741
|
+
audienceId = audience.uuid;
|
|
742
|
+
}
|
|
743
|
+
rs.contacts.insert({
|
|
744
|
+
uuid: generateUuid(),
|
|
745
|
+
audience_id: audienceId,
|
|
746
|
+
email: ct.email,
|
|
747
|
+
first_name: ct.first_name ?? null,
|
|
748
|
+
last_name: ct.last_name ?? null,
|
|
749
|
+
unsubscribed: false
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
var resendPlugin = {
|
|
755
|
+
name: "resend",
|
|
756
|
+
register(app, store, webhooks, baseUrl, tokenMap) {
|
|
757
|
+
const ctx = { app, store, webhooks, baseUrl, tokenMap };
|
|
758
|
+
emailRoutes(ctx);
|
|
759
|
+
domainRoutes(ctx);
|
|
760
|
+
apiKeyRoutes(ctx);
|
|
761
|
+
contactRoutes(ctx);
|
|
762
|
+
inboxRoutes(ctx);
|
|
763
|
+
},
|
|
764
|
+
seed(_store, _baseUrl) {
|
|
765
|
+
}
|
|
766
|
+
};
|
|
767
|
+
var index_default = resendPlugin;
|
|
768
|
+
export {
|
|
769
|
+
index_default as default,
|
|
770
|
+
getResendStore,
|
|
771
|
+
resendPlugin,
|
|
772
|
+
seedFromConfig
|
|
773
|
+
};
|
|
774
|
+
//# sourceMappingURL=dist-QMOJM6DV.js.map
|