@dungarees/email 0.11.4

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.
@@ -0,0 +1,4 @@
1
+ import { z } from 'zod';
2
+ export declare const emailAddress: any;
3
+ export type EmailAddress = z.infer<typeof emailAddress> & {};
4
+ export declare const isEmailAddress: (value: unknown) => value is EmailAddress;
@@ -0,0 +1,3 @@
1
+ import { z } from 'zod';
2
+ export const emailAddress = z.string().email();
3
+ export const isEmailAddress = (value) => emailAddress.safeParse(value).success;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,34 @@
1
+ import { emailAddress, isEmailAddress } from './email-address.js';
2
+ import { expect, test } from 'vitest';
3
+ test('isEmailAddress accepts an ordinary address', () => {
4
+ expect(isEmailAddress('someone@example.com')).toBe(true);
5
+ });
6
+ test('isEmailAddress accepts an address with a subdomain and a plus tag', () => {
7
+ expect(isEmailAddress('someone+tag@mail.example.co.uk')).toBe(true);
8
+ });
9
+ test('isEmailAddress rejects a string with no at sign', () => {
10
+ expect(isEmailAddress('someone.example.com')).toBe(false);
11
+ });
12
+ test('isEmailAddress rejects a string with no domain', () => {
13
+ expect(isEmailAddress('someone@')).toBe(false);
14
+ });
15
+ test('isEmailAddress rejects an empty string', () => {
16
+ expect(isEmailAddress('')).toBe(false);
17
+ });
18
+ test('isEmailAddress rejects a value that is not a string at all', () => {
19
+ expect(isEmailAddress(42)).toBe(false);
20
+ expect(isEmailAddress(undefined)).toBe(false);
21
+ expect(isEmailAddress(null)).toBe(false);
22
+ });
23
+ test('isEmailAddress narrows the value it accepts', () => {
24
+ const value = 'someone@example.com';
25
+ if (isEmailAddress(value)) {
26
+ expect(value.split('@')[1]).toBe('example.com');
27
+ }
28
+ });
29
+ test('the schema parses a valid address', () => {
30
+ expect(emailAddress.parse('someone@example.com')).toBe('someone@example.com');
31
+ });
32
+ test('the schema refuses an invalid address', () => {
33
+ expect(emailAddress.safeParse('not-an-address').success).toBe(false);
34
+ });
package/fake.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ import type { EmailAddress } from './email-address.ts';
2
+ import type { Email, EmailBackend, VerificationAttempt, VerificationRequest } from './type.ts';
3
+ export type FakeEmailBackend = {
4
+ backend: EmailBackend;
5
+ received: Email[];
6
+ verificationRequests: VerificationRequest[];
7
+ verificationAttempts: VerificationAttempt[];
8
+ };
9
+ export declare const FAKE_VERIFICATION_FROM: EmailAddress;
10
+ export declare const createFakeEmailBackend: () => FakeEmailBackend;
package/fake.js ADDED
@@ -0,0 +1,35 @@
1
+ import { isVerificationApproved } from './sendgrid.js';
2
+ import { of } from 'rxjs';
3
+ export const FAKE_VERIFICATION_FROM = 'fake-verification@example.com';
4
+ export const createFakeEmailBackend = () => {
5
+ const received = [];
6
+ const verificationRequests = [];
7
+ const verificationAttempts = [];
8
+ return {
9
+ backend: {
10
+ send: async (email) => {
11
+ received.push(email);
12
+ },
13
+ requestVerification: (request) => {
14
+ verificationRequests.push(request);
15
+ received.push({
16
+ to: request.to,
17
+ from: FAKE_VERIFICATION_FROM,
18
+ subject: 'Verification code',
19
+ body: `Verification code for ${request.to}: ${request.code ?? '(none issued)'}`,
20
+ isHtml: false,
21
+ });
22
+ return of(undefined);
23
+ },
24
+ // Judges the attempt the same way the real backend does, so a test that gets the code wrong
25
+ // fails here too rather than passing against a fake that always approves.
26
+ verify: (attempt) => {
27
+ verificationAttempts.push(attempt);
28
+ return of({ status: isVerificationApproved(attempt) ? 'verified' : 'failed' });
29
+ },
30
+ },
31
+ received,
32
+ verificationRequests,
33
+ verificationAttempts,
34
+ };
35
+ };
package/package.json ADDED
@@ -0,0 +1,461 @@
1
+ {
2
+ "name": "@dungarees/email",
3
+ "engines": {
4
+ "node": ">=25.0.0"
5
+ },
6
+ "scripts": {
7
+ "type-check": "tsc --noEmit"
8
+ },
9
+ "type": "module",
10
+ "dependencies": {
11
+ "@dungarees/core": "*",
12
+ "@sendgrid/mail": "^8.1.5",
13
+ "rxjs": "^7.8.1",
14
+ "zod": "^3.25.76"
15
+ },
16
+ "devDependencies": {
17
+ "typescript": "^5.9.3",
18
+ "vitest": "^3.0.2"
19
+ },
20
+ "author": "info@productkind.com",
21
+ "license": "MIT",
22
+ "version": "0.11.4",
23
+ "exports": {
24
+ "./email-address.test.ts": {
25
+ "import": "./email-address.test.js",
26
+ "types": "./email-address.test.d.ts"
27
+ },
28
+ "./email-address.ts": {
29
+ "import": "./email-address.js",
30
+ "types": "./email-address.d.ts"
31
+ },
32
+ "./fake.ts": {
33
+ "import": "./fake.js",
34
+ "types": "./fake.d.ts"
35
+ },
36
+ "./node_modules/typescript/lib/lib.d.ts": {
37
+ "import": "./node_modules/typescript/lib/lib.d.js",
38
+ "types": "./node_modules/typescript/lib/lib.d.d.ts"
39
+ },
40
+ "./node_modules/typescript/lib/lib.decorators.d.ts": {
41
+ "import": "./node_modules/typescript/lib/lib.decorators.d.js",
42
+ "types": "./node_modules/typescript/lib/lib.decorators.d.d.ts"
43
+ },
44
+ "./node_modules/typescript/lib/lib.decorators.legacy.d.ts": {
45
+ "import": "./node_modules/typescript/lib/lib.decorators.legacy.d.js",
46
+ "types": "./node_modules/typescript/lib/lib.decorators.legacy.d.d.ts"
47
+ },
48
+ "./node_modules/typescript/lib/lib.dom.asynciterable.d.ts": {
49
+ "import": "./node_modules/typescript/lib/lib.dom.asynciterable.d.js",
50
+ "types": "./node_modules/typescript/lib/lib.dom.asynciterable.d.d.ts"
51
+ },
52
+ "./node_modules/typescript/lib/lib.dom.d.ts": {
53
+ "import": "./node_modules/typescript/lib/lib.dom.d.js",
54
+ "types": "./node_modules/typescript/lib/lib.dom.d.d.ts"
55
+ },
56
+ "./node_modules/typescript/lib/lib.dom.iterable.d.ts": {
57
+ "import": "./node_modules/typescript/lib/lib.dom.iterable.d.js",
58
+ "types": "./node_modules/typescript/lib/lib.dom.iterable.d.d.ts"
59
+ },
60
+ "./node_modules/typescript/lib/lib.es2015.collection.d.ts": {
61
+ "import": "./node_modules/typescript/lib/lib.es2015.collection.d.js",
62
+ "types": "./node_modules/typescript/lib/lib.es2015.collection.d.d.ts"
63
+ },
64
+ "./node_modules/typescript/lib/lib.es2015.core.d.ts": {
65
+ "import": "./node_modules/typescript/lib/lib.es2015.core.d.js",
66
+ "types": "./node_modules/typescript/lib/lib.es2015.core.d.d.ts"
67
+ },
68
+ "./node_modules/typescript/lib/lib.es2015.d.ts": {
69
+ "import": "./node_modules/typescript/lib/lib.es2015.d.js",
70
+ "types": "./node_modules/typescript/lib/lib.es2015.d.d.ts"
71
+ },
72
+ "./node_modules/typescript/lib/lib.es2015.generator.d.ts": {
73
+ "import": "./node_modules/typescript/lib/lib.es2015.generator.d.js",
74
+ "types": "./node_modules/typescript/lib/lib.es2015.generator.d.d.ts"
75
+ },
76
+ "./node_modules/typescript/lib/lib.es2015.iterable.d.ts": {
77
+ "import": "./node_modules/typescript/lib/lib.es2015.iterable.d.js",
78
+ "types": "./node_modules/typescript/lib/lib.es2015.iterable.d.d.ts"
79
+ },
80
+ "./node_modules/typescript/lib/lib.es2015.promise.d.ts": {
81
+ "import": "./node_modules/typescript/lib/lib.es2015.promise.d.js",
82
+ "types": "./node_modules/typescript/lib/lib.es2015.promise.d.d.ts"
83
+ },
84
+ "./node_modules/typescript/lib/lib.es2015.proxy.d.ts": {
85
+ "import": "./node_modules/typescript/lib/lib.es2015.proxy.d.js",
86
+ "types": "./node_modules/typescript/lib/lib.es2015.proxy.d.d.ts"
87
+ },
88
+ "./node_modules/typescript/lib/lib.es2015.reflect.d.ts": {
89
+ "import": "./node_modules/typescript/lib/lib.es2015.reflect.d.js",
90
+ "types": "./node_modules/typescript/lib/lib.es2015.reflect.d.d.ts"
91
+ },
92
+ "./node_modules/typescript/lib/lib.es2015.symbol.d.ts": {
93
+ "import": "./node_modules/typescript/lib/lib.es2015.symbol.d.js",
94
+ "types": "./node_modules/typescript/lib/lib.es2015.symbol.d.d.ts"
95
+ },
96
+ "./node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts": {
97
+ "import": "./node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.js",
98
+ "types": "./node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.d.ts"
99
+ },
100
+ "./node_modules/typescript/lib/lib.es2016.array.include.d.ts": {
101
+ "import": "./node_modules/typescript/lib/lib.es2016.array.include.d.js",
102
+ "types": "./node_modules/typescript/lib/lib.es2016.array.include.d.d.ts"
103
+ },
104
+ "./node_modules/typescript/lib/lib.es2016.d.ts": {
105
+ "import": "./node_modules/typescript/lib/lib.es2016.d.js",
106
+ "types": "./node_modules/typescript/lib/lib.es2016.d.d.ts"
107
+ },
108
+ "./node_modules/typescript/lib/lib.es2016.full.d.ts": {
109
+ "import": "./node_modules/typescript/lib/lib.es2016.full.d.js",
110
+ "types": "./node_modules/typescript/lib/lib.es2016.full.d.d.ts"
111
+ },
112
+ "./node_modules/typescript/lib/lib.es2016.intl.d.ts": {
113
+ "import": "./node_modules/typescript/lib/lib.es2016.intl.d.js",
114
+ "types": "./node_modules/typescript/lib/lib.es2016.intl.d.d.ts"
115
+ },
116
+ "./node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts": {
117
+ "import": "./node_modules/typescript/lib/lib.es2017.arraybuffer.d.js",
118
+ "types": "./node_modules/typescript/lib/lib.es2017.arraybuffer.d.d.ts"
119
+ },
120
+ "./node_modules/typescript/lib/lib.es2017.d.ts": {
121
+ "import": "./node_modules/typescript/lib/lib.es2017.d.js",
122
+ "types": "./node_modules/typescript/lib/lib.es2017.d.d.ts"
123
+ },
124
+ "./node_modules/typescript/lib/lib.es2017.date.d.ts": {
125
+ "import": "./node_modules/typescript/lib/lib.es2017.date.d.js",
126
+ "types": "./node_modules/typescript/lib/lib.es2017.date.d.d.ts"
127
+ },
128
+ "./node_modules/typescript/lib/lib.es2017.full.d.ts": {
129
+ "import": "./node_modules/typescript/lib/lib.es2017.full.d.js",
130
+ "types": "./node_modules/typescript/lib/lib.es2017.full.d.d.ts"
131
+ },
132
+ "./node_modules/typescript/lib/lib.es2017.intl.d.ts": {
133
+ "import": "./node_modules/typescript/lib/lib.es2017.intl.d.js",
134
+ "types": "./node_modules/typescript/lib/lib.es2017.intl.d.d.ts"
135
+ },
136
+ "./node_modules/typescript/lib/lib.es2017.object.d.ts": {
137
+ "import": "./node_modules/typescript/lib/lib.es2017.object.d.js",
138
+ "types": "./node_modules/typescript/lib/lib.es2017.object.d.d.ts"
139
+ },
140
+ "./node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts": {
141
+ "import": "./node_modules/typescript/lib/lib.es2017.sharedmemory.d.js",
142
+ "types": "./node_modules/typescript/lib/lib.es2017.sharedmemory.d.d.ts"
143
+ },
144
+ "./node_modules/typescript/lib/lib.es2017.string.d.ts": {
145
+ "import": "./node_modules/typescript/lib/lib.es2017.string.d.js",
146
+ "types": "./node_modules/typescript/lib/lib.es2017.string.d.d.ts"
147
+ },
148
+ "./node_modules/typescript/lib/lib.es2017.typedarrays.d.ts": {
149
+ "import": "./node_modules/typescript/lib/lib.es2017.typedarrays.d.js",
150
+ "types": "./node_modules/typescript/lib/lib.es2017.typedarrays.d.d.ts"
151
+ },
152
+ "./node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts": {
153
+ "import": "./node_modules/typescript/lib/lib.es2018.asyncgenerator.d.js",
154
+ "types": "./node_modules/typescript/lib/lib.es2018.asyncgenerator.d.d.ts"
155
+ },
156
+ "./node_modules/typescript/lib/lib.es2018.asynciterable.d.ts": {
157
+ "import": "./node_modules/typescript/lib/lib.es2018.asynciterable.d.js",
158
+ "types": "./node_modules/typescript/lib/lib.es2018.asynciterable.d.d.ts"
159
+ },
160
+ "./node_modules/typescript/lib/lib.es2018.d.ts": {
161
+ "import": "./node_modules/typescript/lib/lib.es2018.d.js",
162
+ "types": "./node_modules/typescript/lib/lib.es2018.d.d.ts"
163
+ },
164
+ "./node_modules/typescript/lib/lib.es2018.full.d.ts": {
165
+ "import": "./node_modules/typescript/lib/lib.es2018.full.d.js",
166
+ "types": "./node_modules/typescript/lib/lib.es2018.full.d.d.ts"
167
+ },
168
+ "./node_modules/typescript/lib/lib.es2018.intl.d.ts": {
169
+ "import": "./node_modules/typescript/lib/lib.es2018.intl.d.js",
170
+ "types": "./node_modules/typescript/lib/lib.es2018.intl.d.d.ts"
171
+ },
172
+ "./node_modules/typescript/lib/lib.es2018.promise.d.ts": {
173
+ "import": "./node_modules/typescript/lib/lib.es2018.promise.d.js",
174
+ "types": "./node_modules/typescript/lib/lib.es2018.promise.d.d.ts"
175
+ },
176
+ "./node_modules/typescript/lib/lib.es2018.regexp.d.ts": {
177
+ "import": "./node_modules/typescript/lib/lib.es2018.regexp.d.js",
178
+ "types": "./node_modules/typescript/lib/lib.es2018.regexp.d.d.ts"
179
+ },
180
+ "./node_modules/typescript/lib/lib.es2019.array.d.ts": {
181
+ "import": "./node_modules/typescript/lib/lib.es2019.array.d.js",
182
+ "types": "./node_modules/typescript/lib/lib.es2019.array.d.d.ts"
183
+ },
184
+ "./node_modules/typescript/lib/lib.es2019.d.ts": {
185
+ "import": "./node_modules/typescript/lib/lib.es2019.d.js",
186
+ "types": "./node_modules/typescript/lib/lib.es2019.d.d.ts"
187
+ },
188
+ "./node_modules/typescript/lib/lib.es2019.full.d.ts": {
189
+ "import": "./node_modules/typescript/lib/lib.es2019.full.d.js",
190
+ "types": "./node_modules/typescript/lib/lib.es2019.full.d.d.ts"
191
+ },
192
+ "./node_modules/typescript/lib/lib.es2019.intl.d.ts": {
193
+ "import": "./node_modules/typescript/lib/lib.es2019.intl.d.js",
194
+ "types": "./node_modules/typescript/lib/lib.es2019.intl.d.d.ts"
195
+ },
196
+ "./node_modules/typescript/lib/lib.es2019.object.d.ts": {
197
+ "import": "./node_modules/typescript/lib/lib.es2019.object.d.js",
198
+ "types": "./node_modules/typescript/lib/lib.es2019.object.d.d.ts"
199
+ },
200
+ "./node_modules/typescript/lib/lib.es2019.string.d.ts": {
201
+ "import": "./node_modules/typescript/lib/lib.es2019.string.d.js",
202
+ "types": "./node_modules/typescript/lib/lib.es2019.string.d.d.ts"
203
+ },
204
+ "./node_modules/typescript/lib/lib.es2019.symbol.d.ts": {
205
+ "import": "./node_modules/typescript/lib/lib.es2019.symbol.d.js",
206
+ "types": "./node_modules/typescript/lib/lib.es2019.symbol.d.d.ts"
207
+ },
208
+ "./node_modules/typescript/lib/lib.es2020.bigint.d.ts": {
209
+ "import": "./node_modules/typescript/lib/lib.es2020.bigint.d.js",
210
+ "types": "./node_modules/typescript/lib/lib.es2020.bigint.d.d.ts"
211
+ },
212
+ "./node_modules/typescript/lib/lib.es2020.d.ts": {
213
+ "import": "./node_modules/typescript/lib/lib.es2020.d.js",
214
+ "types": "./node_modules/typescript/lib/lib.es2020.d.d.ts"
215
+ },
216
+ "./node_modules/typescript/lib/lib.es2020.date.d.ts": {
217
+ "import": "./node_modules/typescript/lib/lib.es2020.date.d.js",
218
+ "types": "./node_modules/typescript/lib/lib.es2020.date.d.d.ts"
219
+ },
220
+ "./node_modules/typescript/lib/lib.es2020.full.d.ts": {
221
+ "import": "./node_modules/typescript/lib/lib.es2020.full.d.js",
222
+ "types": "./node_modules/typescript/lib/lib.es2020.full.d.d.ts"
223
+ },
224
+ "./node_modules/typescript/lib/lib.es2020.intl.d.ts": {
225
+ "import": "./node_modules/typescript/lib/lib.es2020.intl.d.js",
226
+ "types": "./node_modules/typescript/lib/lib.es2020.intl.d.d.ts"
227
+ },
228
+ "./node_modules/typescript/lib/lib.es2020.number.d.ts": {
229
+ "import": "./node_modules/typescript/lib/lib.es2020.number.d.js",
230
+ "types": "./node_modules/typescript/lib/lib.es2020.number.d.d.ts"
231
+ },
232
+ "./node_modules/typescript/lib/lib.es2020.promise.d.ts": {
233
+ "import": "./node_modules/typescript/lib/lib.es2020.promise.d.js",
234
+ "types": "./node_modules/typescript/lib/lib.es2020.promise.d.d.ts"
235
+ },
236
+ "./node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts": {
237
+ "import": "./node_modules/typescript/lib/lib.es2020.sharedmemory.d.js",
238
+ "types": "./node_modules/typescript/lib/lib.es2020.sharedmemory.d.d.ts"
239
+ },
240
+ "./node_modules/typescript/lib/lib.es2020.string.d.ts": {
241
+ "import": "./node_modules/typescript/lib/lib.es2020.string.d.js",
242
+ "types": "./node_modules/typescript/lib/lib.es2020.string.d.d.ts"
243
+ },
244
+ "./node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts": {
245
+ "import": "./node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.js",
246
+ "types": "./node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.d.ts"
247
+ },
248
+ "./node_modules/typescript/lib/lib.es2021.d.ts": {
249
+ "import": "./node_modules/typescript/lib/lib.es2021.d.js",
250
+ "types": "./node_modules/typescript/lib/lib.es2021.d.d.ts"
251
+ },
252
+ "./node_modules/typescript/lib/lib.es2021.full.d.ts": {
253
+ "import": "./node_modules/typescript/lib/lib.es2021.full.d.js",
254
+ "types": "./node_modules/typescript/lib/lib.es2021.full.d.d.ts"
255
+ },
256
+ "./node_modules/typescript/lib/lib.es2021.intl.d.ts": {
257
+ "import": "./node_modules/typescript/lib/lib.es2021.intl.d.js",
258
+ "types": "./node_modules/typescript/lib/lib.es2021.intl.d.d.ts"
259
+ },
260
+ "./node_modules/typescript/lib/lib.es2021.promise.d.ts": {
261
+ "import": "./node_modules/typescript/lib/lib.es2021.promise.d.js",
262
+ "types": "./node_modules/typescript/lib/lib.es2021.promise.d.d.ts"
263
+ },
264
+ "./node_modules/typescript/lib/lib.es2021.string.d.ts": {
265
+ "import": "./node_modules/typescript/lib/lib.es2021.string.d.js",
266
+ "types": "./node_modules/typescript/lib/lib.es2021.string.d.d.ts"
267
+ },
268
+ "./node_modules/typescript/lib/lib.es2021.weakref.d.ts": {
269
+ "import": "./node_modules/typescript/lib/lib.es2021.weakref.d.js",
270
+ "types": "./node_modules/typescript/lib/lib.es2021.weakref.d.d.ts"
271
+ },
272
+ "./node_modules/typescript/lib/lib.es2022.array.d.ts": {
273
+ "import": "./node_modules/typescript/lib/lib.es2022.array.d.js",
274
+ "types": "./node_modules/typescript/lib/lib.es2022.array.d.d.ts"
275
+ },
276
+ "./node_modules/typescript/lib/lib.es2022.d.ts": {
277
+ "import": "./node_modules/typescript/lib/lib.es2022.d.js",
278
+ "types": "./node_modules/typescript/lib/lib.es2022.d.d.ts"
279
+ },
280
+ "./node_modules/typescript/lib/lib.es2022.error.d.ts": {
281
+ "import": "./node_modules/typescript/lib/lib.es2022.error.d.js",
282
+ "types": "./node_modules/typescript/lib/lib.es2022.error.d.d.ts"
283
+ },
284
+ "./node_modules/typescript/lib/lib.es2022.full.d.ts": {
285
+ "import": "./node_modules/typescript/lib/lib.es2022.full.d.js",
286
+ "types": "./node_modules/typescript/lib/lib.es2022.full.d.d.ts"
287
+ },
288
+ "./node_modules/typescript/lib/lib.es2022.intl.d.ts": {
289
+ "import": "./node_modules/typescript/lib/lib.es2022.intl.d.js",
290
+ "types": "./node_modules/typescript/lib/lib.es2022.intl.d.d.ts"
291
+ },
292
+ "./node_modules/typescript/lib/lib.es2022.object.d.ts": {
293
+ "import": "./node_modules/typescript/lib/lib.es2022.object.d.js",
294
+ "types": "./node_modules/typescript/lib/lib.es2022.object.d.d.ts"
295
+ },
296
+ "./node_modules/typescript/lib/lib.es2022.regexp.d.ts": {
297
+ "import": "./node_modules/typescript/lib/lib.es2022.regexp.d.js",
298
+ "types": "./node_modules/typescript/lib/lib.es2022.regexp.d.d.ts"
299
+ },
300
+ "./node_modules/typescript/lib/lib.es2022.string.d.ts": {
301
+ "import": "./node_modules/typescript/lib/lib.es2022.string.d.js",
302
+ "types": "./node_modules/typescript/lib/lib.es2022.string.d.d.ts"
303
+ },
304
+ "./node_modules/typescript/lib/lib.es2023.array.d.ts": {
305
+ "import": "./node_modules/typescript/lib/lib.es2023.array.d.js",
306
+ "types": "./node_modules/typescript/lib/lib.es2023.array.d.d.ts"
307
+ },
308
+ "./node_modules/typescript/lib/lib.es2023.collection.d.ts": {
309
+ "import": "./node_modules/typescript/lib/lib.es2023.collection.d.js",
310
+ "types": "./node_modules/typescript/lib/lib.es2023.collection.d.d.ts"
311
+ },
312
+ "./node_modules/typescript/lib/lib.es2023.d.ts": {
313
+ "import": "./node_modules/typescript/lib/lib.es2023.d.js",
314
+ "types": "./node_modules/typescript/lib/lib.es2023.d.d.ts"
315
+ },
316
+ "./node_modules/typescript/lib/lib.es2023.full.d.ts": {
317
+ "import": "./node_modules/typescript/lib/lib.es2023.full.d.js",
318
+ "types": "./node_modules/typescript/lib/lib.es2023.full.d.d.ts"
319
+ },
320
+ "./node_modules/typescript/lib/lib.es2023.intl.d.ts": {
321
+ "import": "./node_modules/typescript/lib/lib.es2023.intl.d.js",
322
+ "types": "./node_modules/typescript/lib/lib.es2023.intl.d.d.ts"
323
+ },
324
+ "./node_modules/typescript/lib/lib.es2024.arraybuffer.d.ts": {
325
+ "import": "./node_modules/typescript/lib/lib.es2024.arraybuffer.d.js",
326
+ "types": "./node_modules/typescript/lib/lib.es2024.arraybuffer.d.d.ts"
327
+ },
328
+ "./node_modules/typescript/lib/lib.es2024.collection.d.ts": {
329
+ "import": "./node_modules/typescript/lib/lib.es2024.collection.d.js",
330
+ "types": "./node_modules/typescript/lib/lib.es2024.collection.d.d.ts"
331
+ },
332
+ "./node_modules/typescript/lib/lib.es2024.d.ts": {
333
+ "import": "./node_modules/typescript/lib/lib.es2024.d.js",
334
+ "types": "./node_modules/typescript/lib/lib.es2024.d.d.ts"
335
+ },
336
+ "./node_modules/typescript/lib/lib.es2024.full.d.ts": {
337
+ "import": "./node_modules/typescript/lib/lib.es2024.full.d.js",
338
+ "types": "./node_modules/typescript/lib/lib.es2024.full.d.d.ts"
339
+ },
340
+ "./node_modules/typescript/lib/lib.es2024.object.d.ts": {
341
+ "import": "./node_modules/typescript/lib/lib.es2024.object.d.js",
342
+ "types": "./node_modules/typescript/lib/lib.es2024.object.d.d.ts"
343
+ },
344
+ "./node_modules/typescript/lib/lib.es2024.promise.d.ts": {
345
+ "import": "./node_modules/typescript/lib/lib.es2024.promise.d.js",
346
+ "types": "./node_modules/typescript/lib/lib.es2024.promise.d.d.ts"
347
+ },
348
+ "./node_modules/typescript/lib/lib.es2024.regexp.d.ts": {
349
+ "import": "./node_modules/typescript/lib/lib.es2024.regexp.d.js",
350
+ "types": "./node_modules/typescript/lib/lib.es2024.regexp.d.d.ts"
351
+ },
352
+ "./node_modules/typescript/lib/lib.es2024.sharedmemory.d.ts": {
353
+ "import": "./node_modules/typescript/lib/lib.es2024.sharedmemory.d.js",
354
+ "types": "./node_modules/typescript/lib/lib.es2024.sharedmemory.d.d.ts"
355
+ },
356
+ "./node_modules/typescript/lib/lib.es2024.string.d.ts": {
357
+ "import": "./node_modules/typescript/lib/lib.es2024.string.d.js",
358
+ "types": "./node_modules/typescript/lib/lib.es2024.string.d.d.ts"
359
+ },
360
+ "./node_modules/typescript/lib/lib.es5.d.ts": {
361
+ "import": "./node_modules/typescript/lib/lib.es5.d.js",
362
+ "types": "./node_modules/typescript/lib/lib.es5.d.d.ts"
363
+ },
364
+ "./node_modules/typescript/lib/lib.es6.d.ts": {
365
+ "import": "./node_modules/typescript/lib/lib.es6.d.js",
366
+ "types": "./node_modules/typescript/lib/lib.es6.d.d.ts"
367
+ },
368
+ "./node_modules/typescript/lib/lib.esnext.array.d.ts": {
369
+ "import": "./node_modules/typescript/lib/lib.esnext.array.d.js",
370
+ "types": "./node_modules/typescript/lib/lib.esnext.array.d.d.ts"
371
+ },
372
+ "./node_modules/typescript/lib/lib.esnext.collection.d.ts": {
373
+ "import": "./node_modules/typescript/lib/lib.esnext.collection.d.js",
374
+ "types": "./node_modules/typescript/lib/lib.esnext.collection.d.d.ts"
375
+ },
376
+ "./node_modules/typescript/lib/lib.esnext.d.ts": {
377
+ "import": "./node_modules/typescript/lib/lib.esnext.d.js",
378
+ "types": "./node_modules/typescript/lib/lib.esnext.d.d.ts"
379
+ },
380
+ "./node_modules/typescript/lib/lib.esnext.decorators.d.ts": {
381
+ "import": "./node_modules/typescript/lib/lib.esnext.decorators.d.js",
382
+ "types": "./node_modules/typescript/lib/lib.esnext.decorators.d.d.ts"
383
+ },
384
+ "./node_modules/typescript/lib/lib.esnext.disposable.d.ts": {
385
+ "import": "./node_modules/typescript/lib/lib.esnext.disposable.d.js",
386
+ "types": "./node_modules/typescript/lib/lib.esnext.disposable.d.d.ts"
387
+ },
388
+ "./node_modules/typescript/lib/lib.esnext.error.d.ts": {
389
+ "import": "./node_modules/typescript/lib/lib.esnext.error.d.js",
390
+ "types": "./node_modules/typescript/lib/lib.esnext.error.d.d.ts"
391
+ },
392
+ "./node_modules/typescript/lib/lib.esnext.float16.d.ts": {
393
+ "import": "./node_modules/typescript/lib/lib.esnext.float16.d.js",
394
+ "types": "./node_modules/typescript/lib/lib.esnext.float16.d.d.ts"
395
+ },
396
+ "./node_modules/typescript/lib/lib.esnext.full.d.ts": {
397
+ "import": "./node_modules/typescript/lib/lib.esnext.full.d.js",
398
+ "types": "./node_modules/typescript/lib/lib.esnext.full.d.d.ts"
399
+ },
400
+ "./node_modules/typescript/lib/lib.esnext.intl.d.ts": {
401
+ "import": "./node_modules/typescript/lib/lib.esnext.intl.d.js",
402
+ "types": "./node_modules/typescript/lib/lib.esnext.intl.d.d.ts"
403
+ },
404
+ "./node_modules/typescript/lib/lib.esnext.iterator.d.ts": {
405
+ "import": "./node_modules/typescript/lib/lib.esnext.iterator.d.js",
406
+ "types": "./node_modules/typescript/lib/lib.esnext.iterator.d.d.ts"
407
+ },
408
+ "./node_modules/typescript/lib/lib.esnext.promise.d.ts": {
409
+ "import": "./node_modules/typescript/lib/lib.esnext.promise.d.js",
410
+ "types": "./node_modules/typescript/lib/lib.esnext.promise.d.d.ts"
411
+ },
412
+ "./node_modules/typescript/lib/lib.esnext.sharedmemory.d.ts": {
413
+ "import": "./node_modules/typescript/lib/lib.esnext.sharedmemory.d.js",
414
+ "types": "./node_modules/typescript/lib/lib.esnext.sharedmemory.d.d.ts"
415
+ },
416
+ "./node_modules/typescript/lib/lib.scripthost.d.ts": {
417
+ "import": "./node_modules/typescript/lib/lib.scripthost.d.js",
418
+ "types": "./node_modules/typescript/lib/lib.scripthost.d.d.ts"
419
+ },
420
+ "./node_modules/typescript/lib/lib.webworker.asynciterable.d.ts": {
421
+ "import": "./node_modules/typescript/lib/lib.webworker.asynciterable.d.js",
422
+ "types": "./node_modules/typescript/lib/lib.webworker.asynciterable.d.d.ts"
423
+ },
424
+ "./node_modules/typescript/lib/lib.webworker.d.ts": {
425
+ "import": "./node_modules/typescript/lib/lib.webworker.d.js",
426
+ "types": "./node_modules/typescript/lib/lib.webworker.d.d.ts"
427
+ },
428
+ "./node_modules/typescript/lib/lib.webworker.importscripts.d.ts": {
429
+ "import": "./node_modules/typescript/lib/lib.webworker.importscripts.d.js",
430
+ "types": "./node_modules/typescript/lib/lib.webworker.importscripts.d.d.ts"
431
+ },
432
+ "./node_modules/typescript/lib/lib.webworker.iterable.d.ts": {
433
+ "import": "./node_modules/typescript/lib/lib.webworker.iterable.d.js",
434
+ "types": "./node_modules/typescript/lib/lib.webworker.iterable.d.d.ts"
435
+ },
436
+ "./node_modules/typescript/lib/tsserverlibrary.d.ts": {
437
+ "import": "./node_modules/typescript/lib/tsserverlibrary.d.js",
438
+ "types": "./node_modules/typescript/lib/tsserverlibrary.d.d.ts"
439
+ },
440
+ "./node_modules/typescript/lib/typescript.d.ts": {
441
+ "import": "./node_modules/typescript/lib/typescript.d.js",
442
+ "types": "./node_modules/typescript/lib/typescript.d.d.ts"
443
+ },
444
+ "./sendgrid.ts": {
445
+ "import": "./sendgrid.js",
446
+ "types": "./sendgrid.d.ts"
447
+ },
448
+ "./service.test.ts": {
449
+ "import": "./service.test.js",
450
+ "types": "./service.test.d.ts"
451
+ },
452
+ "./service.ts": {
453
+ "import": "./service.js",
454
+ "types": "./service.d.ts"
455
+ },
456
+ "./type.ts": {
457
+ "import": "./type.js",
458
+ "types": "./type.d.ts"
459
+ }
460
+ }
461
+ }
package/sendgrid.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ import type { EmailBackend, SendGridConfig } from './type.ts';
2
+ export declare const isVerificationApproved: ({ code, attempt, }: {
3
+ code: string | undefined;
4
+ attempt: string;
5
+ }) => boolean;
6
+ export declare const createSendGridBackend: ({ apiKey, verificationFrom, verificationTemplateId, }: SendGridConfig) => EmailBackend;
package/sendgrid.js ADDED
@@ -0,0 +1,26 @@
1
+ import sendgridMail from '@sendgrid/mail';
2
+ import { from, map, of } from 'rxjs';
3
+ // SendGrid has no verification endpoint of its own: it delivers the code, and whoever issued it
4
+ // decides whether an attempt matches. An attempt against a code that was never issued is refused
5
+ // rather than treated as a match.
6
+ export const isVerificationApproved = ({ code, attempt, }) => code !== undefined && code !== '' && code === attempt;
7
+ export const createSendGridBackend = ({ apiKey, verificationFrom, verificationTemplateId, }) => {
8
+ sendgridMail.setApiKey(apiKey);
9
+ return {
10
+ send: async ({ to, from: sender, subject, body, isHtml }) => {
11
+ await sendgridMail.send({
12
+ to,
13
+ from: sender,
14
+ subject,
15
+ ...(isHtml ? { html: body } : { text: body }),
16
+ });
17
+ },
18
+ requestVerification: ({ to, code }) => from(sendgridMail.send({
19
+ to,
20
+ from: verificationFrom,
21
+ templateId: verificationTemplateId,
22
+ dynamicTemplateData: { verificationCode: code },
23
+ })).pipe(map(() => undefined)),
24
+ verify: ({ code, attempt }) => of({ status: isVerificationApproved({ code, attempt }) ? 'verified' : 'failed' }),
25
+ };
26
+ };
package/service.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ import type { EmailBackend, Sender } from './type.ts';
2
+ export declare const createSender: (emailBackend: EmailBackend) => Sender;
package/service.js ADDED
@@ -0,0 +1,15 @@
1
+ import { createCausedError } from '@dungarees/core/error.ts';
2
+ import { catchError, throwError } from 'rxjs';
3
+ const failWith = (message) => (source$) => source$.pipe(catchError((cause) => throwError(() => createCausedError({ message, cause }))));
4
+ export const createSender = (emailBackend) => ({
5
+ send: async (email) => {
6
+ try {
7
+ await emailBackend.send(email);
8
+ }
9
+ catch (cause) {
10
+ throw createCausedError({ message: 'Email sending backend failed', cause });
11
+ }
12
+ },
13
+ requestVerification: (request) => emailBackend.requestVerification(request).pipe(failWith('Email verification request failed')),
14
+ verify: (attempt) => emailBackend.verify(attempt).pipe(failWith('Email verification failed')),
15
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,79 @@
1
+ import { createFakeEmailBackend } from './fake.js';
2
+ import { isVerificationApproved } from './sendgrid.js';
3
+ import { createSender } from './service.js';
4
+ import { addErrorMethodsToFake } from '@dungarees/core/fake.ts';
5
+ import { firstValueFrom } from 'rxjs';
6
+ import { expect, test } from 'vitest';
7
+ const TEST_EMAIL = {
8
+ to: 'to@example.org',
9
+ from: 'from@example.org',
10
+ subject: 'subject',
11
+ body: 'body',
12
+ isHtml: false,
13
+ };
14
+ const failingBackend = (configs) => {
15
+ const { backend } = createFakeEmailBackend();
16
+ return addErrorMethodsToFake(() => backend)(configs);
17
+ };
18
+ test('send hands the email to the backend', async () => {
19
+ const { received, backend } = createFakeEmailBackend();
20
+ const sender = createSender(backend);
21
+ await sender.send(TEST_EMAIL);
22
+ expect(received).toEqual([TEST_EMAIL]);
23
+ });
24
+ test('send reports a failing backend as a sending failure', async () => {
25
+ const cause = new Error('Fake Backend Failed');
26
+ const sender = createSender(failingBackend({ send: { type: 'async', error: cause } }));
27
+ await expect(sender.send(TEST_EMAIL)).rejects.toThrow('Email sending backend failed: Fake Backend Failed');
28
+ });
29
+ test('send keeps the backend failure as the cause', async () => {
30
+ const cause = new Error('Fake Backend Failed');
31
+ const sender = createSender(failingBackend({ send: { type: 'async', error: cause } }));
32
+ await expect(sender.send(TEST_EMAIL)).rejects.toHaveProperty('cause', cause);
33
+ });
34
+ test('requestVerification reaches the backend with the address and code', async () => {
35
+ const { verificationRequests, backend } = createFakeEmailBackend();
36
+ const sender = createSender(backend);
37
+ await firstValueFrom(sender.requestVerification({ to: 'to@example.org', code: '123456' }));
38
+ expect(verificationRequests).toEqual([{ to: 'to@example.org', code: '123456' }]);
39
+ });
40
+ test('requestVerification reports a failing backend as a request failure', async () => {
41
+ const cause = new Error('Fake Backend Failed');
42
+ const sender = createSender(failingBackend({ requestVerification: { type: 'observable', error: cause } }));
43
+ await expect(firstValueFrom(sender.requestVerification({ to: 'to@example.org' }))).rejects.toThrow('Email verification request failed: Fake Backend Failed');
44
+ });
45
+ test('verify hands the attempt to the backend and returns its verdict', async () => {
46
+ const { backend } = createFakeEmailBackend();
47
+ const sender = createSender(backend);
48
+ const result = await firstValueFrom(sender.verify({ to: 'to@example.org', code: '123456', attempt: '123456' }));
49
+ expect(result).toEqual({ status: 'verified' });
50
+ });
51
+ test('verify returns a failed verdict for the wrong attempt', async () => {
52
+ const { backend } = createFakeEmailBackend();
53
+ const sender = createSender(backend);
54
+ const result = await firstValueFrom(sender.verify({ to: 'to@example.org', code: '123456', attempt: '000000' }));
55
+ expect(result).toEqual({ status: 'failed' });
56
+ });
57
+ test('verify reports a failing backend as a verification failure', async () => {
58
+ const cause = new Error('Fake Backend Failed');
59
+ const sender = createSender(failingBackend({ verify: { type: 'observable', error: cause } }));
60
+ await expect(firstValueFrom(sender.verify({ to: 'to@example.org', code: '1', attempt: '1' }))).rejects.toThrow('Email verification failed: Fake Backend Failed');
61
+ });
62
+ test('the fake backend records the verification email it would have sent', async () => {
63
+ const { received, backend } = createFakeEmailBackend();
64
+ const sender = createSender(backend);
65
+ await firstValueFrom(sender.requestVerification({ to: 'to@example.org', code: '123456' }));
66
+ expect(received[0]?.body).toContain('123456');
67
+ });
68
+ test('a verification is approved when the attempt matches the code', () => {
69
+ expect(isVerificationApproved({ code: '123456', attempt: '123456' })).toBe(true);
70
+ });
71
+ test('a verification is refused when the attempt does not match the code', () => {
72
+ expect(isVerificationApproved({ code: '123456', attempt: '654321' })).toBe(false);
73
+ });
74
+ test('a verification is refused when no code was ever issued', () => {
75
+ expect(isVerificationApproved({ code: undefined, attempt: '123456' })).toBe(false);
76
+ });
77
+ test('a verification is refused for an empty attempt against an empty code', () => {
78
+ expect(isVerificationApproved({ code: '', attempt: '' })).toBe(false);
79
+ });
package/type.d.ts ADDED
@@ -0,0 +1,32 @@
1
+ import type { EmailAddress } from './email-address.ts';
2
+ import type { Observable } from 'rxjs';
3
+ export type Email = {
4
+ to: EmailAddress;
5
+ from: EmailAddress;
6
+ subject: string;
7
+ body: string;
8
+ isHtml: boolean;
9
+ };
10
+ export type VerificationRequest = {
11
+ to: EmailAddress;
12
+ code?: string;
13
+ };
14
+ export type VerificationAttempt = {
15
+ to: EmailAddress;
16
+ code: string;
17
+ attempt: string;
18
+ };
19
+ export type VerificationResult = {
20
+ status: 'verified' | 'failed';
21
+ };
22
+ export type EmailBackend = {
23
+ send: (email: Email) => Promise<void>;
24
+ requestVerification: (request: VerificationRequest) => Observable<void>;
25
+ verify: (attempt: VerificationAttempt) => Observable<VerificationResult>;
26
+ };
27
+ export type Sender = EmailBackend;
28
+ export type SendGridConfig = {
29
+ apiKey: string;
30
+ verificationFrom: EmailAddress;
31
+ verificationTemplateId: string;
32
+ };
package/type.js ADDED
@@ -0,0 +1 @@
1
+ export {};