@molecule/api-emails-ses 1.0.0 → 1.0.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 +304 -0
- package/package.json +7 -6
package/README.md
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
AUTO-GENERATED — DO NOT EDIT THIS FILE.
|
|
3
|
+
Generated by `mlcl sync-docs` from the package's src/index.ts JSDoc + mlcl/registry.json.
|
|
4
|
+
Edits here are overwritten on the next commit (molecule's pre-commit hook regenerates).
|
|
5
|
+
To change this document, edit the module-level JSDoc in src/index.ts.
|
|
6
|
+
Generated: 2026-08-04T01:48:05.373Z
|
|
7
|
+
-->
|
|
8
|
+
|
|
9
|
+
# @molecule/api-emails-ses
|
|
10
|
+
|
|
11
|
+
> **Auto-generated, AI-first package reference** for the [molecule.dev](https://molecule.dev) ecosystem.
|
|
12
|
+
> It is written to be read by coding agents as much as by people, and is generated from this
|
|
13
|
+
> package's source — edit `src/index.ts` JSDoc, not this file.
|
|
14
|
+
|
|
15
|
+
AWS SES email provider for molecule.dev.
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
import { setTransport } from '@molecule/api-emails'
|
|
21
|
+
import { provider } from '@molecule/api-emails-ses'
|
|
22
|
+
|
|
23
|
+
setTransport(provider)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Type
|
|
27
|
+
|
|
28
|
+
`provider`
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install @molecule/api-emails-ses @aws-sdk/client-sesv2 @aws-sdk/credential-provider-node @molecule/api-bond @molecule/api-emails @molecule/api-secrets nodemailer
|
|
34
|
+
npm install -D @types/nodemailer
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## API
|
|
38
|
+
|
|
39
|
+
### Interfaces
|
|
40
|
+
|
|
41
|
+
#### `EmailMessage`
|
|
42
|
+
|
|
43
|
+
Email message options.
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
interface EmailMessage {
|
|
47
|
+
/**
|
|
48
|
+
* Sender address.
|
|
49
|
+
*/
|
|
50
|
+
from: string | EmailAddress
|
|
51
|
+
/**
|
|
52
|
+
* Recipient(s).
|
|
53
|
+
*/
|
|
54
|
+
to: string | EmailAddress | (string | EmailAddress)[]
|
|
55
|
+
/**
|
|
56
|
+
* CC recipient(s).
|
|
57
|
+
*/
|
|
58
|
+
cc?: string | EmailAddress | (string | EmailAddress)[]
|
|
59
|
+
/**
|
|
60
|
+
* BCC recipient(s).
|
|
61
|
+
*/
|
|
62
|
+
bcc?: string | EmailAddress | (string | EmailAddress)[]
|
|
63
|
+
/**
|
|
64
|
+
* Reply-to address.
|
|
65
|
+
*/
|
|
66
|
+
replyTo?: string | EmailAddress
|
|
67
|
+
/**
|
|
68
|
+
* Email subject.
|
|
69
|
+
*/
|
|
70
|
+
subject: string
|
|
71
|
+
/**
|
|
72
|
+
* Plain text body.
|
|
73
|
+
*/
|
|
74
|
+
text?: string
|
|
75
|
+
/**
|
|
76
|
+
* HTML body.
|
|
77
|
+
*/
|
|
78
|
+
html?: string
|
|
79
|
+
/**
|
|
80
|
+
* File attachments.
|
|
81
|
+
*/
|
|
82
|
+
attachments?: EmailAttachment[]
|
|
83
|
+
/**
|
|
84
|
+
* i18n key for the subject (for client-side translation).
|
|
85
|
+
*/
|
|
86
|
+
subjectKey?: string
|
|
87
|
+
/**
|
|
88
|
+
* i18n key for the plain text body (for client-side translation).
|
|
89
|
+
*/
|
|
90
|
+
textKey?: string
|
|
91
|
+
/**
|
|
92
|
+
* i18n key for the HTML body (for client-side translation).
|
|
93
|
+
*/
|
|
94
|
+
htmlKey?: string
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
#### `EmailSendResult`
|
|
99
|
+
|
|
100
|
+
Result of sending an email.
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
interface EmailSendResult {
|
|
104
|
+
/**
|
|
105
|
+
* Whether the email was accepted for delivery.
|
|
106
|
+
*/
|
|
107
|
+
accepted: string[]
|
|
108
|
+
/**
|
|
109
|
+
* Addresses that were rejected.
|
|
110
|
+
*/
|
|
111
|
+
rejected: string[]
|
|
112
|
+
/**
|
|
113
|
+
* Message ID from the provider.
|
|
114
|
+
*/
|
|
115
|
+
messageId?: string
|
|
116
|
+
/**
|
|
117
|
+
* Raw response from the provider.
|
|
118
|
+
*/
|
|
119
|
+
response?: string
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
#### `EmailTransport`
|
|
124
|
+
|
|
125
|
+
Email transport interface.
|
|
126
|
+
|
|
127
|
+
All email providers must implement this interface.
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
interface EmailTransport {
|
|
131
|
+
/**
|
|
132
|
+
* Sends an email message.
|
|
133
|
+
* @returns The send result.
|
|
134
|
+
*/
|
|
135
|
+
sendMail(message: EmailMessage): Promise<EmailSendResult>
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Functions
|
|
140
|
+
|
|
141
|
+
#### `getSesClient()`
|
|
142
|
+
|
|
143
|
+
Returns the AWS SESv2 client, constructing it from the environment on the
|
|
144
|
+
FIRST call and memoizing thereafter.
|
|
145
|
+
|
|
146
|
+
Construction is deferred to first use — NOT module load — so a region resolved
|
|
147
|
+
into `process.env` AFTER this module is imported (late secrets resolution via
|
|
148
|
+
a secrets bond) is honored: `AWS_SES_REGION` (default `us-east-1`) and the
|
|
149
|
+
optional `AWS_SES_ENDPOINT` are read at send time, not frozen at import.
|
|
150
|
+
Reading them at import instead pinned an empty/default region and every send
|
|
151
|
+
failed in the WRONG region ("Email address is not verified"). Credentials
|
|
152
|
+
still resolve lazily via the AWS default chain (`AWS_ACCESS_KEY_ID`/
|
|
153
|
+
`AWS_SECRET_ACCESS_KEY`, shared config, or an instance role) at send time, so
|
|
154
|
+
a missing credential surfaces then as a descriptive AWS SDK error.
|
|
155
|
+
|
|
156
|
+
nodemailer 7 requires the SESv2 client + `SendEmailCommand` pair — the old
|
|
157
|
+
`{ ses, aws }` (@aws-sdk/client-ses) shape made `createTransport` THROW
|
|
158
|
+
("legacy SES configuration"), breaking every real consumer of this bond.
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
function getSesClient(): SESv2Client
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
**Returns:** The configured SESv2 client.
|
|
165
|
+
|
|
166
|
+
#### `sendMail(message)`
|
|
167
|
+
|
|
168
|
+
Sends an email through AWS SES via nodemailer. The SES client and transport
|
|
169
|
+
are configured lazily from the environment on the first call, so late-resolved
|
|
170
|
+
region/credentials are honored.
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
function sendMail(message: EmailMessage): Promise<EmailSendResult>
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
- `message` — The email message (to, from, subject, text/html, attachments).
|
|
177
|
+
|
|
178
|
+
**Returns:** Send result with accepted/rejected addresses and message ID.
|
|
179
|
+
|
|
180
|
+
### Constants
|
|
181
|
+
|
|
182
|
+
#### `email` _(deprecated)_
|
|
183
|
+
|
|
184
|
+
Raw nodemailer transport alias.
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
const email: { sendMail: (msg: nodemailer.SendMailOptions) => Promise<any> }
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
#### `emailsSesSecretDefinitions`
|
|
191
|
+
|
|
192
|
+
Secret definitions required by the AWS SES email bond.
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
const emailsSesSecretDefinitions: SecretDefinition[]
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
#### `provider`
|
|
199
|
+
|
|
200
|
+
The SES email provider implementing the `EmailTransport` interface.
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
const provider: EmailTransport
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
#### `transport` _(deprecated)_
|
|
207
|
+
|
|
208
|
+
Raw nodemailer transport for direct access. Lazily configured on first send.
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
const transport: { sendMail: (msg: nodemailer.SendMailOptions) => Promise<any> }
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## Core Interface
|
|
215
|
+
|
|
216
|
+
Implements `@molecule/api-emails` interface.
|
|
217
|
+
|
|
218
|
+
## Bond Wiring
|
|
219
|
+
|
|
220
|
+
Setup function to register this provider with the core interface:
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
import { setTransport } from '@molecule/api-emails'
|
|
224
|
+
import { provider } from '@molecule/api-emails-ses'
|
|
225
|
+
|
|
226
|
+
export function setupEmailsSes(): void {
|
|
227
|
+
setTransport(provider)
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## Injection Notes
|
|
232
|
+
|
|
233
|
+
### Requirements
|
|
234
|
+
|
|
235
|
+
Peer dependencies:
|
|
236
|
+
|
|
237
|
+
- `@molecule/api-bond` ^1.0.1
|
|
238
|
+
- `@molecule/api-emails` ^1.0.1
|
|
239
|
+
- `@molecule/api-secrets` ^1.0.1
|
|
240
|
+
|
|
241
|
+
### Environment Variables
|
|
242
|
+
|
|
243
|
+
- `AWS_ACCESS_KEY_ID` _(required)_ — AWS access key ID
|
|
244
|
+
- Setup: Create an IAM user with the needed policy (SES/S3/SQS) and create an access key under Security credentials.
|
|
245
|
+
- Get it here: [https://console.aws.amazon.com/iam/](https://console.aws.amazon.com/iam/)
|
|
246
|
+
- Example: `AKIA...`
|
|
247
|
+
- `AWS_SECRET_ACCESS_KEY` _(required)_ — AWS secret access key
|
|
248
|
+
- Setup: Shown once when creating the IAM access key — store it immediately.
|
|
249
|
+
- Get it here: [https://console.aws.amazon.com/iam/](https://console.aws.amazon.com/iam/)
|
|
250
|
+
- `AWS_SES_REGION` _(required)_ — AWS SES region
|
|
251
|
+
- Setup: The AWS region where SES is set up (and out of sandbox for production sending).
|
|
252
|
+
- Example: `us-east-1`
|
|
253
|
+
|
|
254
|
+
### Runtime Dependencies
|
|
255
|
+
|
|
256
|
+
- `@aws-sdk/client-sesv2`
|
|
257
|
+
- `@aws-sdk/credential-provider-node`
|
|
258
|
+
- `@molecule/api-bond`
|
|
259
|
+
- `@molecule/api-emails`
|
|
260
|
+
- `@molecule/api-secrets`
|
|
261
|
+
- `nodemailer`
|
|
262
|
+
|
|
263
|
+
- **Configuration is lazy and env-driven**: the SES client is constructed on
|
|
264
|
+
the FIRST send — NOT at import — so `AWS_SES_REGION` (default `us-east-1`)
|
|
265
|
+
and the optional `AWS_SES_ENDPOINT` are read at send time. A region resolved
|
|
266
|
+
into env AFTER this module is imported (late secrets resolution via a
|
|
267
|
+
secrets bond) is honored; reading it at import instead froze the
|
|
268
|
+
default/empty region and sends failed in the WRONG region ("Email address
|
|
269
|
+
is not verified"). Credentials resolve lazily via the AWS default chain
|
|
270
|
+
(`AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY`, shared config, or an instance
|
|
271
|
+
role), so they may arrive after import too.
|
|
272
|
+
- **No fail-fast**: because credentials can legitimately come from an instance
|
|
273
|
+
role or shared config (not env), missing credentials are not pre-checked —
|
|
274
|
+
they surface at first send as a descriptive AWS SDK error ("Could not load
|
|
275
|
+
credentials…"), not a tagged config error naming the env var.
|
|
276
|
+
- New SES accounts are sandboxed: both the sender identity AND every
|
|
277
|
+
recipient must be verified until production access is granted.
|
|
278
|
+
- On success `accepted` is mapped from `envelope.to` — nodemailer's SES
|
|
279
|
+
transport never sets `accepted`/`rejected` (the `@types/nodemailer` typings
|
|
280
|
+
claiming otherwise are drift); a resolved send means SES accepted the
|
|
281
|
+
message for every envelope recipient.
|
|
282
|
+
|
|
283
|
+
## E2E Tests
|
|
284
|
+
|
|
285
|
+
Integration checklist — drive the real UI (live preview, no mocks). The
|
|
286
|
+
sandbox CAPTURES outbound email instead of sending — read each message with
|
|
287
|
+
the `read_activity` tool (filter type 'email'); the verification/reset link
|
|
288
|
+
is in its payload. Never mock the send or modify production code to expose
|
|
289
|
+
it. Adapt each item to this app's actual screens/flows, and check every box
|
|
290
|
+
off one by one. A box you can't check is an integration bug to fix — not a
|
|
291
|
+
skip:
|
|
292
|
+
|
|
293
|
+
- [ ] Each email-triggering flow (signup verification, password-reset request,
|
|
294
|
+
invites/notifications the app defines) confirms the send in the UI ("check
|
|
295
|
+
your inbox") and a message actually reaches the transport.
|
|
296
|
+
- [ ] The password-reset round-trip completes: request a reset → open the
|
|
297
|
+
captured message → follow its single-use link → set a new password → log
|
|
298
|
+
in with it (and the old password no longer works).
|
|
299
|
+
- [ ] The message body contains a LINK, never the raw token/secret, and renders
|
|
300
|
+
with the app's real name/content (no `undefined` placeholders).
|
|
301
|
+
- [ ] Requesting a reset for an unknown email shows the same neutral UI response
|
|
302
|
+
as a known one (no account-existence oracle).
|
|
303
|
+
- [ ] Account emails go only to the account's own address — no UI or endpoint
|
|
304
|
+
lets an unauthenticated caller send to an arbitrary address.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@molecule/api-emails-ses",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "AWS SES email provider for molecule.dev.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
}
|
|
18
18
|
},
|
|
19
19
|
"files": [
|
|
20
|
-
"dist"
|
|
20
|
+
"dist",
|
|
21
|
+
"README.md"
|
|
21
22
|
],
|
|
22
23
|
"keywords": [
|
|
23
24
|
"molecule",
|
|
@@ -33,16 +34,16 @@
|
|
|
33
34
|
"nodemailer": "9.0.3"
|
|
34
35
|
},
|
|
35
36
|
"devDependencies": {
|
|
36
|
-
"@molecule/api-bond": "1.0.
|
|
37
|
+
"@molecule/api-bond": "1.0.1",
|
|
37
38
|
"@types/node": "26.1.2",
|
|
38
39
|
"@types/nodemailer": "8.0.1",
|
|
39
40
|
"typescript": "6.0.3",
|
|
40
41
|
"vitest": "4.1.10"
|
|
41
42
|
},
|
|
42
43
|
"peerDependencies": {
|
|
43
|
-
"@molecule/api-bond": "^1.0.
|
|
44
|
-
"@molecule/api-emails": "^1.0.
|
|
45
|
-
"@molecule/api-secrets": "^1.0.
|
|
44
|
+
"@molecule/api-bond": "^1.0.1",
|
|
45
|
+
"@molecule/api-emails": "^1.0.1",
|
|
46
|
+
"@molecule/api-secrets": "^1.0.1"
|
|
46
47
|
},
|
|
47
48
|
"repository": {
|
|
48
49
|
"type": "git",
|