@porulle/adapter-ses 0.1.0
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 +37 -0
- package/dist/index.d.ts +50 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +79 -0
- package/package.json +49 -0
- package/src/index.ts +119 -0
package/README.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# @porulle/adapter-ses
|
|
2
|
+
|
|
3
|
+
Transactional email via [AWS SES v2](https://docs.aws.amazon.com/ses/latest/dg/Welcome.html).
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { defineConfig } from "@porulle/core";
|
|
9
|
+
import { sesEmailAdapter } from "@porulle/adapter-ses";
|
|
10
|
+
|
|
11
|
+
export default defineConfig({
|
|
12
|
+
email: {
|
|
13
|
+
send: sesEmailAdapter({
|
|
14
|
+
region: "us-east-1",
|
|
15
|
+
from: "Acme Store <orders@acme.com>", // must be a verified SES identity
|
|
16
|
+
credentials: { // optional — falls back to AWS credential chain
|
|
17
|
+
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
|
|
18
|
+
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
|
|
19
|
+
},
|
|
20
|
+
subjects: { "order-confirmation": (d) => `Order ${d.orderNumber}` },
|
|
21
|
+
templates: { "order-confirmation": (d) => `<p>Thanks!</p>` },
|
|
22
|
+
}),
|
|
23
|
+
},
|
|
24
|
+
// …
|
|
25
|
+
});
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Notes
|
|
29
|
+
|
|
30
|
+
- The `from` address must be a verified identity in SES (domain or address). If you're in the SES sandbox, all recipients must be verified too.
|
|
31
|
+
- Credentials are optional — the adapter uses the default AWS credential chain (env vars, IAM role, profile) if `credentials` is omitted.
|
|
32
|
+
- For Workers / non-Node runtimes, prefer `@porulle/adapter-resend` (the AWS SDK isn't worker-friendly).
|
|
33
|
+
|
|
34
|
+
## See also
|
|
35
|
+
|
|
36
|
+
- [AWS SES v2 API reference](https://docs.aws.amazon.com/ses/latest/APIReference-V2/Welcome.html)
|
|
37
|
+
- `@porulle/adapter-resend` — simpler, edge-friendly alternative
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export interface SESAdapterOptions {
|
|
2
|
+
/** AWS region (e.g., "us-east-1"). */
|
|
3
|
+
region: string;
|
|
4
|
+
/** Default sender address (e.g., "Acme Store <orders@acme.com>"). Must be verified in SES. */
|
|
5
|
+
from: string;
|
|
6
|
+
/** AWS credentials. If omitted, uses the default credential chain (env vars, IAM role, etc.). */
|
|
7
|
+
credentials?: {
|
|
8
|
+
accessKeyId: string;
|
|
9
|
+
secretAccessKey: string;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Maps template names to subject line generators.
|
|
13
|
+
* If a template is not in this map, the subject defaults to the template name.
|
|
14
|
+
*/
|
|
15
|
+
subjects?: Record<string, (data: Record<string, unknown>) => string>;
|
|
16
|
+
/**
|
|
17
|
+
* Maps template names to HTML body generators.
|
|
18
|
+
* If a template is not in this map, a minimal default is used.
|
|
19
|
+
*/
|
|
20
|
+
templates?: Record<string, (data: Record<string, unknown>) => string>;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Creates an email adapter backed by AWS SES v2.
|
|
24
|
+
*
|
|
25
|
+
* Implements the `config.email.send()` interface consumed by checkout hooks,
|
|
26
|
+
* auth (password reset, email verification), and appointment plugin notifications.
|
|
27
|
+
*
|
|
28
|
+
* Sender address must be verified in SES. If your account is in the SES sandbox,
|
|
29
|
+
* recipient addresses must also be verified.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```typescript
|
|
33
|
+
* import { sesAdapter } from "@porulle/adapter-ses";
|
|
34
|
+
*
|
|
35
|
+
* export default defineConfig({
|
|
36
|
+
* email: sesAdapter({
|
|
37
|
+
* region: "us-east-1",
|
|
38
|
+
* from: "Acme Store <orders@acme.com>",
|
|
39
|
+
* }),
|
|
40
|
+
* });
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export declare function sesAdapter(options: SESAdapterOptions): {
|
|
44
|
+
send(input: {
|
|
45
|
+
template: string;
|
|
46
|
+
to: string;
|
|
47
|
+
data?: Record<string, unknown>;
|
|
48
|
+
}): Promise<void>;
|
|
49
|
+
};
|
|
50
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,iBAAiB;IAChC,sCAAsC;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,8FAA8F;IAC9F,IAAI,EAAE,MAAM,CAAC;IACb,iGAAiG;IACjG,WAAW,CAAC,EAAE;QACZ,WAAW,EAAE,MAAM,CAAC;QACpB,eAAe,EAAE,MAAM,CAAC;KACzB,CAAC;IACF;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,MAAM,CAAC,CAAC;IACrE;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,MAAM,CAAC,CAAC;CACvE;AAgCD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG;IACtD,IAAI,CAAC,KAAK,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9F,CAyCA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { SESv2Client, SendEmailCommand } from "@aws-sdk/client-sesv2";
|
|
2
|
+
const DEFAULT_SUBJECTS = {
|
|
3
|
+
"order-confirmation": (d) => `Order Confirmed${d.orderId ? ` - #${String(d.orderId).slice(0, 8)}` : ""}`,
|
|
4
|
+
"order-status-change": (d) => `Order Update - ${d.newStatus ?? "Status Changed"}`,
|
|
5
|
+
"password-reset": () => "Reset Your Password",
|
|
6
|
+
"email-verification": () => "Verify Your Email Address",
|
|
7
|
+
"appointment:reminder": (d) => `Appointment Reminder${d.reminderType === "1h" ? " - Starting Soon" : ""}`,
|
|
8
|
+
"appointment:confirmation-notice": () => "Appointment Confirmed",
|
|
9
|
+
"appointment:cancellation-notice": () => "Appointment Cancelled",
|
|
10
|
+
"appointment:no-show-notice": () => "Missed Appointment",
|
|
11
|
+
};
|
|
12
|
+
const DEFAULT_TEMPLATES = {
|
|
13
|
+
"order-confirmation": (d) => `<h2>Order Confirmed</h2><p>Thank you for your order${d.orderId ? ` <strong>#${String(d.orderId).slice(0, 8)}</strong>` : ""}.</p>${d.total ? `<p>Total: ${d.currency ?? "USD"} ${String(d.total)}</p>` : ""}`,
|
|
14
|
+
"order-status-change": (d) => `<h2>Order Update</h2><p>Your order${d.orderId ? ` <strong>#${String(d.orderId).slice(0, 8)}</strong>` : ""} status has been updated to <strong>${d.newStatus ?? "unknown"}</strong>.</p>`,
|
|
15
|
+
"password-reset": (d) => `<h2>Reset Your Password</h2><p>Click the link below to reset your password:</p><p><a href="${d.url ?? "#"}">Reset Password</a></p>`,
|
|
16
|
+
"email-verification": (d) => `<h2>Verify Your Email</h2><p>Click the link below to verify your email address:</p><p><a href="${d.url ?? "#"}">Verify Email</a></p>`,
|
|
17
|
+
"appointment:reminder": (d) => `<h2>Appointment Reminder</h2><p>This is a${d.reminderType === "1h" ? " 1-hour" : " 24-hour"} reminder for your upcoming appointment.</p><p>Booking ID: ${d.bookingId ?? "N/A"}</p>`,
|
|
18
|
+
"appointment:confirmation-notice": (d) => `<h2>Appointment Confirmed</h2><p>Your appointment has been confirmed.</p><p>Booking ID: ${d.bookingId ?? "N/A"}</p>`,
|
|
19
|
+
"appointment:cancellation-notice": (d) => `<h2>Appointment Cancelled</h2><p>Your appointment has been cancelled.</p><p>Booking ID: ${d.bookingId ?? "N/A"}</p>`,
|
|
20
|
+
"appointment:no-show-notice": (d) => `<h2>Missed Appointment</h2><p>You missed your appointment. Please contact us to rebook.</p><p>Booking ID: ${d.bookingId ?? "N/A"}</p>`,
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Creates an email adapter backed by AWS SES v2.
|
|
24
|
+
*
|
|
25
|
+
* Implements the `config.email.send()` interface consumed by checkout hooks,
|
|
26
|
+
* auth (password reset, email verification), and appointment plugin notifications.
|
|
27
|
+
*
|
|
28
|
+
* Sender address must be verified in SES. If your account is in the SES sandbox,
|
|
29
|
+
* recipient addresses must also be verified.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```typescript
|
|
33
|
+
* import { sesAdapter } from "@porulle/adapter-ses";
|
|
34
|
+
*
|
|
35
|
+
* export default defineConfig({
|
|
36
|
+
* email: sesAdapter({
|
|
37
|
+
* region: "us-east-1",
|
|
38
|
+
* from: "Acme Store <orders@acme.com>",
|
|
39
|
+
* }),
|
|
40
|
+
* });
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export function sesAdapter(options) {
|
|
44
|
+
const client = new SESv2Client({
|
|
45
|
+
region: options.region,
|
|
46
|
+
...(options.credentials ? { credentials: options.credentials } : {}),
|
|
47
|
+
});
|
|
48
|
+
const subjects = { ...DEFAULT_SUBJECTS, ...options.subjects };
|
|
49
|
+
const templates = { ...DEFAULT_TEMPLATES, ...options.templates };
|
|
50
|
+
return {
|
|
51
|
+
async send(input) {
|
|
52
|
+
const data = input.data ?? {};
|
|
53
|
+
const subjectFn = subjects[input.template];
|
|
54
|
+
const subject = subjectFn ? subjectFn(data) : input.template;
|
|
55
|
+
const templateFn = templates[input.template];
|
|
56
|
+
const html = templateFn
|
|
57
|
+
? templateFn(data)
|
|
58
|
+
: `<p>Notification: ${input.template}</p><pre>${JSON.stringify(data, null, 2)}</pre>`;
|
|
59
|
+
const command = new SendEmailCommand({
|
|
60
|
+
FromEmailAddress: options.from,
|
|
61
|
+
Destination: {
|
|
62
|
+
ToAddresses: [input.to],
|
|
63
|
+
},
|
|
64
|
+
Content: {
|
|
65
|
+
Simple: {
|
|
66
|
+
Subject: { Data: subject, Charset: "UTF-8" },
|
|
67
|
+
Body: {
|
|
68
|
+
Html: { Data: html, Charset: "UTF-8" },
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
const result = await client.send(command);
|
|
74
|
+
if (!result.MessageId) {
|
|
75
|
+
throw new Error("SES email failed: no MessageId returned");
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@porulle/adapter-ses",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"bun": "./src/index.ts",
|
|
9
|
+
"import": "./dist/index.js",
|
|
10
|
+
"types": "./src/index.ts"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "rm -rf dist tsconfig.build.tsbuildinfo && tsc -p tsconfig.build.json",
|
|
15
|
+
"check-types": "tsc --noEmit",
|
|
16
|
+
"lint": "eslint . --max-warnings 1000",
|
|
17
|
+
"test": "vitest run"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@aws-sdk/client-sesv2": "^3.700.0"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@repo/eslint-config": "*",
|
|
24
|
+
"@repo/typescript-config": "*",
|
|
25
|
+
"@types/node": "^24.5.2",
|
|
26
|
+
"eslint": "^9.39.1",
|
|
27
|
+
"typescript": "5.9.2",
|
|
28
|
+
"vitest": "^3.2.4"
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"src",
|
|
35
|
+
"dist",
|
|
36
|
+
"README.md"
|
|
37
|
+
],
|
|
38
|
+
"description": "Transactional email via AWS SES v2.",
|
|
39
|
+
"homepage": "https://porulle-docs.vercel.app",
|
|
40
|
+
"bugs": {
|
|
41
|
+
"url": "https://github.com/asyncdotengineering/porulle/issues"
|
|
42
|
+
},
|
|
43
|
+
"repository": {
|
|
44
|
+
"type": "git",
|
|
45
|
+
"url": "git+https://github.com/asyncdotengineering/porulle.git",
|
|
46
|
+
"directory": "packages/adapters/adapter-ses"
|
|
47
|
+
},
|
|
48
|
+
"author": "Porulle contributors"
|
|
49
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { SESv2Client, SendEmailCommand } from "@aws-sdk/client-sesv2";
|
|
2
|
+
|
|
3
|
+
export interface SESAdapterOptions {
|
|
4
|
+
/** AWS region (e.g., "us-east-1"). */
|
|
5
|
+
region: string;
|
|
6
|
+
/** Default sender address (e.g., "Acme Store <orders@acme.com>"). Must be verified in SES. */
|
|
7
|
+
from: string;
|
|
8
|
+
/** AWS credentials. If omitted, uses the default credential chain (env vars, IAM role, etc.). */
|
|
9
|
+
credentials?: {
|
|
10
|
+
accessKeyId: string;
|
|
11
|
+
secretAccessKey: string;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Maps template names to subject line generators.
|
|
15
|
+
* If a template is not in this map, the subject defaults to the template name.
|
|
16
|
+
*/
|
|
17
|
+
subjects?: Record<string, (data: Record<string, unknown>) => string>;
|
|
18
|
+
/**
|
|
19
|
+
* Maps template names to HTML body generators.
|
|
20
|
+
* If a template is not in this map, a minimal default is used.
|
|
21
|
+
*/
|
|
22
|
+
templates?: Record<string, (data: Record<string, unknown>) => string>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const DEFAULT_SUBJECTS: Record<string, (data: Record<string, unknown>) => string> = {
|
|
26
|
+
"order-confirmation": (d) => `Order Confirmed${d.orderId ? ` - #${String(d.orderId).slice(0, 8)}` : ""}`,
|
|
27
|
+
"order-status-change": (d) => `Order Update - ${d.newStatus ?? "Status Changed"}`,
|
|
28
|
+
"password-reset": () => "Reset Your Password",
|
|
29
|
+
"email-verification": () => "Verify Your Email Address",
|
|
30
|
+
"appointment:reminder": (d) => `Appointment Reminder${d.reminderType === "1h" ? " - Starting Soon" : ""}`,
|
|
31
|
+
"appointment:confirmation-notice": () => "Appointment Confirmed",
|
|
32
|
+
"appointment:cancellation-notice": () => "Appointment Cancelled",
|
|
33
|
+
"appointment:no-show-notice": () => "Missed Appointment",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const DEFAULT_TEMPLATES: Record<string, (data: Record<string, unknown>) => string> = {
|
|
37
|
+
"order-confirmation": (d) =>
|
|
38
|
+
`<h2>Order Confirmed</h2><p>Thank you for your order${d.orderId ? ` <strong>#${String(d.orderId).slice(0, 8)}</strong>` : ""}.</p>${d.total ? `<p>Total: ${d.currency ?? "USD"} ${String(d.total)}</p>` : ""}`,
|
|
39
|
+
"order-status-change": (d) =>
|
|
40
|
+
`<h2>Order Update</h2><p>Your order${d.orderId ? ` <strong>#${String(d.orderId).slice(0, 8)}</strong>` : ""} status has been updated to <strong>${d.newStatus ?? "unknown"}</strong>.</p>`,
|
|
41
|
+
"password-reset": (d) =>
|
|
42
|
+
`<h2>Reset Your Password</h2><p>Click the link below to reset your password:</p><p><a href="${d.url ?? "#"}">Reset Password</a></p>`,
|
|
43
|
+
"email-verification": (d) =>
|
|
44
|
+
`<h2>Verify Your Email</h2><p>Click the link below to verify your email address:</p><p><a href="${d.url ?? "#"}">Verify Email</a></p>`,
|
|
45
|
+
"appointment:reminder": (d) =>
|
|
46
|
+
`<h2>Appointment Reminder</h2><p>This is a${d.reminderType === "1h" ? " 1-hour" : " 24-hour"} reminder for your upcoming appointment.</p><p>Booking ID: ${d.bookingId ?? "N/A"}</p>`,
|
|
47
|
+
"appointment:confirmation-notice": (d) =>
|
|
48
|
+
`<h2>Appointment Confirmed</h2><p>Your appointment has been confirmed.</p><p>Booking ID: ${d.bookingId ?? "N/A"}</p>`,
|
|
49
|
+
"appointment:cancellation-notice": (d) =>
|
|
50
|
+
`<h2>Appointment Cancelled</h2><p>Your appointment has been cancelled.</p><p>Booking ID: ${d.bookingId ?? "N/A"}</p>`,
|
|
51
|
+
"appointment:no-show-notice": (d) =>
|
|
52
|
+
`<h2>Missed Appointment</h2><p>You missed your appointment. Please contact us to rebook.</p><p>Booking ID: ${d.bookingId ?? "N/A"}</p>`,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Creates an email adapter backed by AWS SES v2.
|
|
57
|
+
*
|
|
58
|
+
* Implements the `config.email.send()` interface consumed by checkout hooks,
|
|
59
|
+
* auth (password reset, email verification), and appointment plugin notifications.
|
|
60
|
+
*
|
|
61
|
+
* Sender address must be verified in SES. If your account is in the SES sandbox,
|
|
62
|
+
* recipient addresses must also be verified.
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```typescript
|
|
66
|
+
* import { sesAdapter } from "@porulle/adapter-ses";
|
|
67
|
+
*
|
|
68
|
+
* export default defineConfig({
|
|
69
|
+
* email: sesAdapter({
|
|
70
|
+
* region: "us-east-1",
|
|
71
|
+
* from: "Acme Store <orders@acme.com>",
|
|
72
|
+
* }),
|
|
73
|
+
* });
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
export function sesAdapter(options: SESAdapterOptions): {
|
|
77
|
+
send(input: { template: string; to: string; data?: Record<string, unknown> }): Promise<void>;
|
|
78
|
+
} {
|
|
79
|
+
const client = new SESv2Client({
|
|
80
|
+
region: options.region,
|
|
81
|
+
...(options.credentials ? { credentials: options.credentials } : {}),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const subjects = { ...DEFAULT_SUBJECTS, ...options.subjects };
|
|
85
|
+
const templates = { ...DEFAULT_TEMPLATES, ...options.templates };
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
async send(input) {
|
|
89
|
+
const data = input.data ?? {};
|
|
90
|
+
const subjectFn = subjects[input.template];
|
|
91
|
+
const subject = subjectFn ? subjectFn(data) : input.template;
|
|
92
|
+
|
|
93
|
+
const templateFn = templates[input.template];
|
|
94
|
+
const html = templateFn
|
|
95
|
+
? templateFn(data)
|
|
96
|
+
: `<p>Notification: ${input.template}</p><pre>${JSON.stringify(data, null, 2)}</pre>`;
|
|
97
|
+
|
|
98
|
+
const command = new SendEmailCommand({
|
|
99
|
+
FromEmailAddress: options.from,
|
|
100
|
+
Destination: {
|
|
101
|
+
ToAddresses: [input.to],
|
|
102
|
+
},
|
|
103
|
+
Content: {
|
|
104
|
+
Simple: {
|
|
105
|
+
Subject: { Data: subject, Charset: "UTF-8" },
|
|
106
|
+
Body: {
|
|
107
|
+
Html: { Data: html, Charset: "UTF-8" },
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const result = await client.send(command);
|
|
114
|
+
if (!result.MessageId) {
|
|
115
|
+
throw new Error("SES email failed: no MessageId returned");
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|