@c9up/rover 0.1.3
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/LICENSE +21 -0
- package/README.md +91 -0
- package/dist/BaseMail.d.ts +28 -0
- package/dist/BaseMail.d.ts.map +1 -0
- package/dist/BaseMail.js +42 -0
- package/dist/BaseMail.js.map +1 -0
- package/dist/Mail.d.ts +166 -0
- package/dist/Mail.d.ts.map +1 -0
- package/dist/Mail.js +451 -0
- package/dist/Mail.js.map +1 -0
- package/dist/MessageBuilder.d.ts +38 -0
- package/dist/MessageBuilder.d.ts.map +1 -0
- package/dist/MessageBuilder.js +70 -0
- package/dist/MessageBuilder.js.map +1 -0
- package/dist/RoverError.d.ts +30 -0
- package/dist/RoverError.d.ts.map +1 -0
- package/dist/RoverError.js +33 -0
- package/dist/RoverError.js.map +1 -0
- package/dist/RoverProvider.d.ts +24 -0
- package/dist/RoverProvider.d.ts.map +1 -0
- package/dist/RoverProvider.js +51 -0
- package/dist/RoverProvider.js.map +1 -0
- package/dist/config.d.ts +4 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +4 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/queue/MailJob.d.ts +41 -0
- package/dist/queue/MailJob.d.ts.map +1 -0
- package/dist/queue/MailJob.js +75 -0
- package/dist/queue/MailJob.js.map +1 -0
- package/dist/retry.d.ts +20 -0
- package/dist/retry.d.ts.map +1 -0
- package/dist/retry.js +101 -0
- package/dist/retry.js.map +1 -0
- package/dist/services/main.d.ts +20 -0
- package/dist/services/main.d.ts.map +1 -0
- package/dist/services/main.js +33 -0
- package/dist/services/main.js.map +1 -0
- package/dist/templating/SimpleTemplate.d.ts +7 -0
- package/dist/templating/SimpleTemplate.d.ts.map +1 -0
- package/dist/templating/SimpleTemplate.js +192 -0
- package/dist/templating/SimpleTemplate.js.map +1 -0
- package/dist/templating/loadNapi.d.ts +21 -0
- package/dist/templating/loadNapi.d.ts.map +1 -0
- package/dist/templating/loadNapi.js +115 -0
- package/dist/templating/loadNapi.js.map +1 -0
- package/dist/testing/FakeMail.d.ts +31 -0
- package/dist/testing/FakeMail.d.ts.map +1 -0
- package/dist/testing/FakeMail.js +90 -0
- package/dist/testing/FakeMail.js.map +1 -0
- package/dist/transports/MailgunTransport.d.ts +7 -0
- package/dist/transports/MailgunTransport.d.ts.map +1 -0
- package/dist/transports/MailgunTransport.js +149 -0
- package/dist/transports/MailgunTransport.js.map +1 -0
- package/dist/transports/ResendTransport.d.ts +7 -0
- package/dist/transports/ResendTransport.d.ts.map +1 -0
- package/dist/transports/ResendTransport.js +111 -0
- package/dist/transports/ResendTransport.js.map +1 -0
- package/dist/transports/SendGridTransport.d.ts +7 -0
- package/dist/transports/SendGridTransport.d.ts.map +1 -0
- package/dist/transports/SendGridTransport.js +165 -0
- package/dist/transports/SendGridTransport.js.map +1 -0
- package/dist/transports/SesTransport.d.ts +7 -0
- package/dist/transports/SesTransport.d.ts.map +1 -0
- package/dist/transports/SesTransport.js +334 -0
- package/dist/transports/SesTransport.js.map +1 -0
- package/dist/transports/fetchError.d.ts +13 -0
- package/dist/transports/fetchError.d.ts.map +1 -0
- package/dist/transports/fetchError.js +35 -0
- package/dist/transports/fetchError.js.map +1 -0
- package/dist/webhooks/context.d.ts +35 -0
- package/dist/webhooks/context.d.ts.map +1 -0
- package/dist/webhooks/context.js +7 -0
- package/dist/webhooks/context.js.map +1 -0
- package/dist/webhooks/mailgun.d.ts +13 -0
- package/dist/webhooks/mailgun.d.ts.map +1 -0
- package/dist/webhooks/mailgun.js +102 -0
- package/dist/webhooks/mailgun.js.map +1 -0
- package/dist/webhooks/resend.d.ts +16 -0
- package/dist/webhooks/resend.d.ts.map +1 -0
- package/dist/webhooks/resend.js +121 -0
- package/dist/webhooks/resend.js.map +1 -0
- package/dist/webhooks/sendgrid.d.ts +13 -0
- package/dist/webhooks/sendgrid.d.ts.map +1 -0
- package/dist/webhooks/sendgrid.js +104 -0
- package/dist/webhooks/sendgrid.js.map +1 -0
- package/index.darwin-arm64.node +0 -0
- package/index.darwin-x64.node +0 -0
- package/index.linux-arm64-gnu.node +0 -0
- package/index.linux-x64-gnu.node +0 -0
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +97 -0
- package/scripts/copy-napi.mjs +62 -0
- package/src/BaseMail.ts +52 -0
- package/src/Mail.ts +663 -0
- package/src/MessageBuilder.ts +101 -0
- package/src/RoverError.ts +44 -0
- package/src/RoverProvider.ts +72 -0
- package/src/config.ts +7 -0
- package/src/index.ts +30 -0
- package/src/queue/MailJob.ts +131 -0
- package/src/retry.ts +135 -0
- package/src/services/main.ts +41 -0
- package/src/templating/SimpleTemplate.ts +240 -0
- package/src/templating/loadNapi.ts +169 -0
- package/src/testing/FakeMail.ts +112 -0
- package/src/transports/MailgunTransport.ts +204 -0
- package/src/transports/ResendTransport.ts +147 -0
- package/src/transports/SendGridTransport.ts +246 -0
- package/src/transports/SesTransport.ts +394 -0
- package/src/transports/fetchError.ts +42 -0
- package/src/webhooks/context.ts +41 -0
- package/src/webhooks/mailgun.ts +134 -0
- package/src/webhooks/resend.ts +164 -0
- package/src/webhooks/sendgrid.ts +133 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
3
|
+
/**
|
|
4
|
+
* Parse a provider-supplied `created_at` into an epoch-ms timestamp,
|
|
5
|
+
* falling back to "now" for anything unparseable. Without the
|
|
6
|
+
* `Number.isFinite` guard, a malformed value like `"not-a-date"`
|
|
7
|
+
* yields `new Date(...).getTime()` === `NaN`, which then leaks into
|
|
8
|
+
* the `mail.*` event payload and breaks any consumer that sorts,
|
|
9
|
+
* persists, or numerically compares the timestamp.
|
|
10
|
+
*/
|
|
11
|
+
function parseTimestamp(raw) {
|
|
12
|
+
if (raw !== undefined && raw !== "") {
|
|
13
|
+
const t = new Date(raw).getTime();
|
|
14
|
+
if (Number.isFinite(t))
|
|
15
|
+
return t;
|
|
16
|
+
}
|
|
17
|
+
return Date.now();
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Resend event → Rover event. Only map events that correspond to durable,
|
|
21
|
+
* decision-grade outcomes. `email.delivery_delayed` is intermediate (not a
|
|
22
|
+
* final failure) and `email.complained` is a deliverability signal (not a
|
|
23
|
+
* delivery failure) — both are dropped to keep the event bus signal-dense.
|
|
24
|
+
*/
|
|
25
|
+
const EVENT_MAP = {
|
|
26
|
+
"email.delivered": "mail.delivered",
|
|
27
|
+
"email.bounced": "mail.bounced",
|
|
28
|
+
};
|
|
29
|
+
export function createResendWebhookHandler(options) {
|
|
30
|
+
if (!options.secret) {
|
|
31
|
+
throw new Error("createResendWebhookHandler: secret is required for HMAC verification.");
|
|
32
|
+
}
|
|
33
|
+
const secretRaw = options.secret.startsWith("whsec_")
|
|
34
|
+
? options.secret.slice(6)
|
|
35
|
+
: options.secret;
|
|
36
|
+
const keyBytes = Buffer.from(secretRaw, "base64");
|
|
37
|
+
const emitter = options.emitter;
|
|
38
|
+
const maxAgeSeconds = options.maxAgeSeconds ?? 300;
|
|
39
|
+
return async (ctx, _next) => {
|
|
40
|
+
const raw = await Promise.resolve(ctx.request.rawBody());
|
|
41
|
+
const id = ctx.request.header("svix-id");
|
|
42
|
+
const timestamp = ctx.request.header("svix-timestamp");
|
|
43
|
+
const signatureHeader = ctx.request.header("svix-signature");
|
|
44
|
+
if (!id || !timestamp || !signatureHeader) {
|
|
45
|
+
ctx.response.status(401).json({ error: "missing_signature" });
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const tsSeconds = Number(timestamp);
|
|
49
|
+
if (!Number.isFinite(tsSeconds)) {
|
|
50
|
+
ctx.response.status(401).json({ error: "invalid_signature" });
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const nowSeconds = Date.now() / 1000;
|
|
54
|
+
if (Math.abs(nowSeconds - tsSeconds) > maxAgeSeconds) {
|
|
55
|
+
ctx.response.status(401).json({ error: "stale_signature" });
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (!verifySvixSignature(raw, id, timestamp, signatureHeader, keyBytes)) {
|
|
59
|
+
ctx.response.status(401).json({ error: "invalid_signature" });
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
let payload;
|
|
63
|
+
try {
|
|
64
|
+
payload = JSON.parse(raw.toString("utf8"));
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
ctx.response.status(400).json({ error: "invalid_json" });
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
emitMappedEvent(emitter, payload);
|
|
71
|
+
ctx.response.status(200).json({ ok: true });
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Verify a Svix signature header. Svix signs `<id>.<timestamp>.<body>` and emits
|
|
76
|
+
* a space-separated list of `v1,<base64sig>` candidates — accept if any matches
|
|
77
|
+
* (constant-time compare).
|
|
78
|
+
*/
|
|
79
|
+
function verifySvixSignature(raw, id, timestamp, signatureHeader, keyBytes) {
|
|
80
|
+
const toSign = Buffer.concat([
|
|
81
|
+
Buffer.from(`${id}.${timestamp}.`, "utf8"),
|
|
82
|
+
raw,
|
|
83
|
+
]);
|
|
84
|
+
const expected = createHmac("sha256", keyBytes)
|
|
85
|
+
.update(toSign)
|
|
86
|
+
.digest("base64");
|
|
87
|
+
for (const part of signatureHeader.split(" ")) {
|
|
88
|
+
const [scheme, sig] = part.split(",");
|
|
89
|
+
if (scheme !== "v1" || !sig)
|
|
90
|
+
continue;
|
|
91
|
+
const sigBuf = Buffer.from(sig, "base64");
|
|
92
|
+
const expBuf = Buffer.from(expected, "base64");
|
|
93
|
+
if (sigBuf.length !== expBuf.length)
|
|
94
|
+
continue;
|
|
95
|
+
if (timingSafeEqual(sigBuf, expBuf))
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
/** Map a Resend payload to a mail event and emit it; emitter throws are swallowed. */
|
|
101
|
+
function emitMappedEvent(emitter, payload) {
|
|
102
|
+
const mapped = payload.type ? EVENT_MAP[payload.type] : undefined;
|
|
103
|
+
if (!mapped)
|
|
104
|
+
return;
|
|
105
|
+
const to = Array.isArray(payload.data?.to)
|
|
106
|
+
? (payload.data.to[0] ?? "")
|
|
107
|
+
: (payload.data?.to ?? "");
|
|
108
|
+
try {
|
|
109
|
+
emitter.emit(mapped, {
|
|
110
|
+
messageId: payload.data?.email_id ?? "",
|
|
111
|
+
to,
|
|
112
|
+
reason: payload.data?.reason,
|
|
113
|
+
timestamp: parseTimestamp(payload.data?.created_at),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// Emitter failures must not flip the webhook response; providers will
|
|
118
|
+
// replay on non-2xx and flood the bus.
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=resend.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resend.js","sourceRoot":"","sources":["../../src/webhooks/resend.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { WebhookEmitter, WebhookMiddleware } from "./context.js";
|
|
2
|
+
export interface SendGridWebhookOptions {
|
|
3
|
+
/** Base64-DER or PEM-encoded Ed25519 public key, as shown in the SendGrid dashboard. */
|
|
4
|
+
publicKey: string;
|
|
5
|
+
emitter: WebhookEmitter;
|
|
6
|
+
/**
|
|
7
|
+
* Maximum age in seconds for a signed payload (default 300 = 5 minutes).
|
|
8
|
+
* Set to `Number.POSITIVE_INFINITY` to disable (not recommended).
|
|
9
|
+
*/
|
|
10
|
+
maxAgeSeconds?: number;
|
|
11
|
+
}
|
|
12
|
+
export declare function createSendGridWebhookHandler(options: SendGridWebhookOptions): WebhookMiddleware;
|
|
13
|
+
//# sourceMappingURL=sendgrid.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sendgrid.d.ts","sourceRoot":"","sources":["../../src/webhooks/sendgrid.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACX,cAAc,EAEd,iBAAiB,EACjB,MAAM,cAAc,CAAC;AAEtB,MAAM,WAAW,sBAAsB;IACtC,wFAAwF;IACxF,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,cAAc,CAAC;IACxB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACvB;AAkBD,wBAAgB,4BAA4B,CAC3C,OAAO,EAAE,sBAAsB,GAC7B,iBAAiB,CA8EnB"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import { createPublicKey, verify } from "node:crypto";
|
|
3
|
+
const EVENT_MAP = {
|
|
4
|
+
delivered: "mail.delivered",
|
|
5
|
+
bounce: "mail.bounced",
|
|
6
|
+
dropped: "mail.failed",
|
|
7
|
+
deferred: "mail.failed",
|
|
8
|
+
blocked: "mail.failed",
|
|
9
|
+
};
|
|
10
|
+
export function createSendGridWebhookHandler(options) {
|
|
11
|
+
if (!options.publicKey) {
|
|
12
|
+
throw new Error("createSendGridWebhookHandler: publicKey is required for Ed25519 verification.");
|
|
13
|
+
}
|
|
14
|
+
const pemKey = toPem(options.publicKey);
|
|
15
|
+
const key = createPublicKey(pemKey);
|
|
16
|
+
const emitter = options.emitter;
|
|
17
|
+
const maxAgeSeconds = options.maxAgeSeconds ?? 300;
|
|
18
|
+
return async (ctx, _next) => {
|
|
19
|
+
const raw = await Promise.resolve(ctx.request.rawBody());
|
|
20
|
+
const signature = ctx.request.header("x-twilio-email-event-webhook-signature");
|
|
21
|
+
const timestamp = ctx.request.header("x-twilio-email-event-webhook-timestamp");
|
|
22
|
+
if (!signature || !timestamp) {
|
|
23
|
+
ctx.response.status(401).json({ error: "missing_signature" });
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const tsSeconds = Number(timestamp);
|
|
27
|
+
if (!Number.isFinite(tsSeconds)) {
|
|
28
|
+
ctx.response.status(401).json({ error: "invalid_signature" });
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const nowSeconds = Date.now() / 1000;
|
|
32
|
+
if (Math.abs(nowSeconds - tsSeconds) > maxAgeSeconds) {
|
|
33
|
+
ctx.response.status(401).json({ error: "stale_signature" });
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const payloadToVerify = Buffer.concat([
|
|
37
|
+
Buffer.from(timestamp, "utf8"),
|
|
38
|
+
raw,
|
|
39
|
+
]);
|
|
40
|
+
let sigBytes;
|
|
41
|
+
try {
|
|
42
|
+
sigBytes = Buffer.from(signature, "base64");
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
ctx.response.status(401).json({ error: "invalid_signature" });
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const ok = verify(null, payloadToVerify, key, sigBytes);
|
|
49
|
+
if (!ok) {
|
|
50
|
+
ctx.response.status(401).json({ error: "invalid_signature" });
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
let events;
|
|
54
|
+
try {
|
|
55
|
+
events = JSON.parse(raw.toString("utf8"));
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
ctx.response.status(400).json({ error: "invalid_json" });
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (!Array.isArray(events)) {
|
|
62
|
+
ctx.response.status(400).json({ error: "expected_event_array" });
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
for (const ev of events) {
|
|
66
|
+
if (!ev.event)
|
|
67
|
+
continue;
|
|
68
|
+
const mapped = EVENT_MAP[ev.event];
|
|
69
|
+
if (!mapped)
|
|
70
|
+
continue;
|
|
71
|
+
try {
|
|
72
|
+
emitter.emit(mapped, {
|
|
73
|
+
messageId: ev.sg_message_id ?? "",
|
|
74
|
+
to: ev.email ?? "",
|
|
75
|
+
reason: ev.reason,
|
|
76
|
+
timestamp: normaliseTimestamp(ev.timestamp),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// Never let emitter failures flip the webhook response.
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
ctx.response.status(200).json({ ok: true });
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function toPem(key) {
|
|
87
|
+
const normalised = key.replace(/\r\n/g, "\n");
|
|
88
|
+
if (normalised.includes("BEGIN PUBLIC KEY"))
|
|
89
|
+
return normalised;
|
|
90
|
+
const der = normalised.replace(/\s/g, "");
|
|
91
|
+
return `-----BEGIN PUBLIC KEY-----\n${der}\n-----END PUBLIC KEY-----`;
|
|
92
|
+
}
|
|
93
|
+
function normaliseTimestamp(ts) {
|
|
94
|
+
// Guard non-finite values (NaN / ±Infinity): a malformed `timestamp`
|
|
95
|
+
// field in the webhook JSON would otherwise multiply into NaN and
|
|
96
|
+
// leak into the emitted mail.* event, breaking consumers that sort
|
|
97
|
+
// / persist / compare timestamps numerically.
|
|
98
|
+
if (ts === undefined || !Number.isFinite(ts))
|
|
99
|
+
return Date.now();
|
|
100
|
+
if (ts > 1e12)
|
|
101
|
+
return ts;
|
|
102
|
+
return ts * 1000;
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=sendgrid.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sendgrid.js","sourceRoot":"","sources":["../../src/webhooks/sendgrid.ts"],"names":[],"mappings":"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"}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@c9up/rover",
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"description": "Mail transport for Ream — SMTP, log, and pluggable transports",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./provider": {
|
|
14
|
+
"types": "./dist/RoverProvider.d.ts",
|
|
15
|
+
"import": "./dist/RoverProvider.js"
|
|
16
|
+
},
|
|
17
|
+
"./services/main": {
|
|
18
|
+
"types": "./dist/services/main.d.ts",
|
|
19
|
+
"import": "./dist/services/main.js"
|
|
20
|
+
},
|
|
21
|
+
"./transports/mailgun": {
|
|
22
|
+
"types": "./dist/transports/MailgunTransport.d.ts",
|
|
23
|
+
"import": "./dist/transports/MailgunTransport.js"
|
|
24
|
+
},
|
|
25
|
+
"./transports/sendgrid": {
|
|
26
|
+
"types": "./dist/transports/SendGridTransport.d.ts",
|
|
27
|
+
"import": "./dist/transports/SendGridTransport.js"
|
|
28
|
+
},
|
|
29
|
+
"./transports/ses": {
|
|
30
|
+
"types": "./dist/transports/SesTransport.d.ts",
|
|
31
|
+
"import": "./dist/transports/SesTransport.js"
|
|
32
|
+
},
|
|
33
|
+
"./transports/resend": {
|
|
34
|
+
"types": "./dist/transports/ResendTransport.d.ts",
|
|
35
|
+
"import": "./dist/transports/ResendTransport.js"
|
|
36
|
+
},
|
|
37
|
+
"./testing": {
|
|
38
|
+
"types": "./dist/testing/FakeMail.d.ts",
|
|
39
|
+
"import": "./dist/testing/FakeMail.js"
|
|
40
|
+
},
|
|
41
|
+
"./webhooks/mailgun": {
|
|
42
|
+
"types": "./dist/webhooks/mailgun.d.ts",
|
|
43
|
+
"import": "./dist/webhooks/mailgun.js"
|
|
44
|
+
},
|
|
45
|
+
"./webhooks/sendgrid": {
|
|
46
|
+
"types": "./dist/webhooks/sendgrid.d.ts",
|
|
47
|
+
"import": "./dist/webhooks/sendgrid.js"
|
|
48
|
+
},
|
|
49
|
+
"./webhooks/resend": {
|
|
50
|
+
"types": "./dist/webhooks/resend.d.ts",
|
|
51
|
+
"import": "./dist/webhooks/resend.js"
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"@sendgrid/mail": "^8.1.3",
|
|
56
|
+
"form-data": "^4.0.1",
|
|
57
|
+
"mailgun.js": "^13.0.1",
|
|
58
|
+
"nodemailer": "^8.0.5"
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@types/node": "*",
|
|
62
|
+
"@types/nodemailer": "^7.0.0",
|
|
63
|
+
"typescript": "^6.0.2",
|
|
64
|
+
"vitest": "^4.1.2"
|
|
65
|
+
},
|
|
66
|
+
"peerDependencies": {
|
|
67
|
+
"@c9up/ream": "^0.1.0"
|
|
68
|
+
},
|
|
69
|
+
"peerDependenciesMeta": {
|
|
70
|
+
"@c9up/ream": {
|
|
71
|
+
"optional": true
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
"files": [
|
|
75
|
+
"src",
|
|
76
|
+
"dist",
|
|
77
|
+
"README.md",
|
|
78
|
+
"LICENSE",
|
|
79
|
+
"index.*.node",
|
|
80
|
+
"scripts"
|
|
81
|
+
],
|
|
82
|
+
"publishConfig": {
|
|
83
|
+
"access": "public"
|
|
84
|
+
},
|
|
85
|
+
"repository": {
|
|
86
|
+
"type": "git",
|
|
87
|
+
"url": "git+https://github.com/C9up/rover.git"
|
|
88
|
+
},
|
|
89
|
+
"scripts": {
|
|
90
|
+
"build": "tsc -p tsconfig.build.json",
|
|
91
|
+
"build:napi": "cargo build --release -p rover-template-engine-napi && node scripts/copy-napi.mjs",
|
|
92
|
+
"test": "vitest run",
|
|
93
|
+
"lint": "biome check src/",
|
|
94
|
+
"test:coverage": "vitest run --coverage",
|
|
95
|
+
"typecheck": "tsc --noEmit"
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { copyFileSync, existsSync } from 'node:fs'
|
|
2
|
+
import { dirname, join } from 'node:path'
|
|
3
|
+
import { arch, env, platform } from 'node:process'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
|
|
6
|
+
const here = dirname(fileURLToPath(import.meta.url))
|
|
7
|
+
const root = join(here, '..')
|
|
8
|
+
const CRATE = 'rover_template_engine_napi'
|
|
9
|
+
const TAG = '[rover:napi]'
|
|
10
|
+
|
|
11
|
+
// Rust target triple -> { suffix, os }. Set CARGO_BUILD_TARGET to cross-compile
|
|
12
|
+
// (e.g. build x86_64-apple-darwin on an arm64 macOS runner so we don't depend
|
|
13
|
+
// on the scarce macos-13 Intel runners). Unset falls back to host platform.
|
|
14
|
+
const tripleMap = {
|
|
15
|
+
'x86_64-unknown-linux-gnu': { suffix: 'linux-x64-gnu', os: 'linux' },
|
|
16
|
+
'aarch64-unknown-linux-gnu': { suffix: 'linux-arm64-gnu', os: 'linux' },
|
|
17
|
+
'x86_64-apple-darwin': { suffix: 'darwin-x64', os: 'darwin' },
|
|
18
|
+
'aarch64-apple-darwin': { suffix: 'darwin-arm64', os: 'darwin' },
|
|
19
|
+
'x86_64-pc-windows-msvc': { suffix: 'win32-x64-msvc', os: 'win32' },
|
|
20
|
+
}
|
|
21
|
+
const hostSuffixMap = {
|
|
22
|
+
'linux-x64': 'linux-x64-gnu',
|
|
23
|
+
'linux-arm64': 'linux-arm64-gnu',
|
|
24
|
+
'darwin-x64': 'darwin-x64',
|
|
25
|
+
'darwin-arm64': 'darwin-arm64',
|
|
26
|
+
'win32-x64': 'win32-x64-msvc',
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const triple = env.CARGO_BUILD_TARGET ?? ''
|
|
30
|
+
let suffix
|
|
31
|
+
let os
|
|
32
|
+
let releaseDir
|
|
33
|
+
if (triple) {
|
|
34
|
+
const entry = tripleMap[triple]
|
|
35
|
+
if (!entry) throw new Error(`${TAG} unsupported CARGO_BUILD_TARGET: ${triple}`)
|
|
36
|
+
suffix = entry.suffix
|
|
37
|
+
os = entry.os
|
|
38
|
+
releaseDir = join(root, 'target', triple, 'release')
|
|
39
|
+
} else {
|
|
40
|
+
suffix = hostSuffixMap[`${platform}-${arch}`]
|
|
41
|
+
os = platform
|
|
42
|
+
releaseDir = join(root, 'target', 'release')
|
|
43
|
+
if (!suffix) throw new Error(`${TAG} unsupported platform/arch: ${platform}-${arch}`)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const candidates =
|
|
47
|
+
os === 'win32'
|
|
48
|
+
? [join(releaseDir, `${CRATE}.dll`), join(releaseDir, `lib${CRATE}.dll`)]
|
|
49
|
+
: os === 'darwin'
|
|
50
|
+
? [join(releaseDir, `lib${CRATE}.dylib`)]
|
|
51
|
+
: [join(releaseDir, `lib${CRATE}.so`)]
|
|
52
|
+
|
|
53
|
+
const source = candidates.find((candidate) => existsSync(candidate))
|
|
54
|
+
if (!source) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
`${TAG} native library not found. Looked for:\n${candidates.map((p) => `- ${p}`).join('\n')}`,
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const target = join(root, `index.${suffix}.node`)
|
|
61
|
+
copyFileSync(source, target)
|
|
62
|
+
console.log(`${TAG} copied ${source} -> ${target}`)
|
package/src/BaseMail.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { type MailMessage, MessageBuilder } from "./MessageBuilder.js";
|
|
2
|
+
|
|
3
|
+
export type MailAddress = string | { address: string; name: string };
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Abstract base for class-based mail messages (Adonis parity).
|
|
7
|
+
*
|
|
8
|
+
* class WelcomeMail extends BaseMail {
|
|
9
|
+
* from = { address: "noreply@acme.com", name: "Acme" };
|
|
10
|
+
* subject = "Welcome!";
|
|
11
|
+
* constructor(private user: User) { super(); }
|
|
12
|
+
* async prepare() {
|
|
13
|
+
* this.message.to(this.user.email).html(...);
|
|
14
|
+
* }
|
|
15
|
+
* }
|
|
16
|
+
*
|
|
17
|
+
* await mail.send(new WelcomeMail(user));
|
|
18
|
+
*/
|
|
19
|
+
export abstract class BaseMail {
|
|
20
|
+
protected readonly message: MessageBuilder = new MessageBuilder();
|
|
21
|
+
|
|
22
|
+
from?: MailAddress;
|
|
23
|
+
subject?: string;
|
|
24
|
+
|
|
25
|
+
constructor() {
|
|
26
|
+
if (new.target === BaseMail) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
"BaseMail is abstract and cannot be instantiated directly",
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
abstract prepare(): void | Promise<void>;
|
|
34
|
+
|
|
35
|
+
async build(): Promise<MailMessage> {
|
|
36
|
+
if (this.from !== undefined) {
|
|
37
|
+
this.message.from(formatAddress(this.from));
|
|
38
|
+
}
|
|
39
|
+
if (this.subject !== undefined) {
|
|
40
|
+
this.message.subject(this.subject);
|
|
41
|
+
}
|
|
42
|
+
await this.prepare();
|
|
43
|
+
return this.message.build();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function formatAddress(addr: MailAddress): string {
|
|
48
|
+
if (typeof addr === "string") {
|
|
49
|
+
return addr;
|
|
50
|
+
}
|
|
51
|
+
return `"${addr.name}" <${addr.address}>`;
|
|
52
|
+
}
|