feat(identity): add resend otp delivery adapter

This commit is contained in:
Jesse_Chen
2026-07-21 17:28:59 +08:00
parent 5da3dd38ff
commit 43213f080a
3 changed files with 198 additions and 0 deletions
@@ -0,0 +1,12 @@
import type {
EmailOtpMessage,
EmailOtpSender,
} from "../contracts.ts";
export class FakeEmailOtpSender implements EmailOtpSender {
readonly messages: EmailOtpMessage[] = [];
async send(message: EmailOtpMessage): Promise<void> {
this.messages.push({ ...message });
}
}
@@ -0,0 +1,73 @@
import type {
EmailOtpMessage,
EmailOtpSender,
EmailOtpType,
} from "../contracts.ts";
const resendEndpoint = "https://api.resend.com/emails";
const safeDeliveryError = "OTP email delivery failed";
const subjectByType: Record<EmailOtpType, string> = {
"sign-in": "Your Jyotisha sign-in code",
"email-verification": "Verify your Jyotisha email",
"forget-password": "Reset your Jyotisha password",
};
function escapeHtml(value: string): string {
return value.replace(/[&<>"']/g, (character) => {
const entities: Record<string, string> = {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#39;",
};
return entities[character];
});
}
export interface ResendEmailOtpSenderOptions {
apiKey: string;
from: string;
fetchImpl?: typeof fetch;
}
export class ResendEmailOtpSender implements EmailOtpSender {
private readonly apiKey: string;
private readonly from: string;
private readonly fetchImpl: typeof fetch;
constructor(options: ResendEmailOtpSenderOptions) {
this.apiKey = options.apiKey;
this.from = options.from;
this.fetchImpl = options.fetchImpl ?? fetch;
}
async send(message: EmailOtpMessage): Promise<void> {
const escapedOtp = escapeHtml(message.otp);
const subject = subjectByType[message.type];
try {
const response = await this.fetchImpl(resendEndpoint, {
method: "POST",
headers: {
Authorization: `Bearer ${this.apiKey}`,
"Content-Type": "application/json",
"Idempotency-Key": message.idempotencyKey,
"User-Agent": "jyotisha-identity/1.0",
},
body: JSON.stringify({
from: this.from,
to: [message.email],
subject,
text: `${subject}: ${message.otp}. This code expires in five minutes.`,
html: `<p>${escapeHtml(subject)}</p><p><strong>${escapedOtp}</strong></p><p>This code expires in five minutes.</p>`,
}),
});
if (!response.ok) throw new Error(safeDeliveryError);
} catch {
throw new Error(safeDeliveryError);
}
}
}