svelte-guardian
Version:
Batteries included authentication for SvelteKit applications.
103 lines (102 loc) • 4.84 kB
JavaScript
import { EmailOptions, EmailProvider } from '../types';
import { html, text } from '../utils';
export class AWSSESProvider {
accessKeyId = process.env.AWS_ACCESS_KEY_ID || '';
secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY || '';
region = process.env.AWS_REGION || 'us-east-1';
from = process.env.EMAIL_FROM || 'Acme <noreply@yourdomain.com>';
createSignatureKey(date, region, service) {
const kDate = this.hmac('AWS4' + this.secretAccessKey, date);
const kRegion = this.hmac(kDate, region);
const kService = this.hmac(kRegion, service);
const kSigning = this.hmac(kService, 'aws4_request');
return kSigning;
}
hmac(key, data) {
return crypto.createHmac('sha256', key).update(data).digest();
}
createCanonicalRequest(method, canonicalUri, canonicalQuerystring, canonicalHeaders, signedHeaders, payloadHash) {
return `${method}\n${canonicalUri}\n${canonicalQuerystring}\n${canonicalHeaders}\n${signedHeaders}\n${payloadHash}`;
}
createStringToSign(date, credentialScope, canonicalRequest) {
const hashedCanonicalRequest = crypto
.createHash('sha256')
.update(canonicalRequest)
.digest('hex');
return `AWS4-HMAC-SHA256\n${date}\n${credentialScope}\n${hashedCanonicalRequest}`;
}
createSignature(stringToSign, signingKey) {
return crypto.createHmac('sha256', signingKey).update(stringToSign).digest('hex');
}
getAuthorizationHeader(date, region, service, signedHeaders, signature) {
const credentialScope = `${date}/${region}/${service}/aws4_request`;
return `AWS4-HMAC-SHA256 Credential=${this.accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
}
async send(options) {
const { to, subject, type, otp, url, theme, customContent } = options;
const { host } = new URL(url || '');
const emailHtml = html({ url, host, theme, otp, type, customContent });
const emailText = text({ url, host, otp, type, customContent });
const service = 'ses';
const method = 'POST';
const canonicalUri = '/v2/email/outbound-emails';
const canonicalQuerystring = '';
const now = new Date();
const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, '');
const dateStamp = amzDate.split('T')[0];
const payload = JSON.stringify({
Content: {
Simple: {
Subject: {
Data: subject,
Charset: 'UTF-8'
},
Body: {
Html: {
Data: emailHtml,
Charset: 'UTF-8'
},
Text: {
Data: emailText,
Charset: 'UTF-8'
}
}
}
},
Destination: {
ToAddresses: [to]
},
FromEmailAddress: this.from
});
const payloadHash = crypto.createHash('sha256').update(payload).digest('hex');
const canonicalHeaders = `content-type:application/json\n` +
`host:email.${this.region}.amazonaws.com\n` +
`x-amz-date:${amzDate}\n`;
const signedHeaders = 'content-type;host;x-amz-date';
const canonicalRequest = this.createCanonicalRequest(method, canonicalUri, canonicalQuerystring, canonicalHeaders, signedHeaders, payloadHash);
const credentialScope = `${dateStamp}/${this.region}/${service}/aws4_request`;
const stringToSign = this.createStringToSign(amzDate, credentialScope, canonicalRequest);
const signingKey = this.createSignatureKey(dateStamp, this.region, service);
const signature = this.createSignature(stringToSign, signingKey);
const authorizationHeader = this.getAuthorizationHeader(amzDate, this.region, service, signedHeaders, signature);
try {
const res = await fetch(`https://email.${this.region}.amazonaws.com${canonicalUri}`, {
method: method,
headers: {
'Content-Type': 'application/json',
'X-Amz-Date': amzDate,
Authorization: authorizationHeader,
'X-Amz-Content-Sha256': payloadHash
},
body: payload
});
if (!res.ok) {
const errorBody = await res.text();
throw new Error(`AWS SES error: ${errorBody}`);
}
}
catch (error) {
throw new Error(`AWS SES send error: ${error instanceof Error ? error.message : JSON.stringify(error)}`);
}
}
}