Repository navigation
Expand file tree
/
Copy pathmessagingService.ts
More file actions
205 lines (166 loc) · 6.28 KB
/
Copy pathmessagingService.ts
File metadata and controls
205 lines (166 loc) · 6.28 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
/*
* Copyright © 2026 Fells Code, LLC
* Licensed under the Apache License, Version 2.0
* See LICENSE file in the project root for full license information
*/
import type { AuthMessagingService, EmailTransport, SmsTransport } from '@seamless-auth/messaging';
import {
createDirectAuthMessagingService,
createDirectEmailTransport,
createDirectSmsTransport,
} from '../config/directMessaging.js';
import { getSystemConfig } from '../config/getSystemConfig.js';
import getLogger from '../utils/logger.js';
import { normalizePhoneNumber } from '../utils/utils.js';
import { DeliveryError } from './deliveryError.js';
const logger = getLogger('messaging');
function shouldBypassDirectMessaging() {
// Not `=== 'development'`. Every other environment gate in the codebase asks whether
// this is production, and testing for one name meant a staging, CI or unset NODE_ENV
// tried to reach a real provider and failed the request that triggered it.
const isProduction = process.env.NODE_ENV === 'production';
const enableInDev = process.env.MESSAGING_ENABLE_IN_DEV === 'true';
return !isProduction && !enableInDev;
}
// Built once and reused. Each construction builds a provider client per channel, so
// doing it per message threw away connection reuse and re-read configuration on every
// OTP. Keyed on app_name, which is the only input, so a config change still takes
// effect on the next send.
let cachedService: { appName: string; service: AuthMessagingService } | null = null;
async function getMessagingService() {
const { app_name } = await getSystemConfig();
if (cachedService?.appName === app_name) {
return cachedService.service;
}
const service = createDirectAuthMessagingService(app_name);
cachedService = { appName: app_name, service };
return service;
}
export const sendOTPEmail = async (to: string, token: string) => {
logger.debug('Sending verification email');
if (shouldBypassDirectMessaging()) {
logger.debug('Skipping direct email delivery in development');
return;
}
try {
const messaging = await getMessagingService();
await messaging.sendOtpEmail({
to,
token,
});
logger.info('Verification email sent');
} catch (error) {
logger.error(`Failed to send verification email ${error}`);
throw new DeliveryError('Failed to send verification email', error);
}
};
export const sendOTPSMS = async (to: string, token: number) => {
logger.debug('Sending verification SMS');
if (shouldBypassDirectMessaging()) {
logger.debug('Skipping direct SMS delivery in development');
return;
}
try {
const messaging = await getMessagingService();
const normalizedPhone = normalizePhoneNumber(to);
if (!normalizedPhone) {
throw new Error('Invalid phone number for direct SMS delivery');
}
await messaging.sendOtpSms({
to: normalizedPhone,
token,
});
} catch (error) {
logger.error(`Failed to send verification SMS ${error}`);
throw new DeliveryError('Failed to send verification SMS', error);
}
};
export const sendMagicLinkEmail = async (to: string, token: string, safeRedirect: string) => {
logger.debug('Sending magic link');
if (shouldBypassDirectMessaging()) {
logger.debug('Skipping direct magic link delivery in development');
return;
}
try {
const messaging = await getMessagingService();
await messaging.sendMagicLinkEmail({
to,
token,
magicLinkUrl: safeRedirect,
});
} catch (error) {
logger.error(`Failed to send magic link email ${error}`);
throw new DeliveryError('Failed to send magic link email', error);
}
};
export const sendEnrollmentInviteEmail = async (to: string, signInUrl: string) => {
logger.debug('Sending enrollment invite');
if (shouldBypassDirectMessaging()) {
logger.debug('Skipping direct enrollment invite delivery in development');
return;
}
try {
const messaging = await getMessagingService();
await messaging.sendEnrollmentInviteEmail({ to, signInUrl });
} catch (error) {
logger.error(`Failed to send enrollment invite email ${error}`);
throw new DeliveryError('Failed to send enrollment invite email', error);
}
};
// The messaging contract has templates only for sign-in messages, so account notices go
// straight to the configured transport with text written here.
let noticeEmailTransport: EmailTransport | null = null;
let noticeSmsTransport: SmsTransport | null = null;
/**
* Whether the API can send an account notice on a channel itself. Outside production a
* send is skipped and reported as sent, so this answers true there too; otherwise the
* channel's transport has to be configured.
*/
export function canSendNoticeDirectly(channel: 'email' | 'phone') {
if (shouldBypassDirectMessaging()) {
return true;
}
try {
if (channel === 'email') {
noticeEmailTransport ??= createDirectEmailTransport();
} else {
noticeSmsTransport ??= createDirectSmsTransport();
}
return true;
} catch (error) {
logger.debug(`No direct transport for account notices on ${channel}: ${error}`);
return false;
}
}
export const sendNoticeEmail = async (to: string, subject: string, text: string) => {
logger.debug('Sending account notice email');
if (shouldBypassDirectMessaging()) {
logger.debug('Skipping direct notice email delivery in development');
return;
}
try {
noticeEmailTransport ??= createDirectEmailTransport();
await noticeEmailTransport.send({ to, subject, text });
} catch (error) {
logger.error(`Failed to send account notice email ${error}`);
throw new DeliveryError('Failed to send account notice email', error);
}
};
export const sendNoticeSms = async (to: string, body: string) => {
logger.debug('Sending account notice SMS');
if (shouldBypassDirectMessaging()) {
logger.debug('Skipping direct notice SMS delivery in development');
return;
}
try {
const normalizedPhone = normalizePhoneNumber(to);
if (!normalizedPhone) {
throw new Error('Invalid phone number for direct SMS delivery');
}
noticeSmsTransport ??= createDirectSmsTransport();
await noticeSmsTransport.send({ to: normalizedPhone, body });
} catch (error) {
logger.error(`Failed to send account notice SMS ${error}`);
throw new DeliveryError('Failed to send account notice SMS', error);
}
};