kempo-resend 0.0.0-stage → 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 +41 -2
- package/admin/settings-tab.global.html +1 -0
- package/hooks/send.js +13 -0
- package/install.js +24 -0
- package/kempo-config.json +21 -0
- package/package.json +30 -4
- package/server/utils/resend.js +47 -0
- package/uninstall.js +2 -0
- package/update.js +6 -0
package/README.md
CHANGED
|
@@ -1,3 +1,42 @@
|
|
|
1
|
-
#
|
|
1
|
+
# kempo-resend
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Sends the emails kempo logs through [Resend](https://resend.com). Kempo does not send email itself: it saves every email in its log (**Admin > Emails**) and hands it to an email extension through the `email:send` hook. This is that extension for Resend.
|
|
4
|
+
|
|
5
|
+
It has no dependencies: it makes one HTTPS call to Resend's API per email.
|
|
6
|
+
|
|
7
|
+
## Setup
|
|
8
|
+
|
|
9
|
+
1. Install it from Admin > Extensions.
|
|
10
|
+
2. On **Admin > Settings**, open the **Resend** tab and paste your API key (from [resend.com/api-keys](https://resend.com/api-keys)). It is stored encrypted and never shown again.
|
|
11
|
+
3. Choose what Resend sends as. Either set **Email from** (System tab) to an address on a domain you have verified with Resend, such as `Your Shop <orders@yourdomain.com>`, or set **Resend From** on the Resend tab, which is used for Resend only and wins over Email from. Resend refuses any other sender.
|
|
12
|
+
|
|
13
|
+
**Trying it before you verify a domain:** put `onboarding@resend.dev` in **Resend From** on the Resend tab. Resend lets any account send as that address, but only to the email address you signed up to Resend with.
|
|
14
|
+
4. On **Admin > Emails**, press **Send a test email** to check it.
|
|
15
|
+
|
|
16
|
+
`SETTINGS_ENCRYPTION_KEY` must be set on the server (kempo makes one for you the first time it starts), because the key is stored encrypted.
|
|
17
|
+
|
|
18
|
+
**Coming from kempo before email extensions:** the key used to be `RESEND_API_KEY` in `.env`. If that is still set when you install this and no key is saved here, it is saved as the setting for you. The `.env` value is left alone.
|
|
19
|
+
|
|
20
|
+
## What it does
|
|
21
|
+
|
|
22
|
+
For each email kempo asks to send, the handler posts `{ from, to, subject, html }` to `https://api.resend.com/emails` with `Authorization: Bearer <key>`, and answers kempo with `{ sent: true, provider: 'resend', messageId }` (the id Resend gives) or `{ sent: false, error }`. Kempo records the result in the log, tries a failed email again (the **Email max retries** setting, 1 by default), and keeps the log for the retention settings. A failure shows in the log in Resend's own words:
|
|
23
|
+
|
|
24
|
+
| Resend says | The log says |
|
|
25
|
+
|---|---|
|
|
26
|
+
| 401, or 403 about the key | Resend did not accept the API key |
|
|
27
|
+
| 403 about a domain | Resend: The example.com domain is not verified |
|
|
28
|
+
| 422 | Resend: Invalid `to` field |
|
|
29
|
+
| 429 | Resend: Too many requests |
|
|
30
|
+
|
|
31
|
+
With no key saved the handler answers that it did not send, so if another email extension is installed (kempo-smtp), it gets the email instead. The first one to send it ends the chain.
|
|
32
|
+
|
|
33
|
+
## Settings
|
|
34
|
+
|
|
35
|
+
| Setting | Meaning |
|
|
36
|
+
|---|---|
|
|
37
|
+
| `resend_api_key` | Secret. Your Resend API key |
|
|
38
|
+
| `resend_from` | Optional. The address to send as through Resend, instead of Email from. Empty uses Email from |
|
|
39
|
+
|
|
40
|
+
## Tests
|
|
41
|
+
|
|
42
|
+
`npm test` covers the request, every kind of failure and the key migration with a fake `fetch`. It never contacts Resend.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<content id="kempo-resend-settings-tab" name="Resend settings tab" owner="kempo-resend" enabled="true" locked="true" location="admin-settings-tabs" priority="20"><k-settings-tab name="resend" label="Resend" position="400"><k-settings-panel owner="kempo-resend"></k-settings-panel></k-settings-tab></content>
|
package/hooks/send.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { getSetting } from 'kempo/server/sdk.js';
|
|
2
|
+
import { sendWithResend } from '../server/utils/resend.js';
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
The `email:send` handler: sends the email through Resend (as the Resend tab's From address when one is set, else as the site's
|
|
6
|
+
Email from) and says how it went (kempo keeps the log). With no API key saved it answers
|
|
7
|
+
that it did not send, so another email extension, if there is one, gets its turn.
|
|
8
|
+
*/
|
|
9
|
+
export default async ({ email }) => {
|
|
10
|
+
const [, apiKey] = await getSetting('kempo-resend', 'resend_api_key', '');
|
|
11
|
+
const [, from] = await getSetting('kempo-resend', 'resend_from', '');
|
|
12
|
+
return sendWithResend({ apiKey, email, from });
|
|
13
|
+
};
|
package/install.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { getSetting, setSetting } from 'kempo/server/sdk.js';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
Before kempo had email extensions its Resend key was `RESEND_API_KEY` in the site's .env. If that is set and no key is saved yet, it is
|
|
5
|
+
saved as this extension's setting (encrypted), so a site that already used Resend only has to install this. The .env value is left
|
|
6
|
+
alone, and once a key is saved here it is never overwritten.
|
|
7
|
+
*/
|
|
8
|
+
export const migrateKey = ({ env, saved }) => (!saved && env ? env.trim() : '');
|
|
9
|
+
|
|
10
|
+
export default async () => {
|
|
11
|
+
if(!process.env.SETTINGS_ENCRYPTION_KEY){
|
|
12
|
+
console.warn('[kempo-resend] SETTINGS_ENCRYPTION_KEY is not set, so the Resend API key cannot be stored. Start the server once to have kempo make one, then reinstall this extension.');
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const [, saved] = await getSetting('kempo-resend', 'resend_api_key', '');
|
|
17
|
+
const key = migrateKey({ env: process.env.RESEND_API_KEY, saved });
|
|
18
|
+
if(key){
|
|
19
|
+
const [error] = await setSetting('kempo-resend', 'resend_api_key', key, 'secret', false, 'Your Resend API key, from resend.com/api-keys. Stored encrypted and never shown again. The address emails are sent from must be on a domain you have verified with Resend');
|
|
20
|
+
if(error) console.warn(`[kempo-resend] The key from RESEND_API_KEY could not be saved: ${error.msg}`);
|
|
21
|
+
else console.log('[kempo-resend] Saved RESEND_API_KEY from the environment as the Resend API key setting.');
|
|
22
|
+
}
|
|
23
|
+
console.log('[kempo-resend] Installed. Add your Resend API key on the Resend tab of the Settings page, and set the Email from address (on a domain verified with Resend).');
|
|
24
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"docs": "https://github.com/dustinpoissant/kempo-resend#readme",
|
|
3
|
+
"git": "https://github.com/dustinpoissant/kempo-resend",
|
|
4
|
+
"hooks": {
|
|
5
|
+
"email:send": "./hooks/send.js"
|
|
6
|
+
},
|
|
7
|
+
"settings": [
|
|
8
|
+
{
|
|
9
|
+
"name": "resend_from",
|
|
10
|
+
"value": "",
|
|
11
|
+
"type": "string",
|
|
12
|
+
"description": "The address to send as through Resend, instead of the site's Email from setting, as name@yourdomain.com or Shop Name <name@yourdomain.com>. Leave empty to use Email from. For testing, Resend lets any account send as onboarding@resend.dev, but only to the address you signed up to Resend with"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "resend_api_key",
|
|
16
|
+
"value": "",
|
|
17
|
+
"type": "secret",
|
|
18
|
+
"description": "Your Resend API key, from resend.com/api-keys. Stored encrypted and never shown again. The address emails are sent from must be on a domain you have verified with Resend"
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,32 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kempo-resend",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Sends the emails kempo logs through Resend (resend.com), with the API key kept as an encrypted setting.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Dustin Poissant",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"test": "node --test"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"admin",
|
|
13
|
+
"hooks",
|
|
14
|
+
"server",
|
|
15
|
+
"install.js",
|
|
16
|
+
"update.js",
|
|
17
|
+
"uninstall.js",
|
|
18
|
+
"kempo-config.json"
|
|
19
|
+
],
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/dustinpoissant/kempo-resend.git"
|
|
23
|
+
},
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"kempo": ">=4.8.0"
|
|
26
|
+
},
|
|
27
|
+
"exports": {
|
|
28
|
+
"./install": "./install.js",
|
|
29
|
+
"./update": "./update.js",
|
|
30
|
+
"./uninstall": "./uninstall.js"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Sending one email through Resend's HTTP API (resend.com/docs/api-reference/emails/send-email). It is given the key and a `fetch`, so it
|
|
3
|
+
is tested with a fake one and Resend is never needed. There is no SDK: the call is one POST.
|
|
4
|
+
|
|
5
|
+
`POST https://api.resend.com/emails` with `Authorization: Bearer <key>` and `{ from, to, subject, html }`. A success is 200 with
|
|
6
|
+
`{ id }`. A failure is a 4xx or 5xx with `{ statusCode, name, message }`, such as 401 for a bad key, 403 for a sender address on a
|
|
7
|
+
domain that is not verified, 422 for an address that is not valid, 429 when sending too fast.
|
|
8
|
+
*/
|
|
9
|
+
export const ENDPOINT = 'https://api.resend.com/emails';
|
|
10
|
+
|
|
11
|
+
/* name@example.com, or Shop Name <name@example.com>, on one line. */
|
|
12
|
+
export const isAddress = value => typeof value === 'string' && value.length <= 200 && /^(?:[^<>\r\n@]+<[^<>\s@]+@[^<>\s@]+\.[^<>\s@]+>|[^<>\s@]+@[^<>\s@]+\.[^<>\s@]+)$/.test(value.trim());
|
|
13
|
+
|
|
14
|
+
/*
|
|
15
|
+
`from` is the Resend tab's own sender, which wins over the site's (`email.from`) when it is set, so Resend can send as an address that is on
|
|
16
|
+
a domain verified with it (or as onboarding@resend.dev while testing) whatever the rest of the site sends as.
|
|
17
|
+
|
|
18
|
+
Resolves to `{ sent: true, provider, messageId }` or `{ sent: false, error }` (with `skipped: true` when there is no key, so kempo knows it did not try): what an email:send handler answers. It never throws. */
|
|
19
|
+
export const sendWithResend = async ({ apiKey, email, from: override = '', fetchImpl = globalThis.fetch }) => {
|
|
20
|
+
if(!apiKey) return { sent: false, skipped: true, error: 'No Resend API key is saved. Add one on the Resend tab of the Settings page' };
|
|
21
|
+
|
|
22
|
+
const from = String(override ?? '').trim() || email.from;
|
|
23
|
+
if(override && String(override).trim() && !isAddress(String(override).trim())) return { sent: false, error: `The Resend From setting (${String(override).trim()}) must look like name@yourdomain.com or Shop Name <name@yourdomain.com>` };
|
|
24
|
+
|
|
25
|
+
let response;
|
|
26
|
+
try {
|
|
27
|
+
response = await fetchImpl(ENDPOINT, {
|
|
28
|
+
method: 'POST',
|
|
29
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
|
|
30
|
+
body: JSON.stringify({ from, to: [email.to], subject: email.subject, html: email.html }),
|
|
31
|
+
});
|
|
32
|
+
} catch(error) {
|
|
33
|
+
return { sent: false, error: `Resend could not be reached: ${error?.message ?? 'no answer'}` };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let body = null;
|
|
37
|
+
try { body = await response.json(); } catch { /* an answer that is not JSON is reported by its status */ }
|
|
38
|
+
|
|
39
|
+
if(response.status === 401 || (response.status === 403 && /api key/i.test(body?.message ?? ''))) return { sent: false, error: 'Resend did not accept the API key' };
|
|
40
|
+
if(!response.ok){
|
|
41
|
+
const reason = body?.message ?? `answered ${response.status}`;
|
|
42
|
+
const sender = /domain/i.test(reason) && [403, 422].includes(response.status) ? ` (it was sent as ${from}). Set ${String(override ?? '').trim() ? 'Resend From on the Resend tab' : 'Email from, on the System tab of the Settings page,'} to an address on a domain verified with Resend` : '';
|
|
43
|
+
return { sent: false, error: `Resend: ${reason}${sender}` };
|
|
44
|
+
}
|
|
45
|
+
if(!body?.id) return { sent: false, error: 'Resend answered without saying the email was accepted' };
|
|
46
|
+
return { sent: true, provider: 'resend', messageId: body.id };
|
|
47
|
+
};
|
package/uninstall.js
ADDED