@stacksjs/email 0.70.88 → 0.70.91

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 (65) hide show
  1. package/dist/css-inliner.d.ts +26 -0
  2. package/dist/css-inliner.js +172 -0
  3. package/dist/drivers/base.d.ts +14 -0
  4. package/dist/drivers/base.js +128 -0
  5. package/dist/drivers/capture.d.ts +55 -0
  6. package/dist/drivers/capture.js +30 -0
  7. package/dist/drivers/index.d.ts +10 -0
  8. package/dist/drivers/index.js +6 -0
  9. package/dist/drivers/log.d.ts +30 -0
  10. package/dist/drivers/log.js +81 -0
  11. package/dist/drivers/mailgun.d.ts +8 -0
  12. package/dist/drivers/mailgun.js +127 -0
  13. package/dist/drivers/mailtrap.d.ts +8 -0
  14. package/dist/drivers/mailtrap.js +129 -0
  15. package/dist/drivers/sendgrid.d.ts +8 -0
  16. package/dist/drivers/sendgrid.js +136 -0
  17. package/dist/drivers/ses.d.ts +8 -0
  18. package/dist/drivers/ses.js +113 -0
  19. package/dist/drivers/smtp.d.ts +14 -0
  20. package/dist/drivers/smtp.js +225 -0
  21. package/dist/email.d.ts +39 -0
  22. package/dist/email.js +198 -0
  23. package/dist/idempotency.d.ts +20 -0
  24. package/dist/idempotency.js +56 -0
  25. package/dist/index.d.ts +22 -0
  26. package/dist/index.js +19 -0
  27. package/dist/mailable.d.ts +116 -0
  28. package/dist/mailable.js +145 -0
  29. package/dist/mime.d.ts +37 -0
  30. package/dist/mime.js +89 -0
  31. package/dist/preview-ui.d.ts +11 -0
  32. package/dist/preview-ui.js +132 -0
  33. package/dist/preview.d.ts +50 -0
  34. package/dist/preview.js +97 -0
  35. package/dist/sdk/index.d.ts +81 -0
  36. package/dist/sdk/index.js +219 -0
  37. package/dist/send.d.ts +1 -0
  38. package/dist/send.js +0 -0
  39. package/dist/server/converter.d.ts +1 -0
  40. package/dist/server/converter.js +0 -0
  41. package/dist/server/inbound.d.ts +1 -0
  42. package/dist/server/inbound.js +0 -0
  43. package/dist/server/outbound.d.ts +1 -0
  44. package/dist/server/outbound.js +0 -0
  45. package/dist/suppression.d.ts +82 -0
  46. package/dist/suppression.js +104 -0
  47. package/dist/template.d.ts +99 -0
  48. package/dist/template.js +170 -0
  49. package/dist/types.d.ts +37 -0
  50. package/dist/types.js +0 -0
  51. package/dist/unsubscribe.d.ts +39 -0
  52. package/dist/unsubscribe.js +65 -0
  53. package/dist/utils/config.d.ts +3 -0
  54. package/dist/utils/config.js +3 -0
  55. package/dist/validation.d.ts +48 -0
  56. package/dist/validation.js +22 -0
  57. package/dist/webhook-dedup.d.ts +10 -0
  58. package/dist/webhook-dedup.js +33 -0
  59. package/dist/webhook-events.d.ts +34 -0
  60. package/dist/webhook-events.js +37 -0
  61. package/dist/webhook-handlers.d.ts +27 -0
  62. package/dist/webhook-handlers.js +264 -0
  63. package/dist/webhook-signatures.d.ts +91 -0
  64. package/dist/webhook-signatures.js +148 -0
  65. package/package.json +5 -5
