@molecule/api-emails-resend 1.0.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/LICENSE +115 -0
- package/README.md +396 -0
- package/dist/browser-guard.d.ts +2 -0
- package/dist/browser-guard.d.ts.map +1 -0
- package/dist/browser-guard.js +19 -0
- package/dist/browser-guard.js.map +1 -0
- package/dist/index.d.ts +61 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +61 -0
- package/dist/index.js.map +1 -0
- package/dist/provider.d.ts +11 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +13 -0
- package/dist/provider.js.map +1 -0
- package/dist/secrets.d.ts +16 -0
- package/dist/secrets.d.ts.map +1 -0
- package/dist/secrets.js +32 -0
- package/dist/secrets.js.map +1 -0
- package/dist/sendMail.d.ts +14 -0
- package/dist/sendMail.d.ts.map +1 -0
- package/dist/sendMail.js +143 -0
- package/dist/sendMail.js.map +1 -0
- package/dist/transport.d.ts +43 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +129 -0
- package/dist/transport.js.map +1 -0
- package/dist/types.d.ts +101 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +7 -0
- package/dist/types.js.map +1 -0
- package/package.json +51 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work.
|
|
38
|
+
|
|
39
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
40
|
+
form, that is based on (or derived from) the Work and for which the
|
|
41
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
42
|
+
represent, as a whole, an original work of authorship.
|
|
43
|
+
|
|
44
|
+
"Contribution" shall mean any work of authorship, including the
|
|
45
|
+
original version of the Work and any modifications or additions
|
|
46
|
+
to that Work, that is intentionally submitted to the Licensor for
|
|
47
|
+
inclusion in the Work by the copyright owner or by an individual or
|
|
48
|
+
Legal Entity authorized to submit on behalf of the copyright owner.
|
|
49
|
+
|
|
50
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
51
|
+
on behalf of whom a Contribution has been received by the Licensor and
|
|
52
|
+
subsequently incorporated within the Work.
|
|
53
|
+
|
|
54
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
55
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
56
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
57
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
58
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
59
|
+
Work and such Derivative Works in Source or Object form.
|
|
60
|
+
|
|
61
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
62
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
63
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
64
|
+
patent license to make, have made, use, offer to sell, sell, import,
|
|
65
|
+
and otherwise transfer the Work.
|
|
66
|
+
|
|
67
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
68
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
69
|
+
modifications, and in Source or Object form, provided that You
|
|
70
|
+
meet the following conditions:
|
|
71
|
+
|
|
72
|
+
(a) You must give any other recipients of the Work or
|
|
73
|
+
Derivative Works a copy of this License; and
|
|
74
|
+
|
|
75
|
+
(b) You must cause any modified files to carry prominent notices
|
|
76
|
+
stating that You changed the files; and
|
|
77
|
+
|
|
78
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
79
|
+
that You distribute, all copyright, patent, trademark, and
|
|
80
|
+
attribution notices from the Source form of the Work,
|
|
81
|
+
excluding those notices that do not pertain to any part of
|
|
82
|
+
the Derivative Works; and
|
|
83
|
+
|
|
84
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
85
|
+
distribution, then any Derivative Works that You distribute must
|
|
86
|
+
include a readable copy of the attribution notices contained
|
|
87
|
+
within such NOTICE file.
|
|
88
|
+
|
|
89
|
+
5. Submission of Contributions.
|
|
90
|
+
|
|
91
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
92
|
+
names, trademarks, service marks, or product names of the Licensor.
|
|
93
|
+
|
|
94
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
95
|
+
agreed to in writing, Licensor provides the Work on an "AS IS" BASIS,
|
|
96
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND.
|
|
97
|
+
|
|
98
|
+
8. Limitation of Liability. In no event and under no legal theory shall
|
|
99
|
+
any Contributor be liable to You for damages.
|
|
100
|
+
|
|
101
|
+
9. Accepting Warranty or Additional Liability.
|
|
102
|
+
|
|
103
|
+
Copyright 2026 Molecule Dev, Inc.
|
|
104
|
+
|
|
105
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
106
|
+
you may not use this file except in compliance with the License.
|
|
107
|
+
You may obtain a copy of the License at
|
|
108
|
+
|
|
109
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
110
|
+
|
|
111
|
+
Unless required by applicable law or agreed to in writing, software
|
|
112
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
113
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
114
|
+
See the License for the specific language governing permissions and
|
|
115
|
+
limitations under the License.
|
package/README.md
ADDED
|
@@ -0,0 +1,396 @@
|
|
|
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-09-04T12:07:53.348Z
|
|
7
|
+
-->
|
|
8
|
+
|
|
9
|
+
# @molecule/api-emails-resend
|
|
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
|
+
Resend 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-resend'
|
|
22
|
+
|
|
23
|
+
setTransport(provider)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Type
|
|
27
|
+
|
|
28
|
+
`provider`
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install @molecule/api-emails-resend @molecule/api-bond @molecule/api-emails @molecule/api-secrets
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## API
|
|
37
|
+
|
|
38
|
+
### Interfaces
|
|
39
|
+
|
|
40
|
+
#### `EmailMessage`
|
|
41
|
+
|
|
42
|
+
Email message options.
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
interface EmailMessage {
|
|
46
|
+
/**
|
|
47
|
+
* Sender address.
|
|
48
|
+
*/
|
|
49
|
+
from: string | EmailAddress
|
|
50
|
+
/**
|
|
51
|
+
* Recipient(s).
|
|
52
|
+
*/
|
|
53
|
+
to: string | EmailAddress | (string | EmailAddress)[]
|
|
54
|
+
/**
|
|
55
|
+
* CC recipient(s).
|
|
56
|
+
*/
|
|
57
|
+
cc?: string | EmailAddress | (string | EmailAddress)[]
|
|
58
|
+
/**
|
|
59
|
+
* BCC recipient(s).
|
|
60
|
+
*/
|
|
61
|
+
bcc?: string | EmailAddress | (string | EmailAddress)[]
|
|
62
|
+
/**
|
|
63
|
+
* Reply-to address.
|
|
64
|
+
*/
|
|
65
|
+
replyTo?: string | EmailAddress
|
|
66
|
+
/**
|
|
67
|
+
* Email subject.
|
|
68
|
+
*/
|
|
69
|
+
subject: string
|
|
70
|
+
/**
|
|
71
|
+
* Plain text body.
|
|
72
|
+
*/
|
|
73
|
+
text?: string
|
|
74
|
+
/**
|
|
75
|
+
* HTML body.
|
|
76
|
+
*/
|
|
77
|
+
html?: string
|
|
78
|
+
/**
|
|
79
|
+
* File attachments.
|
|
80
|
+
*/
|
|
81
|
+
attachments?: EmailAttachment[]
|
|
82
|
+
/**
|
|
83
|
+
* i18n key for the subject (for client-side translation).
|
|
84
|
+
*/
|
|
85
|
+
subjectKey?: string
|
|
86
|
+
/**
|
|
87
|
+
* i18n key for the plain text body (for client-side translation).
|
|
88
|
+
*/
|
|
89
|
+
textKey?: string
|
|
90
|
+
/**
|
|
91
|
+
* i18n key for the HTML body (for client-side translation).
|
|
92
|
+
*/
|
|
93
|
+
htmlKey?: string
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
#### `EmailSendResult`
|
|
98
|
+
|
|
99
|
+
Result of sending an email.
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
interface EmailSendResult {
|
|
103
|
+
/**
|
|
104
|
+
* Whether the email was accepted for delivery.
|
|
105
|
+
*/
|
|
106
|
+
accepted: string[]
|
|
107
|
+
/**
|
|
108
|
+
* Addresses that were rejected.
|
|
109
|
+
*/
|
|
110
|
+
rejected: string[]
|
|
111
|
+
/**
|
|
112
|
+
* Message ID from the provider.
|
|
113
|
+
*/
|
|
114
|
+
messageId?: string
|
|
115
|
+
/**
|
|
116
|
+
* Raw response from the provider.
|
|
117
|
+
*/
|
|
118
|
+
response?: string
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
#### `EmailTransport`
|
|
123
|
+
|
|
124
|
+
Email transport interface.
|
|
125
|
+
|
|
126
|
+
All email providers must implement this interface.
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
interface EmailTransport {
|
|
130
|
+
/**
|
|
131
|
+
* Sends an email message.
|
|
132
|
+
* @returns The send result.
|
|
133
|
+
*/
|
|
134
|
+
sendMail(message: EmailMessage): Promise<EmailSendResult>
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
#### `ResendAttachment`
|
|
139
|
+
|
|
140
|
+
One entry of a send request's `attachments[]`, exactly as the Resend REST API
|
|
141
|
+
expects it: base64 `content` plus snake_case metadata.
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
interface ResendAttachment {
|
|
145
|
+
/** File content, base64-encoded. */
|
|
146
|
+
content: string
|
|
147
|
+
/** File name shown to the recipient; Resend derives the MIME type from it when `content_type` is unset. */
|
|
148
|
+
filename: string
|
|
149
|
+
/** MIME type of the file. */
|
|
150
|
+
content_type?: string
|
|
151
|
+
/** Content-ID for an inline image referenced as `cid:` in the HTML body. */
|
|
152
|
+
content_id?: string
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
#### `ResendClient`
|
|
157
|
+
|
|
158
|
+
The narrow HTTP client this bond uses to reach Resend.
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
interface ResendClient {
|
|
162
|
+
/**
|
|
163
|
+
* Sends one email via `POST /emails`.
|
|
164
|
+
* @returns The HTTP status and the message id.
|
|
165
|
+
*/
|
|
166
|
+
send(request: ResendSendRequest): Promise<ResendSendResponse>
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
#### `ResendErrorBody`
|
|
171
|
+
|
|
172
|
+
Resend's JSON error body (`{ statusCode, message, name }`), with every field
|
|
173
|
+
optional because the wire is not trusted.
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
interface ResendErrorBody {
|
|
177
|
+
/** HTTP status Resend reports in the body (may be `null`). */
|
|
178
|
+
statusCode?: number | null
|
|
179
|
+
/** Human-readable error message. */
|
|
180
|
+
message?: string
|
|
181
|
+
/** Machine-readable error name, e.g. `validation_error`, `daily_quota_exceeded`. */
|
|
182
|
+
name?: string
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
#### `ResendSendRequest`
|
|
187
|
+
|
|
188
|
+
Request body for `POST /emails`. Field names are the REST API's own
|
|
189
|
+
(snake_case, e.g. `reply_to`) — NOT the camelCase the official SDK accepts.
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
interface ResendSendRequest {
|
|
193
|
+
/** Sender, as a bare address or `Name <address>`. */
|
|
194
|
+
from: string
|
|
195
|
+
/** Recipients (Resend accepts at most 50). */
|
|
196
|
+
to: string[]
|
|
197
|
+
/** Subject line. */
|
|
198
|
+
subject: string
|
|
199
|
+
/** CC recipients. */
|
|
200
|
+
cc?: string[]
|
|
201
|
+
/** BCC recipients. */
|
|
202
|
+
bcc?: string[]
|
|
203
|
+
/** Reply-To address. */
|
|
204
|
+
reply_to?: string
|
|
205
|
+
/** HTML body. */
|
|
206
|
+
html?: string
|
|
207
|
+
/** Plain-text body (Resend derives one from `html` when omitted). */
|
|
208
|
+
text?: string
|
|
209
|
+
/** File attachments. */
|
|
210
|
+
attachments?: ResendAttachment[]
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
#### `ResendSendResponse`
|
|
215
|
+
|
|
216
|
+
What {@link ResendClient.send} resolves with: the HTTP status of the accepted
|
|
217
|
+
request and the message `id` Resend returned in the response body.
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
interface ResendSendResponse {
|
|
221
|
+
/** HTTP status code of the response (2xx). */
|
|
222
|
+
status: number
|
|
223
|
+
/** The `id` from the response body, when present. */
|
|
224
|
+
id?: string
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### Classes
|
|
229
|
+
|
|
230
|
+
#### `ResendApiError`
|
|
231
|
+
|
|
232
|
+
A non-2xx response from the Resend API.
|
|
233
|
+
|
|
234
|
+
`status` is the HTTP status, `code` is Resend's machine-readable error `name`
|
|
235
|
+
(e.g. `validation_error`, `rate_limit_exceeded`, `daily_quota_exceeded`) when
|
|
236
|
+
the body carried one, and `body` is the parsed error body. Deliberately NOT
|
|
237
|
+
tagged with `statusCode` / `errorKey`: Resend's status describes OUR request
|
|
238
|
+
to Resend (a 401 is a bad key, a 403 an unverified domain), not the caller's
|
|
239
|
+
request to the app, so the API middleware must fall through to its generic
|
|
240
|
+
500 rather than echo it.
|
|
241
|
+
|
|
242
|
+
### Functions
|
|
243
|
+
|
|
244
|
+
#### `getClient()`
|
|
245
|
+
|
|
246
|
+
Returns the Resend HTTP client. Nothing is configured up front: the API key
|
|
247
|
+
and base URL are read from the environment on every `send()`.
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
function getClient(): ResendClient
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
**Returns:** The Resend client.
|
|
254
|
+
|
|
255
|
+
#### `sendMail(message)`
|
|
256
|
+
|
|
257
|
+
Sends an email through the Resend API.
|
|
258
|
+
|
|
259
|
+
```typescript
|
|
260
|
+
function sendMail(message: EmailMessage): Promise<EmailSendResult>
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
- `message` — The email message (to, from, subject, text/html, attachments).
|
|
264
|
+
|
|
265
|
+
**Returns:** Send result with accepted addresses, message ID, and status code.
|
|
266
|
+
|
|
267
|
+
### Constants
|
|
268
|
+
|
|
269
|
+
#### `emailsResendSecretDefinitions`
|
|
270
|
+
|
|
271
|
+
Secret definitions required by the Resend email bond.
|
|
272
|
+
|
|
273
|
+
```typescript
|
|
274
|
+
const emailsResendSecretDefinitions: SecretDefinition[]
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
#### `provider`
|
|
278
|
+
|
|
279
|
+
The Resend email provider implementing the standard interface.
|
|
280
|
+
|
|
281
|
+
```typescript
|
|
282
|
+
const provider: EmailTransport
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
#### `RESEND_DEFAULT_BASE_URL`
|
|
286
|
+
|
|
287
|
+
Resend's REST API base URL. Override with `RESEND_BASE_URL`.
|
|
288
|
+
|
|
289
|
+
```typescript
|
|
290
|
+
const RESEND_DEFAULT_BASE_URL: 'https://api.resend.com'
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
## Core Interface
|
|
294
|
+
|
|
295
|
+
Implements `@molecule/api-emails` interface.
|
|
296
|
+
|
|
297
|
+
## Bond Wiring
|
|
298
|
+
|
|
299
|
+
Setup function to register this provider with the core interface:
|
|
300
|
+
|
|
301
|
+
```typescript
|
|
302
|
+
import { setTransport } from '@molecule/api-emails'
|
|
303
|
+
import { provider } from '@molecule/api-emails-resend'
|
|
304
|
+
|
|
305
|
+
export function setupEmailsResend(): void {
|
|
306
|
+
setTransport(provider)
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
## Injection Notes
|
|
311
|
+
|
|
312
|
+
### Requirements
|
|
313
|
+
|
|
314
|
+
Peer dependencies:
|
|
315
|
+
|
|
316
|
+
- `@molecule/api-bond` ^1.0.1
|
|
317
|
+
- `@molecule/api-emails` ^1.0.1
|
|
318
|
+
- `@molecule/api-secrets` ^1.0.1
|
|
319
|
+
|
|
320
|
+
### Environment Variables
|
|
321
|
+
|
|
322
|
+
- `RESEND_API_KEY` _(required)_ — Resend API key
|
|
323
|
+
- Setup: Resend → API Keys → Create API Key (Sending access). Verify your sending domain under Domains first.
|
|
324
|
+
- Get it here: [https://resend.com/api-keys](https://resend.com/api-keys)
|
|
325
|
+
- Example: `re_...`
|
|
326
|
+
- `RESEND_FROM` _(optional)_ — Default sender
|
|
327
|
+
- Setup: Optional default From address on a verified domain, e.g. "Acme <hello@acme.com>".
|
|
328
|
+
- Get it here: [https://resend.com/domains](https://resend.com/domains)
|
|
329
|
+
- Example: `Acme <hello@example.com>`
|
|
330
|
+
|
|
331
|
+
### Runtime Dependencies
|
|
332
|
+
|
|
333
|
+
- `@molecule/api-bond`
|
|
334
|
+
- `@molecule/api-emails`
|
|
335
|
+
- `@molecule/api-secrets`
|
|
336
|
+
|
|
337
|
+
- **Zero dependencies — talks to Resend's REST API with the runtime's global
|
|
338
|
+
`fetch`** (`POST https://api.resend.com/emails`), so every app that installs
|
|
339
|
+
this bond stays dependency-free; the official `resend` SDK can be swapped in
|
|
340
|
+
later without changing the bond's interface.
|
|
341
|
+
- **`from` MUST be on a domain you VERIFIED in Resend** (Resend → Domains, DNS
|
|
342
|
+
records added and checked), or the API rejects the send with a 403
|
|
343
|
+
`validation_error` ("domain is not verified"). Before any domain is verified
|
|
344
|
+
the only usable sender is `onboarding@resend.dev` — testing only, and it can
|
|
345
|
+
deliver ONLY to the account owner's own address. Read the sender from config,
|
|
346
|
+
never hardcode a placeholder: set `RESEND_FROM` (e.g.
|
|
347
|
+
`Acme <no-reply@your-verified-domain.com>`) and it is used whenever a
|
|
348
|
+
message's `from` is empty; if both are missing, `sendMail()` throws a tagged
|
|
349
|
+
config-missing error naming `RESEND_FROM`.
|
|
350
|
+
- **Configuration is lazy and env-driven**: `RESEND_API_KEY` (and the optional
|
|
351
|
+
`RESEND_BASE_URL` override for brokers / compatible endpoints) are read on
|
|
352
|
+
EACH send — never at import time — so a key resolved into `process.env`
|
|
353
|
+
after import (late secrets resolution via a secrets bond) is honored. If the
|
|
354
|
+
key is absent at send time, `sendMail()` throws a tagged config-missing
|
|
355
|
+
error (clean 503 / `config.notConfigured`) naming `RESEND_API_KEY` — never
|
|
356
|
+
an opaque Resend 401.
|
|
357
|
+
- **There is NO sandbox / test-mode flag in the Resend API** (nothing like
|
|
358
|
+
SendGrid's `sandboxMode`). Every accepted request is a real send that counts
|
|
359
|
+
against the account's quota — including sends to the simulation recipients
|
|
360
|
+
`delivered@resend.dev`, `bounced@resend.dev` and `complained@resend.dev`,
|
|
361
|
+
which are the supported way to exercise the delivery / bounce / spam paths
|
|
362
|
+
without emailing a real inbox.
|
|
363
|
+
- **Attachments** are sent base64-encoded inline; Resend caps the whole email
|
|
364
|
+
at **40 MB after encoding** (larger sends are rejected). Buffer / string
|
|
365
|
+
content only — a stream attachment throws. A `cid` becomes Resend's
|
|
366
|
+
`content_id` for inline images (`<img src="cid:...">`).
|
|
367
|
+
- **API errors surface as `ResendApiError`** carrying `status` (HTTP) and
|
|
368
|
+
`code` (Resend's error name — `validation_error`, `rate_limit_exceeded`,
|
|
369
|
+
`daily_quota_exceeded`, …). They are deliberately NOT tagged with
|
|
370
|
+
`statusCode` / `errorKey`, so the API middleware returns its generic 500
|
|
371
|
+
instead of echoing Resend's status to the caller. On success `accepted`
|
|
372
|
+
echoes every `to` recipient (Resend returns no per-recipient verdict) and
|
|
373
|
+
`messageId` is the `id` from the response body.
|
|
374
|
+
|
|
375
|
+
## E2E Tests
|
|
376
|
+
|
|
377
|
+
Integration checklist — drive the real UI (live preview, no mocks). The
|
|
378
|
+
sandbox CAPTURES outbound email instead of sending — read each message with
|
|
379
|
+
the `read_activity` tool (filter type 'email'); the verification/reset link
|
|
380
|
+
is in its payload. Never mock the send or modify production code to expose
|
|
381
|
+
it. Adapt each item to this app's actual screens/flows, and check every box
|
|
382
|
+
off one by one. A box you can't check is an integration bug to fix — not a
|
|
383
|
+
skip:
|
|
384
|
+
|
|
385
|
+
- [ ] Each email-triggering flow (signup verification, password-reset request,
|
|
386
|
+
invites/notifications the app defines) confirms the send in the UI ("check
|
|
387
|
+
your inbox") and a message actually reaches the transport.
|
|
388
|
+
- [ ] The password-reset round-trip completes: request a reset → open the
|
|
389
|
+
captured message → follow its single-use link → set a new password → log
|
|
390
|
+
in with it (and the old password no longer works).
|
|
391
|
+
- [ ] The message body contains a LINK, never the raw token/secret, and renders
|
|
392
|
+
with the app's real name/content (no `undefined` placeholders).
|
|
393
|
+
- [ ] Requesting a reset for an unknown email shows the same neutral UI response
|
|
394
|
+
as a known one (no account-existence oracle).
|
|
395
|
+
- [ ] Account emails go only to the account's own address — no UI or endpoint
|
|
396
|
+
lets an unauthenticated caller send to an arbitrary address.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser-guard.d.ts","sourceRoot":"","sources":["../src/browser-guard.ts"],"names":[],"mappings":"AAuBA,OAAO,EAAE,CAAA"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser guard — `@molecule/api-emails-resend` is SERVER-ONLY.
|
|
3
|
+
*
|
|
4
|
+
* Generated by scripts/gen-browser-guards.mjs (workspace root) — edit THAT, not this.
|
|
5
|
+
* Evaluating a server package in a browser bundle is always an import-graph mistake
|
|
6
|
+
* (node APIs, secrets); without this guard it surfaces as a cryptic downstream crash
|
|
7
|
+
* ("Buffer is not defined") far from the culprit. Throwing here names the package and
|
|
8
|
+
* the fix at the exact moment the client bundle evaluates it. jsdom tests and SSR are
|
|
9
|
+
* unaffected: the throw requires browser globals AND the absence of a node runtime.
|
|
10
|
+
*/
|
|
11
|
+
const g = globalThis;
|
|
12
|
+
if (g.window !== undefined && g.document !== undefined && !g.process?.versions?.node) {
|
|
13
|
+
throw new Error('@molecule/api-emails-resend is SERVER-ONLY: it was bundled into browser/client code. Import it only ' +
|
|
14
|
+
'from server code (a server route/function or your API), or dynamic-import it inside ' +
|
|
15
|
+
'the server handler — never from components or shared client modules, and never ' +
|
|
16
|
+
'polyfill Buffer/process to silence this.');
|
|
17
|
+
}
|
|
18
|
+
export {};
|
|
19
|
+
//# sourceMappingURL=browser-guard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser-guard.js","sourceRoot":"","sources":["../src/browser-guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,MAAM,CAAC,GAAG,UAIT,CAAA;AACD,IAAI,CAAC,CAAC,MAAM,KAAK,SAAS,IAAI,CAAC,CAAC,QAAQ,KAAK,SAAS,IAAI,CAAC,CAAC,CAAC,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IACrF,MAAM,IAAI,KAAK,CACb,sGAAsG;QACpG,sFAAsF;QACtF,iFAAiF;QACjF,0CAA0C,CAC7C,CAAA;AACH,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resend email provider for molecule.dev.
|
|
3
|
+
*
|
|
4
|
+
* @see https://resend.com/docs/api-reference/emails/send-email
|
|
5
|
+
*
|
|
6
|
+
* @remarks
|
|
7
|
+
* - **Zero dependencies — talks to Resend's REST API with the runtime's global
|
|
8
|
+
* `fetch`** (`POST https://api.resend.com/emails`), so every app that installs
|
|
9
|
+
* this bond stays dependency-free; the official `resend` SDK can be swapped in
|
|
10
|
+
* later without changing the bond's interface.
|
|
11
|
+
* - **`from` MUST be on a domain you VERIFIED in Resend** (Resend → Domains, DNS
|
|
12
|
+
* records added and checked), or the API rejects the send with a 403
|
|
13
|
+
* `validation_error` ("domain is not verified"). Before any domain is verified
|
|
14
|
+
* the only usable sender is `onboarding@resend.dev` — testing only, and it can
|
|
15
|
+
* deliver ONLY to the account owner's own address. Read the sender from config,
|
|
16
|
+
* never hardcode a placeholder: set `RESEND_FROM` (e.g.
|
|
17
|
+
* `Acme <no-reply@your-verified-domain.com>`) and it is used whenever a
|
|
18
|
+
* message's `from` is empty; if both are missing, `sendMail()` throws a tagged
|
|
19
|
+
* config-missing error naming `RESEND_FROM`.
|
|
20
|
+
* - **Configuration is lazy and env-driven**: `RESEND_API_KEY` (and the optional
|
|
21
|
+
* `RESEND_BASE_URL` override for brokers / compatible endpoints) are read on
|
|
22
|
+
* EACH send — never at import time — so a key resolved into `process.env`
|
|
23
|
+
* after import (late secrets resolution via a secrets bond) is honored. If the
|
|
24
|
+
* key is absent at send time, `sendMail()` throws a tagged config-missing
|
|
25
|
+
* error (clean 503 / `config.notConfigured`) naming `RESEND_API_KEY` — never
|
|
26
|
+
* an opaque Resend 401.
|
|
27
|
+
* - **There is NO sandbox / test-mode flag in the Resend API** (nothing like
|
|
28
|
+
* SendGrid's `sandboxMode`). Every accepted request is a real send that counts
|
|
29
|
+
* against the account's quota — including sends to the simulation recipients
|
|
30
|
+
* `delivered@resend.dev`, `bounced@resend.dev` and `complained@resend.dev`,
|
|
31
|
+
* which are the supported way to exercise the delivery / bounce / spam paths
|
|
32
|
+
* without emailing a real inbox.
|
|
33
|
+
* - **Attachments** are sent base64-encoded inline; Resend caps the whole email
|
|
34
|
+
* at **40 MB after encoding** (larger sends are rejected). Buffer / string
|
|
35
|
+
* content only — a stream attachment throws. A `cid` becomes Resend's
|
|
36
|
+
* `content_id` for inline images (`<img src="cid:...">`).
|
|
37
|
+
* - **API errors surface as `ResendApiError`** carrying `status` (HTTP) and
|
|
38
|
+
* `code` (Resend's error name — `validation_error`, `rate_limit_exceeded`,
|
|
39
|
+
* `daily_quota_exceeded`, …). They are deliberately NOT tagged with
|
|
40
|
+
* `statusCode` / `errorKey`, so the API middleware returns its generic 500
|
|
41
|
+
* instead of echoing Resend's status to the caller. On success `accepted`
|
|
42
|
+
* echoes every `to` recipient (Resend returns no per-recipient verdict) and
|
|
43
|
+
* `messageId` is the `id` from the response body.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```typescript
|
|
47
|
+
* import { setTransport } from '@molecule/api-emails'
|
|
48
|
+
* import { provider } from '@molecule/api-emails-resend'
|
|
49
|
+
*
|
|
50
|
+
* setTransport(provider)
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
53
|
+
* @module
|
|
54
|
+
*/
|
|
55
|
+
export * from './browser-guard.js';
|
|
56
|
+
export * from './provider.js';
|
|
57
|
+
export * from './secrets.js';
|
|
58
|
+
export * from './sendMail.js';
|
|
59
|
+
export * from './transport.js';
|
|
60
|
+
export * from './types.js';
|
|
61
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AAEH,cAAc,oBAAoB,CAAA;AAClC,cAAc,eAAe,CAAA;AAC7B,cAAc,cAAc,CAAA;AAC5B,cAAc,eAAe,CAAA;AAC7B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,YAAY,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resend email provider for molecule.dev.
|
|
3
|
+
*
|
|
4
|
+
* @see https://resend.com/docs/api-reference/emails/send-email
|
|
5
|
+
*
|
|
6
|
+
* @remarks
|
|
7
|
+
* - **Zero dependencies — talks to Resend's REST API with the runtime's global
|
|
8
|
+
* `fetch`** (`POST https://api.resend.com/emails`), so every app that installs
|
|
9
|
+
* this bond stays dependency-free; the official `resend` SDK can be swapped in
|
|
10
|
+
* later without changing the bond's interface.
|
|
11
|
+
* - **`from` MUST be on a domain you VERIFIED in Resend** (Resend → Domains, DNS
|
|
12
|
+
* records added and checked), or the API rejects the send with a 403
|
|
13
|
+
* `validation_error` ("domain is not verified"). Before any domain is verified
|
|
14
|
+
* the only usable sender is `onboarding@resend.dev` — testing only, and it can
|
|
15
|
+
* deliver ONLY to the account owner's own address. Read the sender from config,
|
|
16
|
+
* never hardcode a placeholder: set `RESEND_FROM` (e.g.
|
|
17
|
+
* `Acme <no-reply@your-verified-domain.com>`) and it is used whenever a
|
|
18
|
+
* message's `from` is empty; if both are missing, `sendMail()` throws a tagged
|
|
19
|
+
* config-missing error naming `RESEND_FROM`.
|
|
20
|
+
* - **Configuration is lazy and env-driven**: `RESEND_API_KEY` (and the optional
|
|
21
|
+
* `RESEND_BASE_URL` override for brokers / compatible endpoints) are read on
|
|
22
|
+
* EACH send — never at import time — so a key resolved into `process.env`
|
|
23
|
+
* after import (late secrets resolution via a secrets bond) is honored. If the
|
|
24
|
+
* key is absent at send time, `sendMail()` throws a tagged config-missing
|
|
25
|
+
* error (clean 503 / `config.notConfigured`) naming `RESEND_API_KEY` — never
|
|
26
|
+
* an opaque Resend 401.
|
|
27
|
+
* - **There is NO sandbox / test-mode flag in the Resend API** (nothing like
|
|
28
|
+
* SendGrid's `sandboxMode`). Every accepted request is a real send that counts
|
|
29
|
+
* against the account's quota — including sends to the simulation recipients
|
|
30
|
+
* `delivered@resend.dev`, `bounced@resend.dev` and `complained@resend.dev`,
|
|
31
|
+
* which are the supported way to exercise the delivery / bounce / spam paths
|
|
32
|
+
* without emailing a real inbox.
|
|
33
|
+
* - **Attachments** are sent base64-encoded inline; Resend caps the whole email
|
|
34
|
+
* at **40 MB after encoding** (larger sends are rejected). Buffer / string
|
|
35
|
+
* content only — a stream attachment throws. A `cid` becomes Resend's
|
|
36
|
+
* `content_id` for inline images (`<img src="cid:...">`).
|
|
37
|
+
* - **API errors surface as `ResendApiError`** carrying `status` (HTTP) and
|
|
38
|
+
* `code` (Resend's error name — `validation_error`, `rate_limit_exceeded`,
|
|
39
|
+
* `daily_quota_exceeded`, …). They are deliberately NOT tagged with
|
|
40
|
+
* `statusCode` / `errorKey`, so the API middleware returns its generic 500
|
|
41
|
+
* instead of echoing Resend's status to the caller. On success `accepted`
|
|
42
|
+
* echoes every `to` recipient (Resend returns no per-recipient verdict) and
|
|
43
|
+
* `messageId` is the `id` from the response body.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```typescript
|
|
47
|
+
* import { setTransport } from '@molecule/api-emails'
|
|
48
|
+
* import { provider } from '@molecule/api-emails-resend'
|
|
49
|
+
*
|
|
50
|
+
* setTransport(provider)
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
53
|
+
* @module
|
|
54
|
+
*/
|
|
55
|
+
export * from './browser-guard.js';
|
|
56
|
+
export * from './provider.js';
|
|
57
|
+
export * from './secrets.js';
|
|
58
|
+
export * from './sendMail.js';
|
|
59
|
+
export * from './transport.js';
|
|
60
|
+
export * from './types.js';
|
|
61
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AAEH,cAAc,oBAAoB,CAAA;AAClC,cAAc,eAAe,CAAA;AAC7B,cAAc,cAAc,CAAA;AAC5B,cAAc,eAAe,CAAA;AAC7B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,YAAY,CAAA"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resend provider implementation.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
import type { EmailTransport } from '@molecule/api-emails';
|
|
7
|
+
/**
|
|
8
|
+
* The Resend email provider implementing the standard interface.
|
|
9
|
+
*/
|
|
10
|
+
export declare const provider: EmailTransport;
|
|
11
|
+
//# sourceMappingURL=provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAI1D;;GAEG;AACH,eAAO,MAAM,QAAQ,EAAE,cAEtB,CAAA"}
|
package/dist/provider.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resend provider implementation.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
import { sendMail } from './sendMail.js';
|
|
7
|
+
/**
|
|
8
|
+
* The Resend email provider implementing the standard interface.
|
|
9
|
+
*/
|
|
10
|
+
export const provider = {
|
|
11
|
+
sendMail,
|
|
12
|
+
};
|
|
13
|
+
//# sourceMappingURL=provider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider.js","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AAExC;;GAEG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAmB;IACtC,QAAQ;CACT,CAAA"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resend secret definitions — self-registered at import time so the
|
|
3
|
+
* runtime secrets registry (`@molecule/api-secrets`) can drive boot-time
|
|
4
|
+
* configuration reports and actionable "not configured" errors.
|
|
5
|
+
*
|
|
6
|
+
* Content is derived MECHANICALLY from this package's mlcl registry secrets
|
|
7
|
+
* entry (label/instructions/setupUrl/example) via the fleet formula, so
|
|
8
|
+
* packages sharing a key register byte-identical definitions and
|
|
9
|
+
* registration order never matters.
|
|
10
|
+
*
|
|
11
|
+
* @module
|
|
12
|
+
*/
|
|
13
|
+
import type { SecretDefinition } from '@molecule/api-secrets';
|
|
14
|
+
/** Secret definitions required by the Resend email bond. */
|
|
15
|
+
export declare const emailsResendSecretDefinitions: SecretDefinition[];
|
|
16
|
+
//# sourceMappingURL=secrets.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secrets.d.ts","sourceRoot":"","sources":["../src/secrets.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AAG7D,4DAA4D;AAC5D,eAAO,MAAM,6BAA6B,EAAE,gBAAgB,EAgB3D,CAAA"}
|
package/dist/secrets.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resend secret definitions — self-registered at import time so the
|
|
3
|
+
* runtime secrets registry (`@molecule/api-secrets`) can drive boot-time
|
|
4
|
+
* configuration reports and actionable "not configured" errors.
|
|
5
|
+
*
|
|
6
|
+
* Content is derived MECHANICALLY from this package's mlcl registry secrets
|
|
7
|
+
* entry (label/instructions/setupUrl/example) via the fleet formula, so
|
|
8
|
+
* packages sharing a key register byte-identical definitions and
|
|
9
|
+
* registration order never matters.
|
|
10
|
+
*
|
|
11
|
+
* @module
|
|
12
|
+
*/
|
|
13
|
+
import { registerSecrets } from '@molecule/api-secrets';
|
|
14
|
+
/** Secret definitions required by the Resend email bond. */
|
|
15
|
+
export const emailsResendSecretDefinitions = [
|
|
16
|
+
{
|
|
17
|
+
key: 'RESEND_API_KEY',
|
|
18
|
+
description: 'Resend API key — Resend → API Keys → Create API Key (Sending access is enough).',
|
|
19
|
+
helpUrl: 'https://resend.com/api-keys',
|
|
20
|
+
required: true,
|
|
21
|
+
example: 're_....',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
key: 'RESEND_FROM',
|
|
25
|
+
description: 'Resend default sender — Optional. A from address on a domain verified in Resend → Domains; used when a message has no from address.',
|
|
26
|
+
helpUrl: 'https://resend.com/domains',
|
|
27
|
+
required: false,
|
|
28
|
+
example: 'Acme <no-reply@example.com>',
|
|
29
|
+
},
|
|
30
|
+
];
|
|
31
|
+
registerSecrets(emailsResendSecretDefinitions);
|
|
32
|
+
//# sourceMappingURL=secrets.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secrets.js","sourceRoot":"","sources":["../src/secrets.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAA;AAEvD,4DAA4D;AAC5D,MAAM,CAAC,MAAM,6BAA6B,GAAuB;IAC/D;QACE,GAAG,EAAE,gBAAgB;QACrB,WAAW,EAAE,iFAAiF;QAC9F,OAAO,EAAE,6BAA6B;QACtC,QAAQ,EAAE,IAAI;QACd,OAAO,EAAE,SAAS;KACnB;IACD;QACE,GAAG,EAAE,aAAa;QAClB,WAAW,EACT,qIAAqI;QACvI,OAAO,EAAE,4BAA4B;QACrC,QAAQ,EAAE,KAAK;QACf,OAAO,EAAE,6BAA6B;KACvC;CACF,CAAA;AAED,eAAe,CAAC,6BAA6B,CAAC,CAAA"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resend email sending functionality.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
import type { EmailMessage, EmailSendResult } from '@molecule/api-emails';
|
|
7
|
+
/**
|
|
8
|
+
* Sends an email through the Resend API.
|
|
9
|
+
*
|
|
10
|
+
* @param message - The email message (to, from, subject, text/html, attachments).
|
|
11
|
+
* @returns Send result with accepted addresses, message ID, and status code.
|
|
12
|
+
*/
|
|
13
|
+
export declare const sendMail: (message: EmailMessage) => Promise<EmailSendResult>;
|
|
14
|
+
//# sourceMappingURL=sendMail.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sendMail.d.ts","sourceRoot":"","sources":["../src/sendMail.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EAGV,YAAY,EACZ,eAAe,EAChB,MAAM,sBAAsB,CAAA;AAgG7B;;;;;GAKG;AACH,eAAO,MAAM,QAAQ,GAAU,SAAS,YAAY,KAAG,OAAO,CAAC,eAAe,CA0C7E,CAAA"}
|
package/dist/sendMail.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resend email sending functionality.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
import { getLogger } from '@molecule/api-bond';
|
|
7
|
+
import { configNotConfiguredError } from '@molecule/api-secrets';
|
|
8
|
+
import { getClient } from './transport.js';
|
|
9
|
+
const logger = getLogger();
|
|
10
|
+
/** Display-name characters RFC 5322 allows unquoted (atext plus space). */
|
|
11
|
+
const UNQUOTED_NAME = /^[A-Za-z0-9 !#$%&'*+\-/=?^_`{|}~.]*$/;
|
|
12
|
+
/**
|
|
13
|
+
* Format one address the way Resend expects: a bare address, or
|
|
14
|
+
* `Name <address>`. The display name is quoted when it holds characters that
|
|
15
|
+
* would otherwise parse as address syntax (`,`, `<`, `"`, non-ASCII), and any
|
|
16
|
+
* CR/LF in it is collapsed so a display name can never inject a header.
|
|
17
|
+
* @param addr - A string email address or an `EmailAddress` object with name and address.
|
|
18
|
+
* @returns The address string for the request body.
|
|
19
|
+
*/
|
|
20
|
+
function formatAddress(addr) {
|
|
21
|
+
if (typeof addr === 'string')
|
|
22
|
+
return addr;
|
|
23
|
+
const name = addr.name?.replace(/[\r\n]+/g, ' ').trim();
|
|
24
|
+
if (!name)
|
|
25
|
+
return addr.address;
|
|
26
|
+
const display = UNQUOTED_NAME.test(name) ? name : `"${name.replace(/(["\\])/g, '\\$1')}"`;
|
|
27
|
+
return `${display} <${addr.address}>`;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Normalize a recipient field (single or array) to Resend's list of address strings.
|
|
31
|
+
* @param field - A single address or array of addresses (string or `EmailAddress`).
|
|
32
|
+
* @returns The formatted address strings.
|
|
33
|
+
*/
|
|
34
|
+
function toList(field) {
|
|
35
|
+
return (Array.isArray(field) ? field : [field]).map(formatAddress);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Extract plain email strings from a recipient field.
|
|
39
|
+
* @param field - A single address or array of addresses (string or `EmailAddress`).
|
|
40
|
+
* @returns An array of plain email strings.
|
|
41
|
+
*/
|
|
42
|
+
function extractEmails(field) {
|
|
43
|
+
return (Array.isArray(field) ? field : [field]).map((a) => typeof a === 'string' ? a : a.address);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Resolve the sender: the message's own `from` when it names an address,
|
|
47
|
+
* otherwise the `RESEND_FROM` default. Neither → a tagged config-missing error,
|
|
48
|
+
* because Resend refuses a send whose `from` is not on a verified domain and
|
|
49
|
+
* the fix is configuration, not code.
|
|
50
|
+
* @param from - The message's `from` field.
|
|
51
|
+
* @returns The sender string for the request body.
|
|
52
|
+
* @throws {Error} A tagged config-missing error (503 / `config.notConfigured`) naming `RESEND_FROM`.
|
|
53
|
+
*/
|
|
54
|
+
function resolveFrom(from) {
|
|
55
|
+
if (from !== undefined) {
|
|
56
|
+
const address = typeof from === 'string' ? from : from.address;
|
|
57
|
+
if (address.trim())
|
|
58
|
+
return formatAddress(from);
|
|
59
|
+
}
|
|
60
|
+
const fallback = process.env.RESEND_FROM?.trim();
|
|
61
|
+
if (fallback)
|
|
62
|
+
return fallback;
|
|
63
|
+
throw configNotConfiguredError('RESEND_FROM', 'email sending');
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Convert molecule attachments to Resend's attachment format (base64 content).
|
|
67
|
+
* @param attachments - The molecule email attachments to convert.
|
|
68
|
+
* @returns The Resend-format attachments.
|
|
69
|
+
* @throws {Error} When an attachment's content is a stream.
|
|
70
|
+
*/
|
|
71
|
+
function toResendAttachments(attachments) {
|
|
72
|
+
return attachments.map((att) => {
|
|
73
|
+
let content;
|
|
74
|
+
if (Buffer.isBuffer(att.content)) {
|
|
75
|
+
content = att.content.toString('base64');
|
|
76
|
+
}
|
|
77
|
+
else if (typeof att.content === 'string') {
|
|
78
|
+
const encoding = att.encoding && Buffer.isEncoding(att.encoding) ? att.encoding : 'utf-8';
|
|
79
|
+
content = Buffer.from(att.content, encoding).toString('base64');
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
throw new Error('Stream attachments are not supported by the Resend provider');
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
content,
|
|
86
|
+
filename: att.filename,
|
|
87
|
+
...(att.contentType ? { content_type: att.contentType } : {}),
|
|
88
|
+
...(att.cid ? { content_id: att.cid } : {}),
|
|
89
|
+
};
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Sends an email through the Resend API.
|
|
94
|
+
*
|
|
95
|
+
* @param message - The email message (to, from, subject, text/html, attachments).
|
|
96
|
+
* @returns Send result with accepted addresses, message ID, and status code.
|
|
97
|
+
*/
|
|
98
|
+
export const sendMail = async (message) => {
|
|
99
|
+
if (!process.env.RESEND_API_KEY) {
|
|
100
|
+
// Tagged config-missing error → the API middleware returns a clean 503 +
|
|
101
|
+
// 'config.notConfigured', and the message carries the registered
|
|
102
|
+
// definition's description + setup URL. Without this guard the request
|
|
103
|
+
// would go out unauthenticated and the caller would get an opaque Resend
|
|
104
|
+
// 401 that reads like a bad key rather than a missing one.
|
|
105
|
+
throw configNotConfiguredError('RESEND_API_KEY', 'email sending');
|
|
106
|
+
}
|
|
107
|
+
// Also outside the try: a missing sender is a config condition to surface
|
|
108
|
+
// as-is, not an error to log.
|
|
109
|
+
const from = resolveFrom(message.from);
|
|
110
|
+
try {
|
|
111
|
+
const request = {
|
|
112
|
+
from,
|
|
113
|
+
to: toList(message.to),
|
|
114
|
+
subject: message.subject,
|
|
115
|
+
...(message.text ? { text: message.text } : {}),
|
|
116
|
+
...(message.html ? { html: message.html } : {}),
|
|
117
|
+
};
|
|
118
|
+
const cc = message.cc ? toList(message.cc) : [];
|
|
119
|
+
if (cc.length)
|
|
120
|
+
request.cc = cc;
|
|
121
|
+
const bcc = message.bcc ? toList(message.bcc) : [];
|
|
122
|
+
if (bcc.length)
|
|
123
|
+
request.bcc = bcc;
|
|
124
|
+
if (message.replyTo)
|
|
125
|
+
request.reply_to = formatAddress(message.replyTo);
|
|
126
|
+
if (message.attachments?.length)
|
|
127
|
+
request.attachments = toResendAttachments(message.attachments);
|
|
128
|
+
// The client reads the API key / base URL from the environment on each
|
|
129
|
+
// send (honors late-resolved secrets).
|
|
130
|
+
const { status, id } = await getClient().send(request);
|
|
131
|
+
return {
|
|
132
|
+
accepted: extractEmails(message.to),
|
|
133
|
+
rejected: [],
|
|
134
|
+
messageId: id,
|
|
135
|
+
response: String(status),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
logger.error('Resend sendMail error:', error);
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
//# sourceMappingURL=sendMail.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sendMail.js","sourceRoot":"","sources":["../src/sendMail.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resend HTTP client — a thin `fetch` wrapper around `POST /emails`.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
import './secrets.js';
|
|
7
|
+
import type { ResendClient, ResendErrorBody } from './types.js';
|
|
8
|
+
/** Resend's REST API base URL. Override with `RESEND_BASE_URL`. */
|
|
9
|
+
export declare const RESEND_DEFAULT_BASE_URL = "https://api.resend.com";
|
|
10
|
+
/**
|
|
11
|
+
* A non-2xx response from the Resend API.
|
|
12
|
+
*
|
|
13
|
+
* `status` is the HTTP status, `code` is Resend's machine-readable error `name`
|
|
14
|
+
* (e.g. `validation_error`, `rate_limit_exceeded`, `daily_quota_exceeded`) when
|
|
15
|
+
* the body carried one, and `body` is the parsed error body. Deliberately NOT
|
|
16
|
+
* tagged with `statusCode` / `errorKey`: Resend's status describes OUR request
|
|
17
|
+
* to Resend (a 401 is a bad key, a 403 an unverified domain), not the caller's
|
|
18
|
+
* request to the app, so the API middleware must fall through to its generic
|
|
19
|
+
* 500 rather than echo it.
|
|
20
|
+
*/
|
|
21
|
+
export declare class ResendApiError extends Error {
|
|
22
|
+
/** HTTP status of the Resend response. */
|
|
23
|
+
readonly status: number;
|
|
24
|
+
/** Resend's error name from the body, when present. */
|
|
25
|
+
readonly code: string | undefined;
|
|
26
|
+
/** The parsed error body, when the response was JSON. */
|
|
27
|
+
readonly body: ResendErrorBody | undefined;
|
|
28
|
+
/**
|
|
29
|
+
* Builds the error from a non-2xx Resend response.
|
|
30
|
+
* @param status - HTTP status of the response.
|
|
31
|
+
* @param body - The parsed error body, if the response was JSON.
|
|
32
|
+
* @param rawBody - The raw response text (used in the message when the body was not JSON).
|
|
33
|
+
*/
|
|
34
|
+
constructor(status: number, body: ResendErrorBody | undefined, rawBody: string);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Returns the Resend HTTP client. Nothing is configured up front: the API key
|
|
38
|
+
* and base URL are read from the environment on every `send()`.
|
|
39
|
+
*
|
|
40
|
+
* @returns The Resend client.
|
|
41
|
+
*/
|
|
42
|
+
export declare const getClient: () => ResendClient;
|
|
43
|
+
//# sourceMappingURL=transport.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,cAAc,CAAA;AAIrB,OAAO,KAAK,EACV,YAAY,EACZ,eAAe,EAGhB,MAAM,YAAY,CAAA;AAEnB,mEAAmE;AACnE,eAAO,MAAM,uBAAuB,2BAA2B,CAAA;AAE/D;;;;;;;;;;GAUG;AACH,qBAAa,cAAe,SAAQ,KAAK;IACvC,0CAA0C;IAC1C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,uDAAuD;IACvD,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAA;IACjC,yDAAyD;IACzD,QAAQ,CAAC,IAAI,EAAE,eAAe,GAAG,SAAS,CAAA;IAE1C;;;;;OAKG;gBACS,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,eAAe,GAAG,SAAS,EAAE,OAAO,EAAE,MAAM;CAQ/E;AAsFD;;;;;GAKG;AACH,eAAO,MAAM,SAAS,QAAO,YAAsB,CAAA"}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resend HTTP client — a thin `fetch` wrapper around `POST /emails`.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
// Side-effect import: registers this bond's secret definitions so the
|
|
7
|
+
// runtime registry is populated even when transport.js is imported directly
|
|
8
|
+
// (not through the package barrel).
|
|
9
|
+
import './secrets.js';
|
|
10
|
+
import { configNotConfiguredError } from '@molecule/api-secrets';
|
|
11
|
+
/** Resend's REST API base URL. Override with `RESEND_BASE_URL`. */
|
|
12
|
+
export const RESEND_DEFAULT_BASE_URL = 'https://api.resend.com';
|
|
13
|
+
/**
|
|
14
|
+
* A non-2xx response from the Resend API.
|
|
15
|
+
*
|
|
16
|
+
* `status` is the HTTP status, `code` is Resend's machine-readable error `name`
|
|
17
|
+
* (e.g. `validation_error`, `rate_limit_exceeded`, `daily_quota_exceeded`) when
|
|
18
|
+
* the body carried one, and `body` is the parsed error body. Deliberately NOT
|
|
19
|
+
* tagged with `statusCode` / `errorKey`: Resend's status describes OUR request
|
|
20
|
+
* to Resend (a 401 is a bad key, a 403 an unverified domain), not the caller's
|
|
21
|
+
* request to the app, so the API middleware must fall through to its generic
|
|
22
|
+
* 500 rather than echo it.
|
|
23
|
+
*/
|
|
24
|
+
export class ResendApiError extends Error {
|
|
25
|
+
/** HTTP status of the Resend response. */
|
|
26
|
+
status;
|
|
27
|
+
/** Resend's error name from the body, when present. */
|
|
28
|
+
code;
|
|
29
|
+
/** The parsed error body, when the response was JSON. */
|
|
30
|
+
body;
|
|
31
|
+
/**
|
|
32
|
+
* Builds the error from a non-2xx Resend response.
|
|
33
|
+
* @param status - HTTP status of the response.
|
|
34
|
+
* @param body - The parsed error body, if the response was JSON.
|
|
35
|
+
* @param rawBody - The raw response text (used in the message when the body was not JSON).
|
|
36
|
+
*/
|
|
37
|
+
constructor(status, body, rawBody) {
|
|
38
|
+
const detail = body?.message ?? (rawBody.trim() || 'no response body');
|
|
39
|
+
super(`Resend API error ${status}${body?.name ? ` (${body.name})` : ''}: ${detail}`);
|
|
40
|
+
this.name = 'ResendApiError';
|
|
41
|
+
this.status = status;
|
|
42
|
+
this.code = body?.name;
|
|
43
|
+
this.body = body;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Whether a value is a non-null object (a JSON object on the wire).
|
|
48
|
+
* @param value - The parsed value.
|
|
49
|
+
* @returns `true` for a plain object.
|
|
50
|
+
*/
|
|
51
|
+
const isRecord = (value) => typeof value === 'object' && value !== null;
|
|
52
|
+
/**
|
|
53
|
+
* Parses a response body as JSON, tolerating an empty or non-JSON body.
|
|
54
|
+
* @param text - The raw response text.
|
|
55
|
+
* @returns The parsed value, or `undefined` when the body is empty or not JSON.
|
|
56
|
+
*/
|
|
57
|
+
const parseJson = (text) => {
|
|
58
|
+
if (!text)
|
|
59
|
+
return undefined;
|
|
60
|
+
try {
|
|
61
|
+
return JSON.parse(text);
|
|
62
|
+
}
|
|
63
|
+
catch (_error) {
|
|
64
|
+
// Intentional: a non-JSON body (an HTML gateway page, plain text) is
|
|
65
|
+
// reported by the caller via the raw text, so there is nothing to log here.
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Narrows a parsed error response to Resend's `{ statusCode, message, name }` shape.
|
|
71
|
+
* @param parsed - The parsed response body.
|
|
72
|
+
* @returns The error body, or `undefined` when the body was not a JSON object.
|
|
73
|
+
*/
|
|
74
|
+
const toErrorBody = (parsed) => {
|
|
75
|
+
if (!isRecord(parsed))
|
|
76
|
+
return undefined;
|
|
77
|
+
return {
|
|
78
|
+
statusCode: typeof parsed.statusCode === 'number' ? parsed.statusCode : null,
|
|
79
|
+
message: typeof parsed.message === 'string' ? parsed.message : undefined,
|
|
80
|
+
name: typeof parsed.name === 'string' ? parsed.name : undefined,
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* Resolves the API base URL from the environment on each call (late secrets
|
|
85
|
+
* resolution is honored), trimming any trailing slash.
|
|
86
|
+
* @returns The base URL without a trailing slash.
|
|
87
|
+
*/
|
|
88
|
+
const resolveBaseUrl = () => (process.env.RESEND_BASE_URL || RESEND_DEFAULT_BASE_URL).replace(/\/+$/, '');
|
|
89
|
+
/**
|
|
90
|
+
* The shared client. Environment is read INSIDE `send()` — never at import
|
|
91
|
+
* time — so a `RESEND_API_KEY` / `RESEND_BASE_URL` resolved into `process.env`
|
|
92
|
+
* after this module is imported (a secrets bond resolving at startup) is the
|
|
93
|
+
* value used on the next send.
|
|
94
|
+
*/
|
|
95
|
+
const client = {
|
|
96
|
+
async send(request) {
|
|
97
|
+
const apiKey = process.env.RESEND_API_KEY;
|
|
98
|
+
if (!apiKey) {
|
|
99
|
+
// Tagged config-missing error → clean 503 + 'config.notConfigured'
|
|
100
|
+
// instead of an opaque Resend 401 from an unauthenticated request.
|
|
101
|
+
throw configNotConfiguredError('RESEND_API_KEY', 'email sending');
|
|
102
|
+
}
|
|
103
|
+
const response = await fetch(`${resolveBaseUrl()}/emails`, {
|
|
104
|
+
method: 'POST',
|
|
105
|
+
headers: {
|
|
106
|
+
Authorization: `Bearer ${apiKey}`,
|
|
107
|
+
'Content-Type': 'application/json',
|
|
108
|
+
},
|
|
109
|
+
body: JSON.stringify(request),
|
|
110
|
+
});
|
|
111
|
+
const text = await response.text();
|
|
112
|
+
const parsed = parseJson(text);
|
|
113
|
+
if (!response.ok) {
|
|
114
|
+
throw new ResendApiError(response.status, toErrorBody(parsed), text);
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
status: response.status,
|
|
118
|
+
id: isRecord(parsed) && typeof parsed.id === 'string' ? parsed.id : undefined,
|
|
119
|
+
};
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
/**
|
|
123
|
+
* Returns the Resend HTTP client. Nothing is configured up front: the API key
|
|
124
|
+
* and base URL are read from the environment on every `send()`.
|
|
125
|
+
*
|
|
126
|
+
* @returns The Resend client.
|
|
127
|
+
*/
|
|
128
|
+
export const getClient = () => client;
|
|
129
|
+
//# sourceMappingURL=transport.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport.js","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for the Resend email provider.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
export type { EmailMessage, EmailSendResult, EmailTransport } from '@molecule/api-emails';
|
|
7
|
+
/**
|
|
8
|
+
* One entry of a send request's `attachments[]`, exactly as the Resend REST API
|
|
9
|
+
* expects it: base64 `content` plus snake_case metadata.
|
|
10
|
+
*/
|
|
11
|
+
export interface ResendAttachment {
|
|
12
|
+
/** File content, base64-encoded. */
|
|
13
|
+
content: string;
|
|
14
|
+
/** File name shown to the recipient; Resend derives the MIME type from it when `content_type` is unset. */
|
|
15
|
+
filename: string;
|
|
16
|
+
/** MIME type of the file. */
|
|
17
|
+
content_type?: string;
|
|
18
|
+
/** Content-ID for an inline image referenced as `cid:` in the HTML body. */
|
|
19
|
+
content_id?: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Request body for `POST /emails`. Field names are the REST API's own
|
|
23
|
+
* (snake_case, e.g. `reply_to`) — NOT the camelCase the official SDK accepts.
|
|
24
|
+
*/
|
|
25
|
+
export interface ResendSendRequest {
|
|
26
|
+
/** Sender, as a bare address or `Name <address>`. */
|
|
27
|
+
from: string;
|
|
28
|
+
/** Recipients (Resend accepts at most 50). */
|
|
29
|
+
to: string[];
|
|
30
|
+
/** Subject line. */
|
|
31
|
+
subject: string;
|
|
32
|
+
/** CC recipients. */
|
|
33
|
+
cc?: string[];
|
|
34
|
+
/** BCC recipients. */
|
|
35
|
+
bcc?: string[];
|
|
36
|
+
/** Reply-To address. */
|
|
37
|
+
reply_to?: string;
|
|
38
|
+
/** HTML body. */
|
|
39
|
+
html?: string;
|
|
40
|
+
/** Plain-text body (Resend derives one from `html` when omitted). */
|
|
41
|
+
text?: string;
|
|
42
|
+
/** File attachments. */
|
|
43
|
+
attachments?: ResendAttachment[];
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* What {@link ResendClient.send} resolves with: the HTTP status of the accepted
|
|
47
|
+
* request and the message `id` Resend returned in the response body.
|
|
48
|
+
*/
|
|
49
|
+
export interface ResendSendResponse {
|
|
50
|
+
/** HTTP status code of the response (2xx). */
|
|
51
|
+
status: number;
|
|
52
|
+
/** The `id` from the response body, when present. */
|
|
53
|
+
id?: string;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Resend's JSON error body (`{ statusCode, message, name }`), with every field
|
|
57
|
+
* optional because the wire is not trusted.
|
|
58
|
+
*/
|
|
59
|
+
export interface ResendErrorBody {
|
|
60
|
+
/** HTTP status Resend reports in the body (may be `null`). */
|
|
61
|
+
statusCode?: number | null;
|
|
62
|
+
/** Human-readable error message. */
|
|
63
|
+
message?: string;
|
|
64
|
+
/** Machine-readable error name, e.g. `validation_error`, `daily_quota_exceeded`. */
|
|
65
|
+
name?: string;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The narrow HTTP client this bond uses to reach Resend.
|
|
69
|
+
*/
|
|
70
|
+
export interface ResendClient {
|
|
71
|
+
/**
|
|
72
|
+
* Sends one email via `POST /emails`.
|
|
73
|
+
* @returns The HTTP status and the message id.
|
|
74
|
+
*/
|
|
75
|
+
send(request: ResendSendRequest): Promise<ResendSendResponse>;
|
|
76
|
+
}
|
|
77
|
+
declare global {
|
|
78
|
+
namespace NodeJS {
|
|
79
|
+
/**
|
|
80
|
+
* Process Env interface.
|
|
81
|
+
*/
|
|
82
|
+
interface ProcessEnv {
|
|
83
|
+
/**
|
|
84
|
+
* The key used for Resend's API.
|
|
85
|
+
*/
|
|
86
|
+
RESEND_API_KEY?: string;
|
|
87
|
+
/**
|
|
88
|
+
* Optional default sender (`Name <address>` or a bare address on a
|
|
89
|
+
* domain verified in Resend), used when a message's `from` is empty.
|
|
90
|
+
*/
|
|
91
|
+
RESEND_FROM?: string;
|
|
92
|
+
/**
|
|
93
|
+
* Optional base URL override for the Resend API (e.g. a credential
|
|
94
|
+
* broker or a Resend-compatible endpoint). When unset, the API's
|
|
95
|
+
* default `https://api.resend.com` is used.
|
|
96
|
+
*/
|
|
97
|
+
RESEND_BASE_URL?: string;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,YAAY,EAAE,YAAY,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAA;AAEzF;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B,oCAAoC;IACpC,OAAO,EAAE,MAAM,CAAA;IACf,2GAA2G;IAC3G,QAAQ,EAAE,MAAM,CAAA;IAChB,6BAA6B;IAC7B,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,4EAA4E;IAC5E,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC,qDAAqD;IACrD,IAAI,EAAE,MAAM,CAAA;IACZ,8CAA8C;IAC9C,EAAE,EAAE,MAAM,EAAE,CAAA;IACZ,oBAAoB;IACpB,OAAO,EAAE,MAAM,CAAA;IACf,qBAAqB;IACrB,EAAE,CAAC,EAAE,MAAM,EAAE,CAAA;IACb,sBAAsB;IACtB,GAAG,CAAC,EAAE,MAAM,EAAE,CAAA;IACd,wBAAwB;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,iBAAiB;IACjB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,qEAAqE;IACrE,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,wBAAwB;IACxB,WAAW,CAAC,EAAE,gBAAgB,EAAE,CAAA;CACjC;AAED;;;GAGG;AACH,MAAM,WAAW,kBAAkB;IACjC,8CAA8C;IAC9C,MAAM,EAAE,MAAM,CAAA;IACd,qDAAqD;IACrD,EAAE,CAAC,EAAE,MAAM,CAAA;CACZ;AAED;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,8DAA8D;IAC9D,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,oCAAoC;IACpC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,oFAAoF;IACpF,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B;;;OAGG;IACH,IAAI,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAA;CAC9D;AAED,OAAO,CAAC,MAAM,CAAC;IAEb,UAAU,MAAM,CAAC;QACf;;WAEG;QACH,UAAiB,UAAU;YACzB;;eAEG;YACH,cAAc,CAAC,EAAE,MAAM,CAAA;YAEvB;;;eAGG;YACH,WAAW,CAAC,EAAE,MAAM,CAAA;YAEpB;;;;eAIG;YACH,eAAe,CAAC,EAAE,MAAM,CAAA;SACzB;KACF;CACF"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG"}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@molecule/api-emails-resend",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Resend email provider for molecule.dev.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc",
|
|
10
|
+
"test": "vitest run",
|
|
11
|
+
"test:watch": "vitest"
|
|
12
|
+
},
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"import": "./dist/index.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"README.md"
|
|
22
|
+
],
|
|
23
|
+
"keywords": [
|
|
24
|
+
"molecule",
|
|
25
|
+
"email",
|
|
26
|
+
"resend"
|
|
27
|
+
],
|
|
28
|
+
"license": "Apache-2.0",
|
|
29
|
+
"author": "Molecule Dev, Inc. (https://molecule.dev)",
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@molecule/api-bond": "1.0.1",
|
|
32
|
+
"@types/node": "26.1.2",
|
|
33
|
+
"typescript": "6.0.3",
|
|
34
|
+
"vitest": "4.1.10"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"@molecule/api-bond": "^1.0.1",
|
|
38
|
+
"@molecule/api-emails": "^1.0.1",
|
|
39
|
+
"@molecule/api-secrets": "^1.0.1"
|
|
40
|
+
},
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "https://github.com/molecule-dev/molecule.git",
|
|
44
|
+
"directory": "packages/api/bonds/emails/resend"
|
|
45
|
+
},
|
|
46
|
+
"homepage": "https://github.com/molecule-dev/molecule/tree/main/packages/api/bonds/emails/resend",
|
|
47
|
+
"bugs": "https://github.com/molecule-dev/molecule/issues",
|
|
48
|
+
"publishConfig": {
|
|
49
|
+
"access": "public"
|
|
50
|
+
}
|
|
51
|
+
}
|