@harleyl7/relay-email 0.3.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 +123 -0
- package/index.d.ts +115 -0
- package/index.js +294 -0
- package/package.json +38 -0
package/README.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# @harleyl7/relay-email
|
|
2
|
+
|
|
3
|
+
Server-side SDK for sending and inspecting transactional email through Relay.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @harleyl7/relay-email
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { Relay } from "@harleyl7/relay-email";
|
|
15
|
+
|
|
16
|
+
const relay = new Relay(process.env.RELAY_API_KEY!);
|
|
17
|
+
|
|
18
|
+
const email = await relay.emails.send({
|
|
19
|
+
from: "noreply@client.com",
|
|
20
|
+
to: "user@example.com",
|
|
21
|
+
subject: "Welcome",
|
|
22
|
+
html: "<p>Welcome to the site.</p>",
|
|
23
|
+
text: "Welcome to the site.",
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
console.log(email.id, email.status, email.provider.messageId);
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Batch, List, and Retrieve
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
const batch = await relay.batch.send([
|
|
33
|
+
{
|
|
34
|
+
from: "noreply@client.com",
|
|
35
|
+
to: "one@example.com",
|
|
36
|
+
subject: "First message",
|
|
37
|
+
text: "Hello one",
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
from: "noreply@client.com",
|
|
41
|
+
to: "two@example.com",
|
|
42
|
+
subject: "Second message",
|
|
43
|
+
text: "Hello two",
|
|
44
|
+
},
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
const page = await relay.emails.list({ limit: 20, status: "delivered" });
|
|
48
|
+
const email = await relay.emails.get(batch.data[0].id);
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Batch payloads are validated before sending and processed sequentially. If the email provider fails mid-request, earlier messages may already have been accepted. Use an idempotency key when retrying a batch.
|
|
52
|
+
|
|
53
|
+
## Attachments
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
import { readFile } from "node:fs/promises";
|
|
57
|
+
import { Relay } from "@harleyl7/relay-email";
|
|
58
|
+
|
|
59
|
+
const relay = new Relay(process.env.RELAY_API_KEY!);
|
|
60
|
+
const pdf = await readFile("./invoice.pdf");
|
|
61
|
+
|
|
62
|
+
await relay.emails.send({
|
|
63
|
+
from: "billing@client.com",
|
|
64
|
+
to: "user@example.com",
|
|
65
|
+
subject: "Invoice",
|
|
66
|
+
text: "Attached is your invoice.",
|
|
67
|
+
attachments: [
|
|
68
|
+
{
|
|
69
|
+
filename: "invoice.pdf",
|
|
70
|
+
content: pdf,
|
|
71
|
+
contentType: "application/pdf",
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Options
|
|
78
|
+
|
|
79
|
+
Relay uses `https://relay.vanillasky.dev` by default, so production sites only need `RELAY_API_KEY`. Override `baseUrl` for a preview or self-hosted Relay instance:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
const relay = new Relay(process.env.RELAY_API_KEY!, {
|
|
83
|
+
baseUrl: "https://relay.vanillasky.dev",
|
|
84
|
+
timeoutMs: 30_000,
|
|
85
|
+
});
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Error Handling
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
import { Relay, RelayError } from "@harleyl7/relay-email";
|
|
92
|
+
|
|
93
|
+
const relay = new Relay(process.env.RELAY_API_KEY!);
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
await relay.emails.send({
|
|
97
|
+
from: "noreply@client.com",
|
|
98
|
+
to: "user@example.com",
|
|
99
|
+
subject: "Test",
|
|
100
|
+
text: "Hello",
|
|
101
|
+
});
|
|
102
|
+
} catch (error) {
|
|
103
|
+
if (error instanceof RelayError) {
|
|
104
|
+
console.error(error.status, error.message, error.details);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Intended Use
|
|
110
|
+
|
|
111
|
+
- server-side only
|
|
112
|
+
- website backends, server actions, cron jobs, and worker processes
|
|
113
|
+
- wraps Relay's send, batch, list, and retrieve endpoints
|
|
114
|
+
|
|
115
|
+
## Publish
|
|
116
|
+
|
|
117
|
+
This repo includes a GitHub Actions workflow to publish the public package to npm.
|
|
118
|
+
|
|
119
|
+
After the workflow runs successfully, any project can install without an `.npmrc` file or registry token:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
npm install @harleyl7/relay-email
|
|
123
|
+
```
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
export type RelayAddress = string | string[];
|
|
2
|
+
|
|
3
|
+
export type RelayAttachment = {
|
|
4
|
+
filename: string;
|
|
5
|
+
content: string | Blob | Buffer | Uint8Array | ArrayBuffer;
|
|
6
|
+
contentType?: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type SendEmailRequest = {
|
|
10
|
+
from: string;
|
|
11
|
+
to: RelayAddress;
|
|
12
|
+
cc?: RelayAddress;
|
|
13
|
+
bcc?: RelayAddress;
|
|
14
|
+
replyTo?: RelayAddress;
|
|
15
|
+
subject: string;
|
|
16
|
+
html?: string;
|
|
17
|
+
text?: string;
|
|
18
|
+
headers?: Record<string, string>;
|
|
19
|
+
attachments?: RelayAttachment[];
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type SendEmailResponse = {
|
|
23
|
+
id: string;
|
|
24
|
+
object: "email";
|
|
25
|
+
createdAt: string;
|
|
26
|
+
sentAt: string | null;
|
|
27
|
+
from: string;
|
|
28
|
+
to: string[];
|
|
29
|
+
cc: string[];
|
|
30
|
+
bcc: string[];
|
|
31
|
+
replyTo: string[];
|
|
32
|
+
subject: string;
|
|
33
|
+
html: string | null;
|
|
34
|
+
text: string | null;
|
|
35
|
+
headers: Record<string, string>;
|
|
36
|
+
status: string;
|
|
37
|
+
provider: {
|
|
38
|
+
name: "ses";
|
|
39
|
+
messageId: string | null;
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export type EmailEvent = {
|
|
44
|
+
id: string;
|
|
45
|
+
type: string;
|
|
46
|
+
createdAt: string;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export type EmailAttachment = {
|
|
50
|
+
id: string;
|
|
51
|
+
filename: string;
|
|
52
|
+
contentType: string;
|
|
53
|
+
sizeBytes: number;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export type GetEmailResponse = SendEmailResponse & {
|
|
57
|
+
events: EmailEvent[];
|
|
58
|
+
attachments: EmailAttachment[];
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export type ListEmailsOptions = {
|
|
62
|
+
limit?: number;
|
|
63
|
+
cursor?: string;
|
|
64
|
+
status?: string;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export type ListEmailsResponse = {
|
|
68
|
+
object: "list";
|
|
69
|
+
data: SendEmailResponse[];
|
|
70
|
+
hasMore: boolean;
|
|
71
|
+
nextCursor: string | null;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export type BatchSendEmailResponse = {
|
|
75
|
+
object: "list";
|
|
76
|
+
data: SendEmailResponse[];
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export type RelayRequestOptions = {
|
|
80
|
+
idempotencyKey?: string;
|
|
81
|
+
timeoutMs?: number;
|
|
82
|
+
signal?: AbortSignal;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export type RelayOptions = {
|
|
86
|
+
baseUrl?: string;
|
|
87
|
+
timeoutMs?: number;
|
|
88
|
+
fetch?: typeof fetch;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export declare class RelayError extends Error {
|
|
92
|
+
status: number;
|
|
93
|
+
details: unknown;
|
|
94
|
+
constructor(message: string, options?: { status?: number; details?: unknown });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export declare class Relay {
|
|
98
|
+
constructor(apiKey: string, options?: RelayOptions);
|
|
99
|
+
|
|
100
|
+
emails: {
|
|
101
|
+
send: (payload: SendEmailRequest, options?: RelayRequestOptions) => Promise<SendEmailResponse>;
|
|
102
|
+
get: (id: string, options?: Omit<RelayRequestOptions, "idempotencyKey">) => Promise<GetEmailResponse>;
|
|
103
|
+
list: (
|
|
104
|
+
options?: ListEmailsOptions,
|
|
105
|
+
requestOptions?: Omit<RelayRequestOptions, "idempotencyKey">,
|
|
106
|
+
) => Promise<ListEmailsResponse>;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
batch: {
|
|
110
|
+
send: (
|
|
111
|
+
payloads: SendEmailRequest[],
|
|
112
|
+
options?: RelayRequestOptions,
|
|
113
|
+
) => Promise<BatchSendEmailResponse>;
|
|
114
|
+
};
|
|
115
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
const DEFAULT_BASE_URL = "https://relay.vanillasky.dev";
|
|
2
|
+
|
|
3
|
+
export class RelayError extends Error {
|
|
4
|
+
constructor(message, options = {}) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "RelayError";
|
|
7
|
+
this.status = options.status ?? 500;
|
|
8
|
+
this.details = options.details;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class Relay {
|
|
13
|
+
constructor(apiKey, options = {}) {
|
|
14
|
+
if (!apiKey || typeof apiKey !== "string") {
|
|
15
|
+
throw new RelayError("A Relay API key is required.");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
this.apiKey = apiKey;
|
|
19
|
+
this.baseUrl = normalizeBaseUrl(options.baseUrl ?? DEFAULT_BASE_URL);
|
|
20
|
+
this.fetchImpl = options.fetch ?? globalThis.fetch;
|
|
21
|
+
this.timeoutMs = options.timeoutMs ?? 30_000;
|
|
22
|
+
|
|
23
|
+
if (typeof this.fetchImpl !== "function") {
|
|
24
|
+
throw new RelayError("Global fetch is unavailable. Provide a custom fetch implementation.");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
this.emails = {
|
|
28
|
+
send: (payload, requestOptions) => this.sendEmail(payload, requestOptions),
|
|
29
|
+
get: (id, requestOptions) => this.getEmail(id, requestOptions),
|
|
30
|
+
list: (options, requestOptions) => this.listEmails(options, requestOptions),
|
|
31
|
+
};
|
|
32
|
+
this.batch = {
|
|
33
|
+
send: (payloads, requestOptions) => this.sendBatch(payloads, requestOptions),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async sendEmail(payload, requestOptions = {}) {
|
|
38
|
+
validatePayload(payload);
|
|
39
|
+
|
|
40
|
+
const headers = new Headers({
|
|
41
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
let body;
|
|
45
|
+
|
|
46
|
+
if (payload.attachments?.length) {
|
|
47
|
+
body = buildMultipartBody(payload);
|
|
48
|
+
} else {
|
|
49
|
+
headers.set("Content-Type", "application/json");
|
|
50
|
+
body = JSON.stringify({
|
|
51
|
+
from: payload.from,
|
|
52
|
+
to: payload.to,
|
|
53
|
+
cc: payload.cc,
|
|
54
|
+
bcc: payload.bcc,
|
|
55
|
+
replyTo: payload.replyTo,
|
|
56
|
+
subject: payload.subject,
|
|
57
|
+
html: payload.html,
|
|
58
|
+
text: payload.text,
|
|
59
|
+
headers: payload.headers,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (requestOptions.idempotencyKey) {
|
|
64
|
+
headers.set("Idempotency-Key", requestOptions.idempotencyKey);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return this.request("/api/emails", {
|
|
68
|
+
method: "POST",
|
|
69
|
+
headers,
|
|
70
|
+
body,
|
|
71
|
+
...requestOptions,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async sendBatch(payloads, requestOptions = {}) {
|
|
76
|
+
if (!Array.isArray(payloads) || payloads.length === 0 || payloads.length > 100) {
|
|
77
|
+
throw new RelayError("Batch payload must contain between 1 and 100 emails.");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
for (const payload of payloads) {
|
|
81
|
+
validatePayload(payload);
|
|
82
|
+
if (payload.attachments?.length) {
|
|
83
|
+
throw new RelayError("Batch sends do not support attachments. Send those emails individually.");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const headers = new Headers({
|
|
88
|
+
"Content-Type": "application/json",
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
if (requestOptions.idempotencyKey) {
|
|
92
|
+
headers.set("Idempotency-Key", requestOptions.idempotencyKey);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return this.request("/api/emails/batch", {
|
|
96
|
+
method: "POST",
|
|
97
|
+
headers,
|
|
98
|
+
body: JSON.stringify(payloads),
|
|
99
|
+
...requestOptions,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async getEmail(id, requestOptions = {}) {
|
|
104
|
+
if (!id || typeof id !== "string") {
|
|
105
|
+
throw new RelayError("An email ID is required.");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return this.request(`/api/emails/${encodeURIComponent(id)}`, requestOptions);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async listEmails(options = {}, requestOptions = {}) {
|
|
112
|
+
const searchParams = new URLSearchParams();
|
|
113
|
+
|
|
114
|
+
if (options.limit !== undefined) {
|
|
115
|
+
searchParams.set("limit", String(options.limit));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (options.cursor) {
|
|
119
|
+
searchParams.set("cursor", options.cursor);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (options.status) {
|
|
123
|
+
searchParams.set("status", options.status);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const query = searchParams.size > 0 ? `?${searchParams.toString()}` : "";
|
|
127
|
+
return this.request(`/api/emails${query}`, requestOptions);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async request(path, options = {}) {
|
|
131
|
+
const controller = new AbortController();
|
|
132
|
+
const timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? this.timeoutMs);
|
|
133
|
+
const headers = new Headers(options.headers);
|
|
134
|
+
headers.set("Authorization", `Bearer ${this.apiKey}`);
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
138
|
+
method: options.method ?? "GET",
|
|
139
|
+
headers,
|
|
140
|
+
body: options.body,
|
|
141
|
+
signal: options.signal ?? controller.signal,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const json = await parseJsonResponse(response);
|
|
145
|
+
|
|
146
|
+
if (!response.ok) {
|
|
147
|
+
throw new RelayError(getErrorMessage(json), {
|
|
148
|
+
status: response.status,
|
|
149
|
+
details: json,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return json;
|
|
154
|
+
} catch (error) {
|
|
155
|
+
if (error instanceof RelayError) {
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (error?.name === "AbortError") {
|
|
160
|
+
throw new RelayError("Relay request timed out.", {
|
|
161
|
+
status: 408,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
throw new RelayError(error instanceof Error ? error.message : "Unknown Relay error.");
|
|
166
|
+
} finally {
|
|
167
|
+
clearTimeout(timeout);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function getErrorMessage(response) {
|
|
173
|
+
if (typeof response?.error === "string") {
|
|
174
|
+
return response.error;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (typeof response?.error?.message === "string") {
|
|
178
|
+
return response.error.message;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return "Relay request failed.";
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function normalizeBaseUrl(baseUrl) {
|
|
185
|
+
return baseUrl.replace(/\/+$/, "");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function validatePayload(payload) {
|
|
189
|
+
if (!payload || typeof payload !== "object") {
|
|
190
|
+
throw new RelayError("Email payload must be an object.");
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (!payload.from || typeof payload.from !== "string") {
|
|
194
|
+
throw new RelayError("`from` is required.");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (!payload.subject || typeof payload.subject !== "string") {
|
|
198
|
+
throw new RelayError("`subject` is required.");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (!payload.html && !payload.text) {
|
|
202
|
+
throw new RelayError("Either `html` or `text` is required.");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (
|
|
206
|
+
!payload.to ||
|
|
207
|
+
(typeof payload.to !== "string" && !Array.isArray(payload.to)) ||
|
|
208
|
+
(Array.isArray(payload.to) && payload.to.length === 0)
|
|
209
|
+
) {
|
|
210
|
+
throw new RelayError("`to` must be a non-empty string or string array.");
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function buildMultipartBody(payload) {
|
|
215
|
+
const formData = new FormData();
|
|
216
|
+
|
|
217
|
+
appendAddressField(formData, "from", payload.from);
|
|
218
|
+
appendAddressField(formData, "to", payload.to);
|
|
219
|
+
appendAddressField(formData, "cc", payload.cc);
|
|
220
|
+
appendAddressField(formData, "bcc", payload.bcc);
|
|
221
|
+
appendAddressField(formData, "replyTo", payload.replyTo);
|
|
222
|
+
formData.set("subject", payload.subject);
|
|
223
|
+
|
|
224
|
+
if (payload.html) {
|
|
225
|
+
formData.set("html", payload.html);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (payload.text) {
|
|
229
|
+
formData.set("text", payload.text);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (payload.headers) {
|
|
233
|
+
formData.set("headers", JSON.stringify(payload.headers));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
for (const attachment of payload.attachments ?? []) {
|
|
237
|
+
const blob = toBlob(attachment.content, attachment.contentType);
|
|
238
|
+
formData.append("attachments", blob, attachment.filename);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return formData;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function appendAddressField(formData, key, value) {
|
|
245
|
+
if (!value) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (Array.isArray(value)) {
|
|
250
|
+
for (const entry of value) {
|
|
251
|
+
formData.append(key, entry);
|
|
252
|
+
}
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
formData.set(key, value);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function toBlob(content, contentType) {
|
|
260
|
+
if (content instanceof Blob) {
|
|
261
|
+
return content;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (typeof content === "string") {
|
|
265
|
+
return new Blob([content], {
|
|
266
|
+
type: contentType ?? "text/plain",
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (content instanceof ArrayBuffer || ArrayBuffer.isView(content)) {
|
|
271
|
+
return new Blob([content], {
|
|
272
|
+
type: contentType ?? "application/octet-stream",
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
throw new RelayError("Unsupported attachment content. Use string, Blob, Buffer, Uint8Array, or ArrayBuffer.");
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function parseJsonResponse(response) {
|
|
280
|
+
const text = await response.text();
|
|
281
|
+
|
|
282
|
+
if (!text) {
|
|
283
|
+
return {};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
return JSON.parse(text);
|
|
288
|
+
} catch {
|
|
289
|
+
throw new RelayError("Relay returned a non-JSON response.", {
|
|
290
|
+
status: response.status,
|
|
291
|
+
details: text,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@harleyl7/relay-email",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Server-side SDK for sending and inspecting transactional email through Relay.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./index.js",
|
|
7
|
+
"types": "./index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"index.js",
|
|
10
|
+
"index.d.ts",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./index.d.ts",
|
|
16
|
+
"default": "./index.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public",
|
|
21
|
+
"registry": "https://registry.npmjs.org/"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=18"
|
|
25
|
+
},
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/HarleyL7/relay.git",
|
|
29
|
+
"directory": "packages/relay-email"
|
|
30
|
+
},
|
|
31
|
+
"homepage": "https://github.com/HarleyL7/relay/tree/main/packages/relay-email",
|
|
32
|
+
"keywords": [
|
|
33
|
+
"relay",
|
|
34
|
+
"email",
|
|
35
|
+
"sdk",
|
|
36
|
+
"ses"
|
|
37
|
+
]
|
|
38
|
+
}
|