@@ -0,0 +1,264 @@
1
+ import { Buffer } from "node:buffer";
2
+ import { suppress } from "./suppression";
3
+ import { recordWebhookEventOrSkip } from "./webhook-dedup";
4
+ import {
5
+ emitEmailBounceHard,
6
+ emitEmailBounceSoft,
7
+ emitEmailComplaint,
8
+ emitEmailUnsubscribe,
9
+ suppressionTypeFor
10
+ } from "./webhook-events";
11
+ import {
12
+ verifyMailgunSignature,
13
+ verifyPostmarkAuth,
14
+ verifySendgridSignature,
15
+ verifySesSnsSignature
16
+ } from "./webhook-signatures";
17
+ const OK_DUPLICATE = { status: 200, body: { ok: !0, processed: !1, reason: "duplicate" } };
18
+ function unauthorized(reason) {
19
+ return { status: 401, body: { ok: !1, reason } };
20
+ }
21
+ function badRequest(reason) {
22
+ return { status: 400, body: { ok: !1, reason } };
23
+ }
24
+ async function dispatchClassifiedEvent(classification, payload) {
25
+ const suppressionType = suppressionTypeFor(classification);
26
+ if (suppressionType)
27
+ await suppress(payload.email, suppressionType, payload.reason);
28
+ switch (classification) {
29
+ case "bounce-hard":
30
+ await emitEmailBounceHard(payload);
31
+ break;
32
+ case "bounce-soft":
33
+ await emitEmailBounceSoft(payload);
34
+ break;
35
+ case "complaint":
36
+ await emitEmailComplaint(payload);
37
+ break;
38
+ case "unsubscribe":
39
+ await emitEmailUnsubscribe(payload);
40
+ break;
41
+ case "delivered":
42
+ break;
43
+ }
44
+ }
45
+ export async function handleMailgunWebhook(rawBody, config) {
46
+ let parsed;
47
+ try {
48
+ parsed = JSON.parse(rawBody);
49
+ } catch {
50
+ return badRequest("invalid-json");
51
+ }
52
+ const sig = parsed.signature, event = parsed["event-data"];
53
+ if (!sig || !event)
54
+ return badRequest("missing-fields");
55
+ const v = verifyMailgunSignature({
56
+ timestamp: sig.timestamp,
57
+ token: sig.token,
58
+ signature: sig.signature,
59
+ signingKey: config.signingKey,
60
+ toleranceSeconds: config.toleranceSeconds
61
+ });
62
+ if (!v.ok)
63
+ return unauthorized(v.reason);
64
+ if (!await recordWebhookEventOrSkip("mailgun", event.id))
65
+ return OK_DUPLICATE;
66
+ const classification = classifyMailgunEvent(event.event, event.severity);
67
+ if (!classification)
68
+ return { status: 200, body: { ok: !0, processed: !1, reason: "unhandled-event" } };
69
+ await dispatchClassifiedEvent(classification, {
70
+ email: event.recipient,
71
+ provider: "mailgun",
72
+ reason: event.reason,
73
+ raw: event
74
+ });
75
+ return { status: 200, body: { ok: !0, processed: !0, classification } };
76
+ }
77
+ function classifyMailgunEvent(event, severity) {
78
+ switch (event) {
79
+ case "failed":
80
+ return severity === "temporary" ? "bounce-soft" : "bounce-hard";
81
+ case "complained":
82
+ return "complaint";
83
+ case "unsubscribed":
84
+ return "unsubscribe";
85
+ case "delivered":
86
+ return "delivered";
87
+ default:
88
+ return null;
89
+ }
90
+ }
91
+ export async function handlePostmarkWebhook(rawBody, authorizationHeader, sourceIp, config) {
92
+ const v = verifyPostmarkAuth({
93
+ authorizationHeader,
94
+ expectedUsername: config.username,
95
+ expectedPassword: config.password,
96
+ sourceIp,
97
+ ipAllowlist: config.ipAllowlist
98
+ });
99
+ if (!v.ok)
100
+ return unauthorized(v.reason);
101
+ let parsed;
102
+ try {
103
+ parsed = JSON.parse(rawBody);
104
+ } catch {
105
+ return badRequest("invalid-json");
106
+ }
107
+ const eventId = String(parsed.ID ?? parsed.MessageID ?? ""), email = String(parsed.Email ?? parsed.Recipient ?? "");
108
+ if (!email)
109
+ return badRequest("missing-recipient");
110
+ if (!await recordWebhookEventOrSkip("postmark", eventId))
111
+ return OK_DUPLICATE;
112
+ const classification = classifyPostmarkEvent(parsed);
113
+ if (!classification)
114
+ return { status: 200, body: { ok: !0, processed: !1, reason: "unhandled-event" } };
115
+ await dispatchClassifiedEvent(classification, {
116
+ email,
117
+ provider: "postmark",
118
+ reason: parsed.Description,
119
+ raw: parsed
120
+ });
121
+ return { status: 200, body: { ok: !0, processed: !0, classification } };
122
+ }
123
+ function classifyPostmarkEvent(msg) {
124
+ switch (msg.RecordType) {
125
+ case "Bounce":
126
+ return msg.TypeCode === 1 ? "bounce-hard" : "bounce-soft";
127
+ case "SpamComplaint":
128
+ return "complaint";
129
+ case "SubscriptionChange":
130
+ return msg.SuppressSending ? "unsubscribe" : null;
131
+ case "Delivery":
132
+ return "delivered";
133
+ default:
134
+ return null;
135
+ }
136
+ }
137
+ export async function handleSesWebhook(rawBody, config = {}) {
138
+ let snsMessage;
139
+ try {
140
+ snsMessage = JSON.parse(rawBody);
141
+ } catch {
142
+ return badRequest("invalid-json");
143
+ }
144
+ const v = await verifySesSnsSignature({
145
+ message: snsMessage,
146
+ certUrlHostAllowlist: config.certUrlHostAllowlist,
147
+ fetchCert: config.fetchCert
148
+ });
149
+ if (!v.ok)
150
+ return unauthorized(v.reason);
151
+ if (snsMessage.Type === "SubscriptionConfirmation") {
152
+ if (config.autoConfirmSubscriptions !== !1 && snsMessage.SubscribeURL)
153
+ try {
154
+ await fetch(snsMessage.SubscribeURL);
155
+ } catch {}
156
+ return { status: 200, body: { ok: !0, processed: !0, reason: "subscription-confirmed" } };
157
+ }
158
+ if (snsMessage.Type === "UnsubscribeConfirmation")
159
+ return { status: 200, body: { ok: !0, processed: !0, reason: "subscription-removed" } };
160
+ let innerMessage;
161
+ try {
162
+ innerMessage = JSON.parse(snsMessage.Message);
163
+ } catch {
164
+ return badRequest("invalid-inner-json");
165
+ }
166
+ if (!await recordWebhookEventOrSkip("ses", snsMessage.MessageId))
167
+ return OK_DUPLICATE;
168
+ const dispatched = [];
169
+ if (innerMessage.notificationType === "Bounce" && innerMessage.bounce) {
170
+ const classification = innerMessage.bounce.bounceType === "Permanent" ? "bounce-hard" : "bounce-soft";
171
+ for (const r of innerMessage.bounce.bouncedRecipients) {
172
+ await dispatchClassifiedEvent(classification, {
173
+ email: r.emailAddress,
174
+ provider: "ses",
175
+ reason: r.diagnosticCode,
176
+ raw: innerMessage
177
+ });
178
+ dispatched.push(classification);
179
+ }
180
+ } else if (innerMessage.notificationType === "Complaint" && innerMessage.complaint)
181
+ for (const r of innerMessage.complaint.complainedRecipients) {
182
+ await dispatchClassifiedEvent("complaint", {
183
+ email: r.emailAddress,
184
+ provider: "ses",
185
+ reason: innerMessage.complaint.complaintFeedbackType,
186
+ raw: innerMessage
187
+ });
188
+ dispatched.push("complaint");
189
+ }
190
+ else if (innerMessage.notificationType === "Delivery" && innerMessage.delivery)
191
+ for (const r of innerMessage.delivery.recipients) {
192
+ await dispatchClassifiedEvent("delivered", { email: r, provider: "ses", raw: innerMessage });
193
+ dispatched.push("delivered");
194
+ }
195
+ return {
196
+ status: 200,
197
+ body: {
198
+ ok: !0,
199
+ processed: dispatched.length > 0,
200
+ classification: dispatched[0]
201
+ }
202
+ };
203
+ }
204
+ export async function handleSendgridWebhook(rawBody, signatureHeader, timestampHeader, config) {
205
+ const v = verifySendgridSignature({
206
+ body: rawBody,
207
+ signature: signatureHeader,
208
+ timestamp: timestampHeader,
209
+ publicKeyPem: config.publicKeyPem,
210
+ toleranceSeconds: config.toleranceSeconds
211
+ });
212
+ if (!v.ok)
213
+ return unauthorized(v.reason);
214
+ let events;
215
+ try {
216
+ events = JSON.parse(rawBody);
217
+ } catch {
218
+ return badRequest("invalid-json");
219
+ }
220
+ if (!Array.isArray(events))
221
+ return badRequest("expected-array");
222
+ let processed = 0;
223
+ const classifications = [];
224
+ for (const ev of events) {
225
+ if (!ev.email)
226
+ continue;
227
+ const id = ev.sg_event_id ?? `${ev.event}:${ev.email}:${Date.now()}`;
228
+ if (!await recordWebhookEventOrSkip("sendgrid", id))
229
+ continue;
230
+ const classification = classifySendgridEvent(ev.event, ev.type);
231
+ if (!classification)
232
+ continue;
233
+ await dispatchClassifiedEvent(classification, {
234
+ email: ev.email,
235
+ provider: "sendgrid",
236
+ reason: ev.reason,
237
+ raw: ev
238
+ });
239
+ processed++;
240
+ classifications.push(classification);
241
+ }
242
+ return {
243
+ status: 200,
244
+ body: { ok: !0, processed: processed > 0, classification: classifications[0] }
245
+ };
246
+ }
247
+ function classifySendgridEvent(event, type) {
248
+ switch (event) {
249
+ case "bounce":
250
+ return type === "blocked" ? "bounce-soft" : "bounce-hard";
251
+ case "dropped":
252
+ return "bounce-hard";
253
+ case "spamreport":
254
+ return "complaint";
255
+ case "unsubscribe":
256
+ return "unsubscribe";
257
+ case "group_unsubscribe":
258
+ return "unsubscribe";
259
+ case "delivered":
260
+ return "delivered";
261
+ default:
262
+ return null;
263
+ }
264
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Verify a Mailgun webhook signature. Mailgun signs
3
+ * `${timestamp}${token}` with HMAC-SHA256; the hex digest is
4
+ * compared in constant time against the `signature` field.
5
+ *
6
+ * The timestamp is checked against wall clock with a configurable
7
+ * tolerance (default 5 minutes) to reject replays. Constant-time
8
+ * compare prevents signature-timing oracles.
9
+ */
10
+ export declare function verifyMailgunSignature(input: MailgunSignatureInput): SignatureVerification;
11
+ /**
12
+ * Verify a Postmark webhook. Postmark uses HTTP Basic Auth (the app
13
+ * configures the username + password when registering the webhook in
14
+ * the Postmark dashboard); they don't sign the body. The auth check
15
+ * is constant-time. Optional IP-allowlist check rejects requests
16
+ * from outside Postmark's published source IPs.
17
+ */
18
+ export declare function verifyPostmarkAuth(input: PostmarkAuthInput): SignatureVerification;
19
+ /**
20
+ * Verify an SNS message signature (SES uses SNS for delivery). The
21
+ * cert URL host MUST match `sns.<region>.amazonaws.com` — any other
22
+ * host is treated as untrusted (defense against SSRF via crafted
23
+ * SigningCertURL).
24
+ *
25
+ * Note: this function intentionally only does the structural +
26
+ * cert-URL check + signature verify. SubscriptionConfirmation
27
+ * handling (responding to `SubscribeURL` to complete the topic
28
+ * binding) is the route handler's job since it's a one-time setup
29
+ * action distinct from per-event verification.
30
+ */
31
+ export declare function verifySesSnsSignature(input: SesSnsSignatureInput): Promise<SignatureVerification>;
32
+ /**
33
+ * Verify a SendGrid Event Webhook signature. SendGrid signs
34
+ * `${timestamp}${body}` with ECDSA-SHA256 using the public key from
35
+ * their signed-webhook setup page. Signature is base64 in the
36
+ * `X-Twilio-Email-Event-Webhook-Signature` header.
37
+ */
38
+ export declare function verifySendgridSignature(input: SendgridSignatureInput): SignatureVerification;
39
+ // =============================================================================
40
+ // Mailgun
41
+ // =============================================================================
42
+ export declare interface MailgunSignatureInput {
43
+ timestamp: string
44
+ token: string
45
+ signature: string
46
+ signingKey: string
47
+ toleranceSeconds?: number
48
+ }
49
+ // =============================================================================
50
+ // Postmark
51
+ // =============================================================================
52
+ export declare interface PostmarkAuthInput {
53
+ authorizationHeader: string | null | undefined
54
+ expectedUsername: string
55
+ expectedPassword: string
56
+ sourceIp?: string
57
+ ipAllowlist?: ReadonlyArray<string>
58
+ }
59
+ // =============================================================================
60
+ // SES (via SNS)
61
+ // =============================================================================
62
+ export declare interface SesSnsSignatureInput {
63
+ message: {
64
+ Type: 'Notification' | 'SubscriptionConfirmation' | 'UnsubscribeConfirmation'
65
+ MessageId: string
66
+ TopicArn: string
67
+ Subject?: string
68
+ Message: string
69
+ Timestamp: string
70
+ SignatureVersion: string
71
+ Signature: string
72
+ SigningCertURL: string
73
+ Token?: string
74
+ SubscribeURL?: string
75
+ [key: string]: unknown
76
+ }
77
+ fetchCert?: (url: string) => Promise<string>
78
+ certUrlHostAllowlist?: RegExp
79
+ }
80
+ // =============================================================================
81
+ // SendGrid
82
+ // =============================================================================
83
+ export declare interface SendgridSignatureInput {
84
+ body: string
85
+ signature: string | null | undefined
86
+ timestamp: string | null | undefined
87
+ publicKeyPem: string
88
+ toleranceSeconds?: number
89
+ }
90
+ export type SignatureVerification = | { ok: true }
91
+ | { ok: false, reason: 'missing-config' | 'missing-signature' | 'bad-signature' | 'expired' | 'untrusted-cert-url' | 'cert-fetch-failed' }
@@ -0,0 +1,148 @@
1
+ import { createHmac, createVerify, timingSafeEqual } from "node:crypto";
2
+ import { Buffer } from "node:buffer";
3
+ export function verifyMailgunSignature(input) {
4
+ if (!input.signingKey)
5
+ return { ok: !1, reason: "missing-config" };
6
+ if (!input.timestamp || !input.token || !input.signature)
7
+ return { ok: !1, reason: "missing-signature" };
8
+ const tolerance = input.toleranceSeconds ?? 300, ts = Number(input.timestamp);
9
+ if (!Number.isFinite(ts))
10
+ return { ok: !1, reason: "bad-signature" };
11
+ const now = Math.floor(Date.now() / 1000);
12
+ if (Math.abs(now - ts) > tolerance)
13
+ return { ok: !1, reason: "expired" };
14
+ const expected = createHmac("sha256", input.signingKey).update(`${input.timestamp}${input.token}`).digest("hex");
15
+ let provided;
16
+ try {
17
+ provided = Buffer.from(input.signature, "hex");
18
+ } catch {
19
+ return { ok: !1, reason: "bad-signature" };
20
+ }
21
+ const expectedBuf = Buffer.from(expected, "hex");
22
+ if (provided.length !== expectedBuf.length)
23
+ return { ok: !1, reason: "bad-signature" };
24
+ if (!timingSafeEqual(provided, expectedBuf))
25
+ return { ok: !1, reason: "bad-signature" };
26
+ return { ok: !0 };
27
+ }
28
+ export function verifyPostmarkAuth(input) {
29
+ if (!input.expectedUsername || !input.expectedPassword)
30
+ return { ok: !1, reason: "missing-config" };
31
+ if (!input.authorizationHeader || !input.authorizationHeader.startsWith("Basic "))
32
+ return { ok: !1, reason: "missing-signature" };
33
+ let decoded;
34
+ try {
35
+ decoded = Buffer.from(input.authorizationHeader.slice(6), "base64").toString("utf8");
36
+ } catch {
37
+ return { ok: !1, reason: "bad-signature" };
38
+ }
39
+ const [user, pass] = decoded.split(":");
40
+ if (!user || pass === void 0)
41
+ return { ok: !1, reason: "bad-signature" };
42
+ const userMatch = safeStringEquals(user, input.expectedUsername), passMatch = safeStringEquals(pass, input.expectedPassword);
43
+ if (!userMatch || !passMatch)
44
+ return { ok: !1, reason: "bad-signature" };
45
+ if (input.ipAllowlist && input.ipAllowlist.length > 0) {
46
+ if (!input.sourceIp || !input.ipAllowlist.includes(input.sourceIp))
47
+ return { ok: !1, reason: "bad-signature" };
48
+ }
49
+ return { ok: !0 };
50
+ }
51
+ function safeStringEquals(a, b) {
52
+ const aBuf = Buffer.from(a, "utf8"), bBuf = Buffer.from(b, "utf8");
53
+ if (aBuf.length !== bBuf.length) {
54
+ const dummy = Buffer.alloc(Math.max(aBuf.length, bBuf.length));
55
+ timingSafeEqual(dummy, dummy);
56
+ return !1;
57
+ }
58
+ return timingSafeEqual(aBuf, bBuf);
59
+ }
60
+ const DEFAULT_SNS_HOST_ALLOWLIST = /^sns\.[a-z0-9-]+\.amazonaws\.com$/;
61
+ async function defaultCertFetcher(url) {
62
+ const res = await fetch(url, { redirect: "error" });
63
+ if (!res.ok)
64
+ throw Error(`SNS cert fetch returned ${res.status}`);
65
+ return await res.text();
66
+ }
67
+ export async function verifySesSnsSignature(input) {
68
+ const msg = input.message;
69
+ if (!msg || !msg.Signature || !msg.SigningCertURL)
70
+ return { ok: !1, reason: "missing-signature" };
71
+ const allowlist = input.certUrlHostAllowlist ?? DEFAULT_SNS_HOST_ALLOWLIST;
72
+ let certUrl;
73
+ try {
74
+ certUrl = new URL(msg.SigningCertURL);
75
+ } catch {
76
+ return { ok: !1, reason: "untrusted-cert-url" };
77
+ }
78
+ if (certUrl.protocol !== "https:")
79
+ return { ok: !1, reason: "untrusted-cert-url" };
80
+ if (!allowlist.test(certUrl.host))
81
+ return { ok: !1, reason: "untrusted-cert-url" };
82
+ let certPem;
83
+ try {
84
+ certPem = await (input.fetchCert ?? defaultCertFetcher)(msg.SigningCertURL);
85
+ } catch {
86
+ return { ok: !1, reason: "cert-fetch-failed" };
87
+ }
88
+ const canonical = canonicalSnsString(msg);
89
+ if (!canonical)
90
+ return { ok: !1, reason: "bad-signature" };
91
+ let sigBuf;
92
+ try {
93
+ sigBuf = Buffer.from(msg.Signature, "base64");
94
+ } catch {
95
+ return { ok: !1, reason: "bad-signature" };
96
+ }
97
+ const algo = msg.SignatureVersion === "2" ? "SHA256" : "SHA1", verifier = createVerify(`RSA-${algo}`);
98
+ verifier.update(canonical, "utf8");
99
+ return verifier.verify(certPem, sigBuf) ? { ok: !0 } : { ok: !1, reason: "bad-signature" };
100
+ }
101
+ function canonicalSnsString(msg) {
102
+ const fields = [];
103
+ if (msg.Type === "Notification") {
104
+ fields.push("Message", msg.Message);
105
+ fields.push("MessageId", msg.MessageId);
106
+ if (msg.Subject !== void 0)
107
+ fields.push("Subject", msg.Subject);
108
+ fields.push("Timestamp", msg.Timestamp);
109
+ fields.push("TopicArn", msg.TopicArn);
110
+ fields.push("Type", msg.Type);
111
+ } else if (msg.Type === "SubscriptionConfirmation" || msg.Type === "UnsubscribeConfirmation") {
112
+ fields.push("Message", msg.Message);
113
+ fields.push("MessageId", msg.MessageId);
114
+ if (!msg.SubscribeURL || !msg.Token)
115
+ return null;
116
+ fields.push("SubscribeURL", msg.SubscribeURL);
117
+ fields.push("Timestamp", msg.Timestamp);
118
+ fields.push("Token", msg.Token);
119
+ fields.push("TopicArn", msg.TopicArn);
120
+ fields.push("Type", msg.Type);
121
+ } else
122
+ return null;
123
+ let out = "";
124
+ for (const f of fields)
125
+ out += `${f}
126
+ `;
127
+ return out;
128
+ }
129
+ export function verifySendgridSignature(input) {
130
+ if (!input.publicKeyPem)
131
+ return { ok: !1, reason: "missing-config" };
132
+ if (!input.signature || !input.timestamp)
133
+ return { ok: !1, reason: "missing-signature" };
134
+ const tolerance = input.toleranceSeconds ?? 300, ts = Number(input.timestamp);
135
+ if (!Number.isFinite(ts))
136
+ return { ok: !1, reason: "bad-signature" };
137
+ if (Math.abs(Math.floor(Date.now() / 1000) - ts) > tolerance)
138
+ return { ok: !1, reason: "expired" };
139
+ let sigBuf;
140
+ try {
141
+ sigBuf = Buffer.from(input.signature, "base64");
142
+ } catch {
143
+ return { ok: !1, reason: "bad-signature" };
144
+ }
145
+ const verifier = createVerify("SHA256");
146
+ verifier.update(`${input.timestamp}${input.body}`, "utf8");
147
+ return verifier.verify(input.publicKeyPem, sigBuf) ? { ok: !0 } : { ok: !1, reason: "bad-signature" };
148
+ }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/email",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.70.88",
5
+ "version": "0.70.91",
6
6
  "description": "The Stacks Email integration. Painlessly create & manage your inboxes, templates, and send emails.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -58,10 +58,10 @@
58
58
  "@stacksjs/ts-cloud": "^0.7.17"
59
59
  },
60
60
  "devDependencies": {
61
- "@stacksjs/cli": "0.70.88",
62
- "@stacksjs/config": "0.70.88",
61
+ "@stacksjs/cli": "0.70.91",
62
+ "@stacksjs/config": "0.70.91",
63
63
  "better-dx": "^0.2.16",
64
- "@stacksjs/error-handling": "0.70.88",
65
- "@stacksjs/types": "0.70.88"
64
+ "@stacksjs/error-handling": "0.70.91",
65
+ "@stacksjs/types": "0.70.91"
66
66
  }
67
67
  }