8 Commits

Author SHA1 Message Date
5362ff7599 feat: switch back to meta llama
Some checks failed
Deploy / lint-build-deploy (push) Failing after 1m7s
2026-02-15 10:30:08 +00:00
21b3c3ba83 feat: switch to deepseek llama
Some checks failed
Deploy / lint-build-deploy (push) Failing after 1m52s
2026-02-08 10:22:08 +00:00
44b9793c60 feat: switch to umami
Some checks failed
Deploy / lint-build-deploy (push) Failing after 1m51s
2026-01-31 22:50:00 +01:00
35020f2499 feat: switch to sweego
Some checks failed
Deploy / lint-build-deploy (push) Failing after 1m19s
2026-01-30 18:12:29 +01:00
20b09849bc revert: restore lucide-react icons for emails
Some checks failed
Deploy / lint-build-deploy (push) Failing after 1m33s
2026-01-29 19:42:21 +01:00
dc3850ac4d fix: rename env var
Some checks failed
Deploy / lint-build-deploy (push) Failing after 1m23s
2026-01-29 18:19:00 +01:00
e73262b3b3 chore: update privacy
Some checks failed
Deploy / lint-build-deploy (push) Failing after 2m7s
2026-01-28 19:56:07 +01:00
7299a266f1 fix: correct infrastructure path
All checks were successful
Deploy / lint-build-deploy (push) Successful in 2m10s
2026-01-22 17:44:36 +00:00
36 changed files with 216 additions and 3340 deletions

View File

@@ -1,6 +1,6 @@
ADMIN_EMAIL="" ADMIN_EMAIL=""
CRON_SECRET="" CRON_SECRET=""
HOME_URL="" NEXT_PUBLIC_HOME_URL=""
OVHCLOUD_API_KEY="" OVHCLOUD_API_KEY=""
MAINTENANCE_MODE="0" MAINTENANCE_MODE="0"
NEWS_LIMIT="50" NEWS_LIMIT="50"
@@ -8,12 +8,8 @@ NEWS_TO_USE="10"
NEXT_PUBLIC_BRAND_COUNTRY="" NEXT_PUBLIC_BRAND_COUNTRY=""
NEXT_PUBLIC_BRAND_EMAIL="" NEXT_PUBLIC_BRAND_EMAIL=""
NEXT_PUBLIC_BRAND_NAME="" NEXT_PUBLIC_BRAND_NAME=""
NEXT_PUBLIC_BRAND_OWNER_NAME=""
DATABASE_URL="" DATABASE_URL=""
SWEEGO_API_KEY=""
SWEEGO_FROM=""
SECRET_HASH="" SECRET_HASH=""
EMAIL_HOST="postfix"
EMAIL_PORT="25"
EMAIL_FROM=""
MAIL_DOMAIN=""
MAIL_HOSTNAME=""
DKIM_SELECTOR="mail"

View File

@@ -36,6 +36,6 @@ jobs:
ssh debian@51.210.247.57 << 'EOF' ssh debian@51.210.247.57 << 'EOF'
cd /home/debian/newsletter-hackernews cd /home/debian/newsletter-hackernews
git pull origin main git pull origin main
cd /home/debian/gitea cd /home/debian/infrastructure
docker compose up -d --build newsletter docker compose up -d --build newsletter
EOF EOF

View File

@@ -11,10 +11,14 @@ COPY . .
ARG NEXT_PUBLIC_BRAND_NAME ARG NEXT_PUBLIC_BRAND_NAME
ARG NEXT_PUBLIC_BRAND_EMAIL ARG NEXT_PUBLIC_BRAND_EMAIL
ARG NEXT_PUBLIC_BRAND_COUNTRY ARG NEXT_PUBLIC_BRAND_COUNTRY
ARG NEXT_PUBLIC_HOME_URL
ARG NEXT_PUBLIC_BRAND_OWNER_NAME
ENV NEXT_PUBLIC_BRAND_NAME=$NEXT_PUBLIC_BRAND_NAME ENV NEXT_PUBLIC_BRAND_NAME=$NEXT_PUBLIC_BRAND_NAME
ENV NEXT_PUBLIC_BRAND_EMAIL=$NEXT_PUBLIC_BRAND_EMAIL ENV NEXT_PUBLIC_BRAND_EMAIL=$NEXT_PUBLIC_BRAND_EMAIL
ENV NEXT_PUBLIC_BRAND_COUNTRY=$NEXT_PUBLIC_BRAND_COUNTRY ENV NEXT_PUBLIC_BRAND_COUNTRY=$NEXT_PUBLIC_BRAND_COUNTRY
ENV NEXT_PUBLIC_HOME_URL=$NEXT_PUBLIC_HOME_URL
ENV NEXT_PUBLIC_BRAND_OWNER_NAME=$NEXT_PUBLIC_BRAND_OWNER_NAME
RUN npm run build RUN npm run build

View File

@@ -14,8 +14,8 @@ export const dynamic = 'force-dynamic'; // defaults to force-static
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
try { try {
if (!process.env.RESEND_KEY) { if (!process.env.SWEEGO_API_KEY) {
throw new Error('Resend variables not set'); throw new Error('SWEEGO_API_KEY is not set');
} }
const body = await request.json(); const body = await request.json();
const validation = ConfirmationSchema.safeParse(body); const validation = ConfirmationSchema.safeParse(body);

View File

@@ -1,7 +1,7 @@
import { NewsletterTemplate } from '@components/email/Newsletter'; import { NewsletterTemplate } from '@components/email/Newsletter';
import prisma from '@prisma/prisma'; import prisma from '@prisma/prisma';
import { formatApiResponse } from '@utils/formatApiResponse'; import { formatApiResponse } from '@utils/formatApiResponse';
import { sender } from '@utils/mailer'; import { sender } from '@utils/sweego';
import { import {
INTERNAL_SERVER_ERROR, INTERNAL_SERVER_ERROR,
STATUS_INTERNAL_SERVER_ERROR, STATUS_INTERNAL_SERVER_ERROR,

View File

@@ -1,7 +1,7 @@
import { ConfirmationTemplate } from '@components/email/Confirmation'; import { ConfirmationTemplate } from '@components/email/Confirmation';
import prisma from '@prisma/prisma'; import prisma from '@prisma/prisma';
import { formatApiResponse } from '@utils/formatApiResponse'; import { formatApiResponse } from '@utils/formatApiResponse';
import { sender } from '@utils/mailer'; import { sender } from '@utils/sweego';
import { import {
BAD_REQUEST, BAD_REQUEST,
INTERNAL_SERVER_ERROR, INTERNAL_SERVER_ERROR,

View File

@@ -1,7 +1,7 @@
import { UnsubscribeTemplate } from '@components/email/Unsubscribe'; import { UnsubscribeTemplate } from '@components/email/Unsubscribe';
import prisma from '@prisma/prisma'; import prisma from '@prisma/prisma';
import { formatApiResponse } from '@utils/formatApiResponse'; import { formatApiResponse } from '@utils/formatApiResponse';
import { sender } from '@utils/mailer'; import { sender } from '@utils/sweego';
import { import {
BAD_REQUEST, BAD_REQUEST,
INTERNAL_SERVER_ERROR, INTERNAL_SERVER_ERROR,
@@ -30,13 +30,10 @@ export async function POST(request: NextRequest) {
} }
}); });
if (user && !user.deleted) { if (user) {
await prisma.user.update({ await prisma.user.delete({
where: { where: {
email email
},
data: {
deleted: true
} }
}); });

View File

@@ -86,7 +86,7 @@ const Confirmation = () => {
'@type': 'WebSite', '@type': 'WebSite',
name: 'Hackernews Newsletter', name: 'Hackernews Newsletter',
title: 'Subscription Confirmation', title: 'Subscription Confirmation',
url: `${process.env.HOME_URL}/confirmation` url: `${process.env.NEXT_PUBLIC_HOME_URL}/confirmation`
}; };
return ( return (

View File

@@ -1,8 +1,8 @@
import { Tiles } from '@components/tiles/Tiles'; import { Tiles } from '@components/tiles/Tiles';
import { cn } from '@utils/cn'; import { cn } from '@utils/cn';
import { Analytics } from '@vercel/analytics/react';
import type { Metadata } from 'next'; import type { Metadata } from 'next';
import { Inter as FontSans } from 'next/font/google'; import { Inter as FontSans } from 'next/font/google';
import Script from 'next/script';
import './globals.css'; import './globals.css';
export const metadata: Metadata = { export const metadata: Metadata = {
@@ -34,8 +34,12 @@ export default function RootLayout({
<Tiles> <Tiles>
<div className='z-10'>{children}</div> <div className='z-10'>{children}</div>
</Tiles> </Tiles>
<Analytics />
</body> </body>
<Script
defer
src='https://analytics.frompixels.com/script.js'
data-website-id='588e7b7d-e9cd-4b96-94bf-8269c499b0a2'
/>
</html> </html>
); );
} }

View File

@@ -31,7 +31,7 @@ export const Home = () => {
'@type': 'WebSite', '@type': 'WebSite',
name: 'Hackernews Newsletter', name: 'Hackernews Newsletter',
title: 'Home', title: 'Home',
url: process.env.HOME_URL url: process.env.NEXT_PUBLIC_HOME_URL
}; };
const form = useForm<SubscribeFormType>({ const form = useForm<SubscribeFormType>({

View File

@@ -2,458 +2,113 @@
import { CustomCard } from '@components/CustomCard'; import { CustomCard } from '@components/CustomCard';
import { SchemaOrg } from '@components/SchemaOrg'; import { SchemaOrg } from '@components/SchemaOrg';
import { useState, useEffect } from 'react';
const useObfuscatedEmail = () => {
const [email, setEmail] = useState<string | null>(null);
useEffect(() => {
setEmail(process.env.NEXT_PUBLIC_BRAND_EMAIL || null);
}, []);
return email;
};
const Privacy = () => { const Privacy = () => {
const email = useObfuscatedEmail();
const schema = { const schema = {
'@context': 'https://schema.org', '@context': 'https://schema.org',
'@type': 'WebSite', '@type': 'WebSite',
name: 'Hackernews Newsletter', name: 'Hackernews Newsletter',
title: 'Privacy Policy', title: 'Privacy Policy',
url: `${process.env.HOME_URL}/privacy` url: `${process.env.NEXT_PUBLIC_HOME_URL}/privacy`
}; };
const body = ( const body = (
<div className='privacy-content my-2 max-h-[50vh] space-y-1 overflow-auto'> <div className='privacy-content my-2 max-h-[50vh] space-y-1 overflow-auto'>
<h2>Who We Are</h2>
<p className='leading-relaxed'> <p className='leading-relaxed'>
This Privacy Policy describes Our policies and procedures on the Data controller: {process.env.NEXT_PUBLIC_BRAND_OWNER_NAME}, an
collection, use and disclosure of Your information when You use the individual based in {process.env.NEXT_PUBLIC_BRAND_COUNTRY}.
Service and tells You about Your privacy rights and how the law protects
You.
</p> </p>
<p> <p>
We use Your Personal data to provide and improve the Service. By using Contact:{' '}
the Service, You agree to the collection and use of information in
accordance with this Privacy Policy.
</p>
<h2>Interpretation and Definitions</h2>
<h3>Interpretation</h3>
<p className='leading-relaxed'>
The words of which the initial letter is capitalized have meanings
defined under the following conditions. The following definitions shall
have the same meaning regardless of whether they appear in singular or
in plural.
</p>
<h3>Definitions</h3>
<p className='leading-relaxed'>
For the purposes of this Privacy Policy:
</p>
<ul className='list-disc space-y-4 pl-6'>
<li>
<p>
<strong>Account</strong> means a unique account created for You to
access our Service or parts of our Service.
</p>
</li>
<li>
<p>
<strong>Affiliate</strong> means an entity that controls, is
controlled by or is under common control with a party, where
&quot;control&quot; means ownership of 50% or more of the shares,
equity interest or other securities entitled to vote for election of
directors or other managing authority.
</p>
</li>
<li>
<p>
<strong>Application</strong> refers to{' '}
{process.env.NEXT_PUBLIC_BRAND_NAME}, the software program provided
by the Company.
</p>
</li>
<li>
<p>
<strong>Company</strong> (referred to as either &quot;the
Company&quot;, &quot;We&quot;, &quot;Us&quot; or &quot;Our&quot; in
this Agreement) refers to {process.env.NEXT_PUBLIC_BRAND_NAME}.
</p>
</li>
<li>
<p>
<strong>Country</strong> refers to:{' '}
{process.env.NEXT_PUBLIC_BRAND_COUNTRY}
</p>
</li>
<li>
<p>
<strong>Device</strong> means any device that can access the Service
such as a computer, a cellphone or a digital tablet.
</p>
</li>
<li>
<p>
<strong>Personal Data</strong> is any information that relates to an
identified or identifiable individual.
</p>
</li>
<li>
<p>
<strong>Service</strong> refers to the Application.
</p>
</li>
<li>
<p>
<strong>Service Provider</strong> means any natural or legal person
who processes the data on behalf of the Company. It refers to
third-party companies or individuals employed by the Company to
facilitate the Service, to provide the Service on behalf of the
Company, to perform services related to the Service or to assist the
Company in analyzing how the Service is used.
</p>
</li>
<li>
<p>
<strong>Usage Data</strong> refers to data collected automatically,
either generated by the use of the Service or from the Service
infrastructure itself (for example, the duration of a page visit).
</p>
</li>
<li>
<p>
<strong>You</strong> means the individual accessing or using the
Service, or the company, or other legal entity on behalf of which
such individual is accessing or using the Service, as applicable.
</p>
</li>
</ul>
<h2>Data Collection and Usage</h2>
<h3>Types of Data Collected</h3>
<h4>Personal Data</h4>
<p className='leading-relaxed'>
While using Our Service, We may ask You to provide Us with certain
personally identifiable information that can be used to contact or
identify You. Personally identifiable information may include, but is
not limited to:
</p>
<ul className='list-disc space-y-4 pl-6'>
<li>
<p>Email address</p>
</li>
<li>
<p>Usage Data</p>
</li>
</ul>
<h4>Usage Data</h4>
<p className='leading-relaxed'>
Usage Data is collected automatically when using the Service.
</p>
<p>
Usage Data may include information such as Your Device&apos;s Internet
Protocol address (e.g. IP address), browser type, browser version, the
pages of our Service that You visit, the time and date of Your visit,
the time spent on those pages, unique device identifiers and other
diagnostic data.
</p>
<p>
When You access the Service by or through a mobile device, We may
collect certain information automatically, including, but not limited
to, the type of mobile device You use, Your mobile device unique ID, the
IP address of Your mobile device, Your mobile operating system, the type
of mobile Internet browser You use, unique device identifiers and other
diagnostic data.
</p>
<p>
We may also collect information that Your browser sends whenever You
visit our Service or when You access the Service by or through a mobile
device.
</p>
<h2>Use of Your Personal Data</h2>
<p className='leading-relaxed'>
The Company may use Personal Data for the following purposes:
</p>
<ul className='list-disc space-y-4 pl-6'>
<li>
<p>
<strong>To provide and maintain our Service</strong>, including to
monitor the usage of our Service.
</p>
</li>
<li>
<p>
<strong>To manage Your Account:</strong> to manage Your registration
as a user of the Service. The Personal Data You provide can give You
access to different functionalities of the Service that are
available to You as a registered user.
</p>
</li>
<li>
<p>
<strong>For the performance of a contract:</strong> the development,
compliance and undertaking of the purchase contract for the
products, items or services You have purchased or of any other
contract with Us through the Service.
</p>
</li>
<li>
<p>
<strong>To contact You:</strong> To contact You by email, telephone
calls, SMS, or other equivalent forms of electronic communication,
such as a mobile application&apos;s push notifications regarding
updates or informative communications related to the
functionalities, products or contracted services, including the
security updates, when necessary or reasonable for their
implementation.
</p>
</li>
<li>
<p>
<strong>To provide You</strong> with news, special offers and
general information about other goods, services and events which we
offer that are similar to those that you have already purchased or
enquired about unless You have opted not to receive such
information.
</p>
</li>
<li>
<p>
<strong>To manage Your requests:</strong> To attend and manage Your
requests to Us.
</p>
</li>
<li>
<p>
<strong>For business transfers:</strong> We may use Your information
to evaluate or conduct a merger, divestiture, restructuring,
reorganization, dissolution, or other sale or transfer of some or
all of Our assets, whether as a going concern or as part of
bankruptcy, liquidation, or similar proceeding, in which Personal
Data held by Us about our Service users is among the assets
transferred.
</p>
</li>
<li>
<p>
<strong>For other purposes</strong>: We may use Your information for
other purposes, such as data analysis, identifying usage trends,
determining the effectiveness of our promotional campaigns and to
evaluate and improve our Service, products, services, marketing and
your experience.
</p>
</li>
</ul>
<p>We may share Your personal information in the following situations:</p>
<ul className='list-disc space-y-4 pl-6'>
<li>
<strong>With Service Providers:</strong> We may share Your personal
information with Service Providers to monitor and analyze the use of
our Service, to contact You.
</li>
<li>
<strong>For business transfers:</strong> We may share or transfer Your
personal information in connection with, or during negotiations of,
any merger, sale of Company assets, financing, or acquisition of all
or a portion of Our business to another company.
</li>
<li>
<strong>With Affiliates:</strong> We may share Your information with
Our affiliates, in which case we will require those affiliates to
honor this Privacy Policy. Affiliates include Our parent company and
any other subsidiaries, joint venture partners or other companies that
We control or that are under common control with Us.
</li>
<li>
<strong>With business partners:</strong> We may share Your information
with Our business partners to offer You certain products, services or
promotions.
</li>
<li>
<strong>With other users:</strong> when You share personal information
or otherwise interact in the public areas with other users, such
information may be viewed by all users and may be publicly distributed
outside.
</li>
<li>
<strong>With Your consent</strong>: We may disclose Your personal
information for any other purpose with Your consent.
</li>
</ul>
<h2>Data Handling and Security</h2>
<h3>Retention of Your Personal Data</h3>
<p className='leading-relaxed'>
The Company will retain Your Personal Data only for as long as is
necessary for the purposes set out in this Privacy Policy. We will
retain and use Your Personal Data to the extent necessary to comply with
our legal obligations (for example, if we are required to retain your
data to comply with applicable laws), resolve disputes, and enforce our
legal agreements and policies.
</p>
<p>
The Company will also retain Usage Data for internal analysis purposes.
Usage Data is generally retained for a shorter period of time, except
when this data is used to strengthen the security or to improve the
functionality of Our Service, or We are legally obligated to retain this
data for longer time periods.
</p>
<h3>Transfer of Your Personal Data</h3>
<p className='leading-relaxed'>
Your information, including Personal Data, is processed at the
Company&apos;s operating offices and in any other places where the
parties involved in the processing are located. It means that this
information may be transferred to and maintained on computers
located outside of Your state, province, country or other governmental
jurisdiction where the data protection laws may differ than those from
Your jurisdiction.
</p>
<h3>Security of Your Personal Data</h3>
<p className='leading-relaxed'>
Your consent to this Privacy Policy followed by Your submission of such
information represents Your agreement to that transfer.
</p>
<p>
The Company will take all steps reasonably necessary to ensure that Your
data is treated securely and in accordance with this Privacy Policy and
no transfer of Your Personal Data will take place to an organization or
a country unless there are adequate controls in place including the
security of Your data and other personal information.
</p>
<h4 className='text-lg font-medium text-gray-800'>
Delete Your Personal Data
</h4>
<p className='leading-relaxed'>
You have the right to delete or request that We assist in deleting the
Personal Data that We have collected about You.
</p>
<p>
Our Service may give You the ability to delete certain information about
You from within the Service.
</p>
<p>
You may update, amend, or delete Your information at any time by signing
in to Your Account, if you have one, and visiting the account settings
section that allows you to manage Your personal information. You may
also contact Us to request access to, correct, or delete any personal
information that You have provided to Us.
</p>
<p>
Please note, however, that We may need to retain certain information
when we have a legal obligation or lawful basis to do so.
</p>
<h2>Legal Disclosures</h2>
<h3>Business Transactions</h3>
<p className='leading-relaxed'>
If the Company is involved in a merger, acquisition or asset sale, Your
Personal Data may be transferred. We will provide notice before Your
Personal Data is transferred and becomes subject to a different Privacy
Policy.
</p>
<h3>Law Enforcement</h3>
<p className='leading-relaxed'>
Under certain circumstances, the Company may be required to disclose
Your Personal Data if required to do so by law or in response to valid
requests by public authorities (e.g. a court or a government agency).
</p>
<h3>Other Legal Requirements</h3>
<p className='leading-relaxed'>
The Company may disclose Your Personal Data in the good faith belief
that such action is necessary to:
</p>
<ul className='list-disc space-y-4 pl-6'>
<li>Comply with a legal obligation</li>
<li>Protect and defend the rights or property of the Company</li>
<li>
Prevent or investigate possible wrongdoing in connection with the
Service
</li>
<li>
Protect the personal safety of Users of the Service or the public
</li>
<li>Protect against legal liability</li>
</ul>
<h2>Additional Information</h2>
<p className='leading-relaxed'>
The security of Your Personal Data is important to Us, but remember that
no method of transmission over the Internet, or method of electronic
storage is 100% secure. While We strive to use commercially acceptable
means to protect Your Personal Data, We cannot guarantee its absolute
security.
</p>
<h3>Children&apos;s Privacy</h3>
<p className='leading-relaxed'>
Our Service does not address anyone under the age of 13. We do not
knowingly collect personally identifiable information from anyone under
the age of 13. If You are a parent or guardian and You are aware that
Your child has provided Us with Personal Data, please contact Us. If We
become aware that We have collected Personal Data from anyone under the
age of 13 without verification of parental consent, We take steps to
remove that information from Our servers.
</p>
<p>
If We need to rely on consent as a legal basis for processing Your
information and Your country requires consent from a parent, We may
require Your parent&apos;s consent before We collect and use that
information.
</p>
<h3>Links to Other Websites</h3>
<p className='leading-relaxed'>
Our Service may contain links to other websites that are not operated by
Us. If You click on a third party link, You will be directed to that
third party&apos;s site. We strongly advise You to review the Privacy
Policy of every site You visit.
</p>
<p>
We have no control over and assume no responsibility for the content,
privacy policies or practices of any third party sites or services.
</p>
<h3>Changes to this Privacy Policy</h3>
<p className='leading-relaxed'>
We may update Our Privacy Policy from time to time. We will notify You
of any changes by posting the new Privacy Policy on this page.
</p>
<p>
We will let You know via email and/or a prominent notice on Our Service,
prior to the change becoming effective and update the &quot;Last
updated&quot; date at the top of this Privacy Policy.
</p>
<p>
You are advised to review this Privacy Policy periodically for any
changes. Changes to this Privacy Policy are effective when they are
posted on this page.
</p>
<h2>Contact Information</h2>
<p className='leading-relaxed'>
If you have any questions about this Privacy Policy, You can contact us
by writing to{' '}
{email ? (
<a <a
href={`mailto:${email}`} href={`mailto:${process.env.NEXT_PUBLIC_BRAND_EMAIL}`}
className='text-purple-600 hover:text-purple-700' className='text-purple-600 hover:text-purple-700'
> >
{email} {process.env.NEXT_PUBLIC_BRAND_EMAIL}
</a>
</p>
<h2>What We Collect</h2>
<p className='leading-relaxed'>
Your email address (required to send the newsletter).
</p>
<h2>Why We Collect It</h2>
<p className='leading-relaxed'>
To deliver the daily {process.env.NEXT_PUBLIC_BRAND_NAME}{' '}
newsletter&mdash;a digest of top Hacker News stories with AI-generated
commentary.
</p>
<p>
We do not sell products, track your activity beyond essential delivery,
or share your data for marketing.
</p>
<h2>Legal Basis</h2>
<p className='leading-relaxed'>
Your explicit consent via double opt-in signup (you receive a
confirmation email with an activation link).
</p>
<h2>Third Parties</h2>
<ul className='list-disc space-y-4 pl-6'>
<li>
<strong>Email delivery:</strong> Resend (US). GDPR-compliant email
infrastructure.
</li>
<li>
<strong>Hosting &amp; analytics:</strong> Vercel (US). Analytics are
anonymized and aggregated&mdash;no personal data beyond your email is
collected.
</li>
<li>
<strong>Content source:</strong> Public Hacker News API (Y Combinator,
US).
</li>
</ul>
<h2>Your Rights</h2>
<ul className='list-disc space-y-4 pl-6'>
<li>Unsubscribe anytime via the link in every email</li>
<li>
Request deletion of your email by contacting{' '}
<a
href={`mailto:${process.env.NEXT_PUBLIC_BRAND_EMAIL}`}
className='text-purple-600 hover:text-purple-700'
>
{process.env.NEXT_PUBLIC_BRAND_EMAIL}
</a>
</li>
<li>
Upon unsubscribe or deletion request, your email is permanently
deleted from our database
</li>
</ul>
<h2>Data Retention</h2>
<p className='leading-relaxed'>
We retain your email address only until you unsubscribe.
</p>
<h2>International Transfers</h2>
<p className='leading-relaxed'>
We rely on EU-approved safeguards including Standard Contractual Clauses
to ensure adequate protection when using US-based services.
</p>
<h2>Changes</h2>
<p className='leading-relaxed'>
We may update this policy occasionally. The latest version will always
be available at{' '}
<a
href={`${process.env.NEXT_PUBLIC_HOME_URL}/privacy`}
target='_blank'
rel='noopener noreferrer'
className='text-purple-600 hover:text-purple-700'
>
{process.env.NEXT_PUBLIC_HOME_URL}/privacy
</a> </a>
) : (
<span className='text-gray-400'>loading...</span>
)}
. .
</p> </p>
</div> </div>
@@ -465,7 +120,7 @@ const Privacy = () => {
<CustomCard <CustomCard
className='max-90vh max-90vw' className='max-90vh max-90vw'
title='Privacy Policy' title='Privacy Policy'
description='Last updated: November 23, 2024' description='Last updated: January 28, 2026'
content={body} content={body}
/> />
</> </>

View File

@@ -6,6 +6,6 @@ export default function robots(): MetadataRoute.Robots {
userAgent: '*', userAgent: '*',
disallow: '' disallow: ''
}, },
sitemap: `${process.env.HOME_URL!}/sitemap.xml` sitemap: `${process.env.NEXT_PUBLIC_HOME_URL!}/sitemap.xml`
}; };
} }

View File

@@ -3,19 +3,19 @@ import { MetadataRoute } from 'next';
export default function sitemap(): MetadataRoute.Sitemap { export default function sitemap(): MetadataRoute.Sitemap {
return [ return [
{ {
url: process.env.HOME_URL!, url: process.env.NEXT_PUBLIC_HOME_URL!,
lastModified: new Date(), lastModified: new Date(),
changeFrequency: 'yearly', changeFrequency: 'yearly',
priority: 1 priority: 1
}, },
{ {
url: `${process.env.HOME_URL!}/privacy`, url: `${process.env.NEXT_PUBLIC_HOME_URL!}/privacy`,
lastModified: new Date(), lastModified: new Date(),
changeFrequency: 'yearly', changeFrequency: 'yearly',
priority: 0.5 priority: 0.5
}, },
{ {
url: `${process.env.HOME_URL!}/unsubscribe`, url: `${process.env.NEXT_PUBLIC_HOME_URL!}/unsubscribe`,
lastModified: new Date(), lastModified: new Date(),
changeFrequency: 'yearly', changeFrequency: 'yearly',
priority: 0.2 priority: 0.2

View File

@@ -33,7 +33,7 @@ const Unsubscribe = () => {
'@type': 'WebSite', '@type': 'WebSite',
name: 'Hackernews Newsletter', name: 'Hackernews Newsletter',
title: 'Unsubscribe', title: 'Unsubscribe',
url: `${process.env.HOME_URL}/unsubscribe` url: `${process.env.NEXT_PUBLIC_HOME_URL}/unsubscribe`
}; };
const form = useForm<UnsubscribeFormType>({ const form = useForm<UnsubscribeFormType>({

View File

@@ -22,7 +22,7 @@ export const ConfirmationTemplate = (code: string) => {
}} }}
> >
<a <a
href={`${process.env.HOME_URL}/confirmation?code=${code}`} href={`${process.env.NEXT_PUBLIC_HOME_URL}/confirmation?code=${code}`}
style={{ style={{
display: 'inline-block', display: 'inline-block',
padding: '12px 24px', padding: '12px 24px',

View File

@@ -22,7 +22,7 @@ export const UnsubscribeTemplate = () => {
}} }}
> >
<a <a
href={`${process.env.HOME_URL}/`} href={`${process.env.NEXT_PUBLIC_HOME_URL}/`}
style={{ style={{
display: 'inline-block', display: 'inline-block',
padding: '12px 24px', padding: '12px 24px',

View File

@@ -1,17 +1,12 @@
const iconStyle = { import {
display: 'inline-block', User,
verticalAlign: 'middle' Building2,
}; Mail,
LogOut,
const Icon = ({ name, size = 16 }: { name: string; size?: number }) => ( LayoutGrid,
<img Shield,
src={`${process.env.HOME_URL}/email-icons/${name}.png`} Home
width={size} } from 'lucide-react';
height={size}
alt=""
style={iconStyle}
/>
);
export const Footer = () => { export const Footer = () => {
return ( return (
@@ -49,7 +44,7 @@ export const Footer = () => {
letterSpacing: '0.05em' letterSpacing: '0.05em'
}} }}
> >
<Icon name="user" size={16} /> <User size={16} color='#386FA4' />
Contact Us Contact Us
</h4> </h4>
<p <p
@@ -62,7 +57,7 @@ export const Footer = () => {
color: '#4A5568' color: '#4A5568'
}} }}
> >
<Icon name="building-2" size={14} /> <Building2 size={14} color='#386FA4' />
{process.env.NEXT_PUBLIC_BRAND_NAME} {process.env.NEXT_PUBLIC_BRAND_NAME}
</p> </p>
<p <p
@@ -75,7 +70,7 @@ export const Footer = () => {
color: '#4A5568' color: '#4A5568'
}} }}
> >
<Icon name="mail" size={14} /> <Mail size={14} color='#386FA4' />
<a <a
href={`mailto:${process.env.NEXT_PUBLIC_BRAND_EMAIL}`} href={`mailto:${process.env.NEXT_PUBLIC_BRAND_EMAIL}`}
style={{ color: '#386FA4', textDecoration: 'none' }} style={{ color: '#386FA4', textDecoration: 'none' }}
@@ -93,11 +88,11 @@ export const Footer = () => {
color: '#4A5568' color: '#4A5568'
}} }}
> >
<Icon name="log-out" size={14} /> <LogOut size={14} color='#386FA4' />
<span> <span>
Click{' '} Click{' '}
<a <a
href={`${process.env.HOME_URL}/unsubscribe`} href={`${process.env.NEXT_PUBLIC_HOME_URL}/unsubscribe`}
style={{ color: '#386FA4', textDecoration: 'none' }} style={{ color: '#386FA4', textDecoration: 'none' }}
> >
here here
@@ -129,7 +124,7 @@ export const Footer = () => {
letterSpacing: '0.05em' letterSpacing: '0.05em'
}} }}
> >
<Icon name="layout-grid" size={16} /> <LayoutGrid size={16} color='#386FA4' />
Quick Links Quick Links
</h4> </h4>
<p <p
@@ -142,9 +137,9 @@ export const Footer = () => {
color: '#4A5568' color: '#4A5568'
}} }}
> >
<Icon name="shield" size={14} /> <Shield size={14} color='#386FA4' />
<a <a
href={`${process.env.HOME_URL}/privacy`} href={`${process.env.NEXT_PUBLIC_HOME_URL}/privacy`}
style={{ color: '#386FA4', textDecoration: 'none' }} style={{ color: '#386FA4', textDecoration: 'none' }}
> >
Privacy Policy Privacy Policy
@@ -160,9 +155,9 @@ export const Footer = () => {
color: '#4A5568' color: '#4A5568'
}} }}
> >
<Icon name="house" size={14} /> <Home size={14} color='#386FA4' />
<a <a
href={process.env.HOME_URL} href={process.env.NEXT_PUBLIC_HOME_URL}
style={{ color: '#386FA4', textDecoration: 'none' }} style={{ color: '#386FA4', textDecoration: 'none' }}
> >
Visit Website Visit Website

View File

@@ -1,3 +1,4 @@
version: '3.8'
services: services:
postgres: postgres:
image: postgres:16-alpine image: postgres:16-alpine
@@ -10,37 +11,6 @@ services:
- '5432:5432' - '5432:5432'
volumes: volumes:
- postgres-data:/var/lib/postgresql/data - postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U newsletter"]
interval: 10s
timeout: 5s
retries: 5
# Postfix mail server with OpenDKIM for self-hosted email
postfix:
build: ./docker/postfix
container_name: newsletter-postfix
restart: always
# dns: Uses VPS default DNS. If MX lookups fail, uncomment and set explicit DNS:
# - 213.186.33.99 # OVH DNS
environment:
- MAIL_DOMAIN=${MAIL_DOMAIN:-example.com}
- MAIL_HOSTNAME=${MAIL_HOSTNAME:-mail.example.com}
- DKIM_SELECTOR=${DKIM_SELECTOR:-mail}
volumes:
# Persist DKIM keys across container rebuilds
- postfix-dkim:/etc/opendkim/keys
# Persist mail queue
- postfix-spool:/var/spool/postfix
# Port 25 not exposed to host - only accessible within Docker network
healthcheck:
test: ["CMD-SHELL", "echo 'QUIT' | nc -w 5 localhost 25 | grep -q '220' || exit 1"]
interval: 30s
timeout: 10s
start_period: 10s
retries: 3
volumes: volumes:
postgres-data: postgres-data:
postfix-dkim:
postfix-spool:

View File

@@ -1,30 +0,0 @@
FROM debian:bookworm-slim
# install oostfix and OpenDKIM
RUN apt-get update && apt-get install -y \
postfix \
opendkim \
opendkim-tools \
mailutils \
ca-certificates \
netcat-openbsd \
&& rm -rf /var/lib/apt/lists/*
# create OpenDKIM paths
RUN mkdir -p /etc/opendkim/keys && \
chown -R opendkim:opendkim /etc/opendkim && \
chmod 700 /etc/opendkim/keys
# copy config
COPY postfix-main.cf /etc/postfix/main.cf
COPY opendkim.conf /etc/opendkim.conf
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
EXPOSE 25
HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \
CMD echo "QUIT" | nc -w 5 localhost 25 | grep -q "220" || exit 1
ENTRYPOINT ["/entrypoint.sh"]

View File

@@ -1,90 +0,0 @@
#!/bin/bash
set -e
# environment variables
MAIL_DOMAIN="${MAIL_DOMAIN:-example.com}"
MAIL_HOSTNAME="${MAIL_HOSTNAME:-mail.example.com}"
DKIM_SELECTOR="${DKIM_SELECTOR:-mail}"
echo "Setting up postfix for domain: ${MAIL_DOMAIN}"
echo "Hostname: ${MAIL_HOSTNAME}"
# configure postfix domain
postconf -e "myhostname=${MAIL_HOSTNAME}"
postconf -e "mydomain=${MAIL_DOMAIN}"
postconf -e "myorigin=\$mydomain"
postconf -e "mydestination=\$myhostname, localhost.\$mydomain, localhost"
# create OpenDKIM key folder for domain
DKIM_KEY_DIR="/etc/opendkim/keys/${MAIL_DOMAIN}"
mkdir -p "${DKIM_KEY_DIR}"
# generate DKIM keys if they don't exist
if [ ! -f "${DKIM_KEY_DIR}/${DKIM_SELECTOR}.private" ]; then
echo "Generating DKIM keys for ${MAIL_DOMAIN}..."
opendkim-genkey -b 2048 -d "${MAIL_DOMAIN}" -D "${DKIM_KEY_DIR}" -s "${DKIM_SELECTOR}" -v
chown -R opendkim:opendkim "${DKIM_KEY_DIR}"
chmod 600 "${DKIM_KEY_DIR}/${DKIM_SELECTOR}.private"
echo ""
echo "============================================"
echo "DKIM PUBLIC KEY - ADD THIS TO YOUR DNS:"
echo "============================================"
echo "Record Type: TXT"
echo "Name: ${DKIM_SELECTOR}._domainkey.${MAIL_DOMAIN}"
echo ""
cat "${DKIM_KEY_DIR}/${DKIM_SELECTOR}.txt"
echo ""
echo "============================================"
echo ""
else
echo "Using existing DKIM keys"
fi
# configure OpenDKIM KeyTable
cat > /etc/opendkim/KeyTable << EOF
${DKIM_SELECTOR}._domainkey.${MAIL_DOMAIN} ${MAIL_DOMAIN}:${DKIM_SELECTOR}:${DKIM_KEY_DIR}/${DKIM_SELECTOR}.private
EOF
# configure OpenDKIM SigningTable
cat > /etc/opendkim/SigningTable << EOF
*@${MAIL_DOMAIN} ${DKIM_SELECTOR}._domainkey.${MAIL_DOMAIN}
EOF
# configure OpenDKIM TrustedHosts
cat > /etc/opendkim/TrustedHosts << EOF
127.0.0.1
localhost
${MAIL_DOMAIN}
*.${MAIL_DOMAIN}
172.16.0.0/12
192.168.0.0/16
10.0.0.0/8
EOF
# set permissions
chown -R opendkim:opendkim /etc/opendkim
chmod 600 /etc/opendkim/KeyTable
chmod 600 /etc/opendkim/SigningTable
# create postfix spool folders
mkdir -p /var/spool/postfix/pid
chown root:root /var/spool/postfix
chown root:root /var/spool/postfix/pid
# start OpenDKIM in background
echo "Starting OpenDKIM..."
opendkim -f &
# wait for OpenDKIM to start
sleep 2
# copy DNS config to postfix chroot
mkdir -p /var/spool/postfix/etc
cp /etc/resolv.conf /var/spool/postfix/etc/
cp /etc/services /var/spool/postfix/etc/
cp /etc/hosts /var/spool/postfix/etc/
# start postfix in foreground
echo "Starting Postfix..."
postfix start-fg

View File

@@ -1,30 +0,0 @@
# OpenDKIM configuration for email signing
# log to syslog
Syslog yes
SyslogSuccess yes
LogWhy yes
# required for verification
Canonicalization relaxed/simple
# sign mode (s=sign, v=verify)
Mode sv
# sign subdomains
SubDomains no
# key configuration (will be set by entrypoint)
KeyTable /etc/opendkim/KeyTable
SigningTable refile:/etc/opendkim/SigningTable
ExternalIgnoreList refile:/etc/opendkim/TrustedHosts
InternalHosts refile:/etc/opendkim/TrustedHosts
# socket for postfix connection
Socket inet:8891@localhost
# user
UserID opendkim
# permissions
RequireSafeKeys false

View File

@@ -1,50 +0,0 @@
# Postfix main configuration for newsletter sending
# Domain and hostname will be set by entrypoint script
smtpd_banner = $myhostname ESMTP
biff = no
append_dot_mydomain = no
readme_directory = no
# TLS parameters (for outbound connections)
smtp_tls_security_level = may
smtp_tls_CAfile = /etc/ssl/certs/ca-certificates.crt
# Network settings
inet_interfaces = all
inet_protocols = ipv4
# Relay settings (don't relay for others)
relayhost =
mynetworks = 127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128 172.16.0.0/12 192.168.0.0/16 10.0.0.0/8
# Message size limit (10MB)
message_size_limit = 10240000
# Queue settings
maximal_queue_lifetime = 5d
bounce_queue_lifetime = 5d
# Security settings
smtpd_helo_required = yes
disable_vrfy_command = yes
smtpd_helo_restrictions =
permit_mynetworks,
reject_invalid_helo_hostname,
reject_non_fqdn_helo_hostname,
permit
smtpd_recipient_restrictions =
permit_mynetworks,
reject_unauth_destination,
permit
# OpenDKIM integration
milter_default_action = accept
milter_protocol = 6
smtpd_milters = inet:localhost:8891
non_smtpd_milters = inet:localhost:8891
# Notify on bounces
notify_classes = bounce, delay, resource, software

1465
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -26,14 +26,12 @@
"@prisma/client": "^5.6.0", "@prisma/client": "^5.6.0",
"@radix-ui/react-label": "^2.0.2", "@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-slot": "^1.0.2",
"@vercel/analytics": "^1.1.1",
"axios": "^1.12.0", "axios": "^1.12.0",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.0.0", "clsx": "^2.0.0",
"isomorphic-dompurify": "^2.15.0", "isomorphic-dompurify": "^2.15.0",
"lucide-react": "^0.460.0", "lucide-react": "^0.460.0",
"next": "^15.5.9", "next": "^15.5.9",
"nodemailer": "^7.0.12",
"openai": "^4.77.0", "openai": "^4.77.0",
"postcss-nesting": "^12.0.2", "postcss-nesting": "^12.0.2",
"react": "^18", "react": "^18",
@@ -47,7 +45,6 @@
"@commitlint/cli": "^18.4.3", "@commitlint/cli": "^18.4.3",
"@commitlint/config-conventional": "^18.4.3", "@commitlint/config-conventional": "^18.4.3",
"@types/node": "^20", "@types/node": "^20",
"@types/nodemailer": "^7.0.5",
"@types/react": "^18", "@types/react": "^18",
"@types/react-dom": "^18", "@types/react-dom": "^18",
"@typescript-eslint/eslint-plugin": "^6.12.0", "@typescript-eslint/eslint-plugin": "^6.12.0",

View File

@@ -1,23 +0,0 @@
-- CreateTable
CREATE TABLE "email_logs" (
"id" SERIAL NOT NULL,
"recipient" TEXT NOT NULL,
"subject" TEXT,
"message_id" TEXT,
"status" TEXT NOT NULL,
"sent_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"error_message" TEXT,
"bounce_type" TEXT,
"bounce_details" JSONB,
CONSTRAINT "email_logs_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "email_logs_recipient_idx" ON "email_logs"("recipient");
-- CreateIndex
CREATE INDEX "email_logs_status_idx" ON "email_logs"("status");
-- CreateIndex
CREATE INDEX "email_logs_sent_at_idx" ON "email_logs"("sent_at");

View File

@@ -32,20 +32,3 @@ model News {
@@map(name: "news") @@map(name: "news")
} }
model EmailLog {
id Int @id @default(autoincrement())
recipient String
subject String?
messageId String? @map("message_id")
status String // 'sent', 'failed', 'bounced'
sentAt DateTime @default(now()) @map("sent_at")
errorMessage String? @map("error_message")
bounceType String? @map("bounce_type")
bounceDetails Json? @map("bounce_details")
@@index([recipient])
@@index([status])
@@index([sentAt])
@@map(name: "email_logs")
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 397 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 416 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 305 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 328 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 414 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 480 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 394 B

View File

@@ -1,213 +0,0 @@
import nodemailer from 'nodemailer';
import prisma from '@prisma/prisma';
interface EmailTemplate {
subject: string;
template: JSX.Element;
}
interface SendResult {
success: boolean;
messageId?: string;
error?: string;
}
// Create nodemailer transporter for Postfix container
const createTransporter = () => {
// Default to 'postfix' for Docker networking (service name in docker-compose)
// Override with EMAIL_HOST for different setups
const host = process.env.EMAIL_HOST || 'postfix';
const port = parseInt(process.env.EMAIL_PORT || '25', 10);
return nodemailer.createTransport({
host,
port,
secure: false, // true for 465, false for other ports
tls: {
rejectUnauthorized: false
},
// Connection pooling for better performance
pool: true,
maxConnections: 5,
maxMessages: 100
});
};
// Render React component to HTML string using dynamic import
const renderTemplate = async (template: JSX.Element): Promise<string> => {
// Dynamic import to avoid Next.js bundling issues with react-dom/server
const { renderToStaticMarkup } = await import('react-dom/server');
const html = renderToStaticMarkup(template);
return `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body style="margin: 0; padding: 0; background-color: #f4f4f5;">
${html}
</body>
</html>`;
};
// Generate plain text version from HTML (basic extraction)
const htmlToPlainText = (html: string): string => {
return html
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
.replace(/<[^>]+>/g, '')
.replace(/&nbsp;/g, ' ')
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'")
.replace(/\s+/g, ' ')
.trim();
};
// Log email send attempt to database
const logEmailSend = async (
recipient: string,
subject: string,
status: 'sent' | 'failed',
messageId?: string,
errorMessage?: string
) => {
try {
await prisma.emailLog.create({
data: {
recipient,
subject,
status,
messageId,
errorMessage
}
});
} catch (error) {
console.error('Failed to log email send:', error);
}
};
// Send a single email
const sendSingleEmail = async (
transporter: nodemailer.Transporter,
recipient: string,
subject: string,
htmlContent: string,
textContent: string
): Promise<SendResult> => {
const fromAddress = process.env.EMAIL_FROM || process.env.RESEND_FROM;
const fromName = process.env.NEXT_PUBLIC_BRAND_NAME || 'HackerNews Newsletter';
try {
const info = await transporter.sendMail({
from: `"${fromName}" <${fromAddress}>`,
to: recipient,
subject,
text: textContent,
html: htmlContent,
headers: {
'List-Unsubscribe': `<${process.env.HOME_URL}/unsubscribe>`,
'X-Newsletter-ID': `hackernews-${Date.now()}`
}
});
await logEmailSend(recipient, subject, 'sent', info.messageId);
return {
success: true,
messageId: info.messageId
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
await logEmailSend(recipient, subject, 'failed', undefined, errorMessage);
return {
success: false,
error: errorMessage
};
}
};
// Main sender function - maintains same API as Resend version
export async function sender(
recipients: string[],
{ subject, template }: EmailTemplate
): Promise<boolean> {
const fromAddress = process.env.EMAIL_FROM || process.env.RESEND_FROM;
if (!fromAddress) {
throw new Error('EMAIL_FROM or RESEND_FROM environment variable is not set');
}
if (recipients.length === 0) {
console.info(`${subject} email skipped for having zero recipients`);
return true;
}
const transporter = createTransporter();
// Render template to HTML
const htmlContent = await renderTemplate(template);
const textContent = htmlToPlainText(htmlContent);
// Add small delay between sends to avoid overwhelming mail server
const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
let successCount = 0;
let failCount = 0;
const errors: Array<{ email: string; error: string }> = [];
for (const recipient of recipients) {
const result = await sendSingleEmail(
transporter,
recipient,
subject,
htmlContent,
textContent
);
if (result.success) {
successCount++;
} else {
failCount++;
errors.push({ email: recipient, error: result.error || 'Unknown error' });
}
// Add 100ms delay between sends
if (recipients.length > 1) {
await delay(100);
}
}
// Close the transporter pool
transporter.close();
if (errors.length > 0) {
console.error('Email send errors:', errors);
}
console.info(
`${subject} email: ${successCount} sent, ${failCount} failed out of ${recipients.length} recipients`
);
// Return true if at least one email was sent successfully
return successCount > 0;
}
// Verify transporter connection (useful for health checks)
export async function verifyMailer(): Promise<boolean> {
const transporter = createTransporter();
try {
await transporter.verify();
console.log('Email server connection verified');
transporter.close();
return true;
} catch (error) {
console.error('Email server connection failed:', error);
transporter.close();
return false;
}
}

90
utils/sweego.ts Normal file
View File

@@ -0,0 +1,90 @@
interface EmailTemplate {
subject: string;
template: JSX.Element;
}
const SWEEGO_API_URL = 'https://api.sweego.io/send';
const renderTemplate = async (template: JSX.Element): Promise<string> => {
const { renderToStaticMarkup } = await import('react-dom/server');
const html = renderToStaticMarkup(template);
return `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body style="margin: 0; padding: 0; background-color: #f4f4f5;">
${html}
</body>
</html>`;
};
export async function sender(
recipients: string[],
{ subject, template }: EmailTemplate
): Promise<boolean> {
if (!process.env.SWEEGO_API_KEY) {
throw new Error('SWEEGO_API_KEY is not set');
}
if (!process.env.SWEEGO_FROM) {
throw new Error('SWEEGO_FROM is not set');
}
if (recipients.length === 0) {
console.info(`${subject} email skipped for having zero recipients`);
return true;
}
const htmlContent = await renderTemplate(template);
const fromName = process.env.NEXT_PUBLIC_BRAND_NAME || 'Newsletter';
let successCount = 0;
let failCount = 0;
for (const recipient of recipients) {
try {
const response = await fetch(SWEEGO_API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Api-Key': process.env.SWEEGO_API_KEY
},
body: JSON.stringify({
channel: 'email',
provider: 'sweego',
recipients: [{ email: recipient }],
from: {
name: fromName,
email: process.env.SWEEGO_FROM
},
subject,
'message-html': htmlContent,
headers: {
'List-Unsubscribe': `<mailto:${process.env.NEXT_PUBLIC_BRAND_EMAIL}>`
}
})
});
if (!response.ok) {
const error = await response.text();
console.error(
`Failed to send to ${recipient}: ${response.status} ${error}`
);
failCount++;
continue;
}
successCount++;
} catch (error) {
console.error(`Failed to send to ${recipient}:`, error);
failCount++;
}
}
console.info(
`${subject} email: ${successCount} sent, ${failCount} failed out of ${recipients.length} recipients`
);
return successCount > 0;
}

920
yarn.lock
View File

@@ -34,482 +34,6 @@
resolved "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz" resolved "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz"
integrity sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q== integrity sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==
"@aws-crypto/sha256-browser@5.2.0":
version "5.2.0"
resolved "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz"
integrity sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==
dependencies:
"@aws-crypto/sha256-js" "^5.2.0"
"@aws-crypto/supports-web-crypto" "^5.2.0"
"@aws-crypto/util" "^5.2.0"
"@aws-sdk/types" "^3.222.0"
"@aws-sdk/util-locate-window" "^3.0.0"
"@smithy/util-utf8" "^2.0.0"
tslib "^2.6.2"
"@aws-crypto/sha256-js@^5.2.0", "@aws-crypto/sha256-js@5.2.0":
version "5.2.0"
resolved "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz"
integrity sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==
dependencies:
"@aws-crypto/util" "^5.2.0"
"@aws-sdk/types" "^3.222.0"
tslib "^2.6.2"
"@aws-crypto/supports-web-crypto@^5.2.0":
version "5.2.0"
resolved "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz"
integrity sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==
dependencies:
tslib "^2.6.2"
"@aws-crypto/util@^5.2.0":
version "5.2.0"
resolved "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz"
integrity sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==
dependencies:
"@aws-sdk/types" "^3.222.0"
"@smithy/util-utf8" "^2.0.0"
tslib "^2.6.2"
"@aws-sdk/client-sesv2@^3.839.0":
version "3.971.0"
resolved "https://registry.npmjs.org/@aws-sdk/client-sesv2/-/client-sesv2-3.971.0.tgz"
integrity sha512-NP/lbf3mfY10Txzl0ml2YnTjnZwflp1+faOotMCrXi4fb6kInosdW0ZSHXNlNulFo9cW+llq07lD59Sw3nny+A==
dependencies:
"@aws-crypto/sha256-browser" "5.2.0"
"@aws-crypto/sha256-js" "5.2.0"
"@aws-sdk/core" "3.970.0"
"@aws-sdk/credential-provider-node" "3.971.0"
"@aws-sdk/middleware-host-header" "3.969.0"
"@aws-sdk/middleware-logger" "3.969.0"
"@aws-sdk/middleware-recursion-detection" "3.969.0"
"@aws-sdk/middleware-user-agent" "3.970.0"
"@aws-sdk/region-config-resolver" "3.969.0"
"@aws-sdk/signature-v4-multi-region" "3.970.0"
"@aws-sdk/types" "3.969.0"
"@aws-sdk/util-endpoints" "3.970.0"
"@aws-sdk/util-user-agent-browser" "3.969.0"
"@aws-sdk/util-user-agent-node" "3.971.0"
"@smithy/config-resolver" "^4.4.6"
"@smithy/core" "^3.20.6"
"@smithy/fetch-http-handler" "^5.3.9"
"@smithy/hash-node" "^4.2.8"
"@smithy/invalid-dependency" "^4.2.8"
"@smithy/middleware-content-length" "^4.2.8"
"@smithy/middleware-endpoint" "^4.4.7"
"@smithy/middleware-retry" "^4.4.23"
"@smithy/middleware-serde" "^4.2.9"
"@smithy/middleware-stack" "^4.2.8"
"@smithy/node-config-provider" "^4.3.8"
"@smithy/node-http-handler" "^4.4.8"
"@smithy/protocol-http" "^5.3.8"
"@smithy/smithy-client" "^4.10.8"
"@smithy/types" "^4.12.0"
"@smithy/url-parser" "^4.2.8"
"@smithy/util-base64" "^4.3.0"
"@smithy/util-body-length-browser" "^4.2.0"
"@smithy/util-body-length-node" "^4.2.1"
"@smithy/util-defaults-mode-browser" "^4.3.22"
"@smithy/util-defaults-mode-node" "^4.2.25"
"@smithy/util-endpoints" "^3.2.8"
"@smithy/util-middleware" "^4.2.8"
"@smithy/util-retry" "^4.2.8"
"@smithy/util-utf8" "^4.2.0"
tslib "^2.6.2"
"@aws-sdk/client-sso@3.971.0":
version "3.971.0"
resolved "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.971.0.tgz"
integrity sha512-Xx+w6DQqJxDdymYyIxyKJnRzPvVJ4e/Aw0czO7aC9L/iraaV7AG8QtRe93OGW6aoHSh72CIiinnpJJfLsQqP4g==
dependencies:
"@aws-crypto/sha256-browser" "5.2.0"
"@aws-crypto/sha256-js" "5.2.0"
"@aws-sdk/core" "3.970.0"
"@aws-sdk/middleware-host-header" "3.969.0"
"@aws-sdk/middleware-logger" "3.969.0"
"@aws-sdk/middleware-recursion-detection" "3.969.0"
"@aws-sdk/middleware-user-agent" "3.970.0"
"@aws-sdk/region-config-resolver" "3.969.0"
"@aws-sdk/types" "3.969.0"
"@aws-sdk/util-endpoints" "3.970.0"
"@aws-sdk/util-user-agent-browser" "3.969.0"
"@aws-sdk/util-user-agent-node" "3.971.0"
"@smithy/config-resolver" "^4.4.6"
"@smithy/core" "^3.20.6"
"@smithy/fetch-http-handler" "^5.3.9"
"@smithy/hash-node" "^4.2.8"
"@smithy/invalid-dependency" "^4.2.8"
"@smithy/middleware-content-length" "^4.2.8"
"@smithy/middleware-endpoint" "^4.4.7"
"@smithy/middleware-retry" "^4.4.23"
"@smithy/middleware-serde" "^4.2.9"
"@smithy/middleware-stack" "^4.2.8"
"@smithy/node-config-provider" "^4.3.8"
"@smithy/node-http-handler" "^4.4.8"
"@smithy/protocol-http" "^5.3.8"
"@smithy/smithy-client" "^4.10.8"
"@smithy/types" "^4.12.0"
"@smithy/url-parser" "^4.2.8"
"@smithy/util-base64" "^4.3.0"
"@smithy/util-body-length-browser" "^4.2.0"
"@smithy/util-body-length-node" "^4.2.1"
"@smithy/util-defaults-mode-browser" "^4.3.22"
"@smithy/util-defaults-mode-node" "^4.2.25"
"@smithy/util-endpoints" "^3.2.8"
"@smithy/util-middleware" "^4.2.8"
"@smithy/util-retry" "^4.2.8"
"@smithy/util-utf8" "^4.2.0"
tslib "^2.6.2"
"@aws-sdk/core@3.970.0":
version "3.970.0"
resolved "https://registry.npmjs.org/@aws-sdk/core/-/core-3.970.0.tgz"
integrity sha512-klpzObldOq8HXzDjDlY6K8rMhYZU6mXRz6P9F9N+tWnjoYFfeBMra8wYApydElTUYQKP1O7RLHwH1OKFfKcqIA==
dependencies:
"@aws-sdk/types" "3.969.0"
"@aws-sdk/xml-builder" "3.969.0"
"@smithy/core" "^3.20.6"
"@smithy/node-config-provider" "^4.3.8"
"@smithy/property-provider" "^4.2.8"
"@smithy/protocol-http" "^5.3.8"
"@smithy/signature-v4" "^5.3.8"
"@smithy/smithy-client" "^4.10.8"
"@smithy/types" "^4.12.0"
"@smithy/util-base64" "^4.3.0"
"@smithy/util-middleware" "^4.2.8"
"@smithy/util-utf8" "^4.2.0"
tslib "^2.6.2"
"@aws-sdk/credential-provider-env@3.970.0":
version "3.970.0"
resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.970.0.tgz"
integrity sha512-rtVzXzEtAfZBfh+lq3DAvRar4c3jyptweOAJR2DweyXx71QSMY+O879hjpMwES7jl07a3O1zlnFIDo4KP/96kQ==
dependencies:
"@aws-sdk/core" "3.970.0"
"@aws-sdk/types" "3.969.0"
"@smithy/property-provider" "^4.2.8"
"@smithy/types" "^4.12.0"
tslib "^2.6.2"
"@aws-sdk/credential-provider-http@3.970.0":
version "3.970.0"
resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.970.0.tgz"
integrity sha512-CjDbWL7JxjLc9ZxQilMusWSw05yRvUJKRpz59IxDpWUnSMHC9JMMUUkOy5Izk8UAtzi6gupRWArp4NG4labt9Q==
dependencies:
"@aws-sdk/core" "3.970.0"
"@aws-sdk/types" "3.969.0"
"@smithy/fetch-http-handler" "^5.3.9"
"@smithy/node-http-handler" "^4.4.8"
"@smithy/property-provider" "^4.2.8"
"@smithy/protocol-http" "^5.3.8"
"@smithy/smithy-client" "^4.10.8"
"@smithy/types" "^4.12.0"
"@smithy/util-stream" "^4.5.10"
tslib "^2.6.2"
"@aws-sdk/credential-provider-ini@3.971.0":
version "3.971.0"
resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.971.0.tgz"
integrity sha512-c0TGJG4xyfTZz3SInXfGU8i5iOFRrLmy4Bo7lMyH+IpngohYMYGYl61omXqf2zdwMbDv+YJ9AviQTcCaEUKi8w==
dependencies:
"@aws-sdk/core" "3.970.0"
"@aws-sdk/credential-provider-env" "3.970.0"
"@aws-sdk/credential-provider-http" "3.970.0"
"@aws-sdk/credential-provider-login" "3.971.0"
"@aws-sdk/credential-provider-process" "3.970.0"
"@aws-sdk/credential-provider-sso" "3.971.0"
"@aws-sdk/credential-provider-web-identity" "3.971.0"
"@aws-sdk/nested-clients" "3.971.0"
"@aws-sdk/types" "3.969.0"
"@smithy/credential-provider-imds" "^4.2.8"
"@smithy/property-provider" "^4.2.8"
"@smithy/shared-ini-file-loader" "^4.4.3"
"@smithy/types" "^4.12.0"
tslib "^2.6.2"
"@aws-sdk/credential-provider-login@3.971.0":
version "3.971.0"
resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.971.0.tgz"
integrity sha512-yhbzmDOsk0RXD3rTPhZra4AWVnVAC4nFWbTp+sUty1hrOPurUmhuz8bjpLqYTHGnlMbJp+UqkQONhS2+2LzW2g==
dependencies:
"@aws-sdk/core" "3.970.0"
"@aws-sdk/nested-clients" "3.971.0"
"@aws-sdk/types" "3.969.0"
"@smithy/property-provider" "^4.2.8"
"@smithy/protocol-http" "^5.3.8"
"@smithy/shared-ini-file-loader" "^4.4.3"
"@smithy/types" "^4.12.0"
tslib "^2.6.2"
"@aws-sdk/credential-provider-node@3.971.0":
version "3.971.0"
resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.971.0.tgz"
integrity sha512-epUJBAKivtJqalnEBRsYIULKYV063o/5mXNJshZfyvkAgNIzc27CmmKRXTN4zaNOZg8g/UprFp25BGsi19x3nQ==
dependencies:
"@aws-sdk/credential-provider-env" "3.970.0"
"@aws-sdk/credential-provider-http" "3.970.0"
"@aws-sdk/credential-provider-ini" "3.971.0"
"@aws-sdk/credential-provider-process" "3.970.0"
"@aws-sdk/credential-provider-sso" "3.971.0"
"@aws-sdk/credential-provider-web-identity" "3.971.0"
"@aws-sdk/types" "3.969.0"
"@smithy/credential-provider-imds" "^4.2.8"
"@smithy/property-provider" "^4.2.8"
"@smithy/shared-ini-file-loader" "^4.4.3"
"@smithy/types" "^4.12.0"
tslib "^2.6.2"
"@aws-sdk/credential-provider-process@3.970.0":
version "3.970.0"
resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.970.0.tgz"
integrity sha512-0XeT8OaT9iMA62DFV9+m6mZfJhrD0WNKf4IvsIpj2Z7XbaYfz3CoDDvNoALf3rPY9NzyMHgDxOspmqdvXP00mw==
dependencies:
"@aws-sdk/core" "3.970.0"
"@aws-sdk/types" "3.969.0"
"@smithy/property-provider" "^4.2.8"
"@smithy/shared-ini-file-loader" "^4.4.3"
"@smithy/types" "^4.12.0"
tslib "^2.6.2"
"@aws-sdk/credential-provider-sso@3.971.0":
version "3.971.0"
resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.971.0.tgz"
integrity sha512-dY0hMQ7dLVPQNJ8GyqXADxa9w5wNfmukgQniLxGVn+dMRx3YLViMp5ZpTSQpFhCWNF0oKQrYAI5cHhUJU1hETw==
dependencies:
"@aws-sdk/client-sso" "3.971.0"
"@aws-sdk/core" "3.970.0"
"@aws-sdk/token-providers" "3.971.0"
"@aws-sdk/types" "3.969.0"
"@smithy/property-provider" "^4.2.8"
"@smithy/shared-ini-file-loader" "^4.4.3"
"@smithy/types" "^4.12.0"
tslib "^2.6.2"
"@aws-sdk/credential-provider-web-identity@3.971.0":
version "3.971.0"
resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.971.0.tgz"
integrity sha512-F1AwfNLr7H52T640LNON/h34YDiMuIqW/ZreGzhRR6vnFGaSPtNSKAKB2ssAMkLM8EVg8MjEAYD3NCUiEo+t/w==
dependencies:
"@aws-sdk/core" "3.970.0"
"@aws-sdk/nested-clients" "3.971.0"
"@aws-sdk/types" "3.969.0"
"@smithy/property-provider" "^4.2.8"
"@smithy/shared-ini-file-loader" "^4.4.3"
"@smithy/types" "^4.12.0"
tslib "^2.6.2"
"@aws-sdk/middleware-host-header@3.969.0":
version "3.969.0"
resolved "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.969.0.tgz"
integrity sha512-AWa4rVsAfBR4xqm7pybQ8sUNJYnjyP/bJjfAw34qPuh3M9XrfGbAHG0aiAfQGrBnmS28jlO6Kz69o+c6PRw1dw==
dependencies:
"@aws-sdk/types" "3.969.0"
"@smithy/protocol-http" "^5.3.8"
"@smithy/types" "^4.12.0"
tslib "^2.6.2"
"@aws-sdk/middleware-logger@3.969.0":
version "3.969.0"
resolved "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.969.0.tgz"
integrity sha512-xwrxfip7Y2iTtCMJ+iifN1E1XMOuhxIHY9DreMCvgdl4r7+48x2S1bCYPWH3eNY85/7CapBWdJ8cerpEl12sQQ==
dependencies:
"@aws-sdk/types" "3.969.0"
"@smithy/types" "^4.12.0"
tslib "^2.6.2"
"@aws-sdk/middleware-recursion-detection@3.969.0":
version "3.969.0"
resolved "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.969.0.tgz"
integrity sha512-2r3PuNquU3CcS1Am4vn/KHFwLi8QFjMdA/R+CRDXT4AFO/0qxevF/YStW3gAKntQIgWgQV8ZdEtKAoJvLI4UWg==
dependencies:
"@aws-sdk/types" "3.969.0"
"@aws/lambda-invoke-store" "^0.2.2"
"@smithy/protocol-http" "^5.3.8"
"@smithy/types" "^4.12.0"
tslib "^2.6.2"
"@aws-sdk/middleware-sdk-s3@3.970.0":
version "3.970.0"
resolved "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.970.0.tgz"
integrity sha512-v/Y5F1lbFFY7vMeG5yYxuhnn0CAshz6KMxkz1pDyPxejNE9HtA0w8R6OTBh/bVdIm44QpjhbI7qeLdOE/PLzXQ==
dependencies:
"@aws-sdk/core" "3.970.0"
"@aws-sdk/types" "3.969.0"
"@aws-sdk/util-arn-parser" "3.968.0"
"@smithy/core" "^3.20.6"
"@smithy/node-config-provider" "^4.3.8"
"@smithy/protocol-http" "^5.3.8"
"@smithy/signature-v4" "^5.3.8"
"@smithy/smithy-client" "^4.10.8"
"@smithy/types" "^4.12.0"
"@smithy/util-config-provider" "^4.2.0"
"@smithy/util-middleware" "^4.2.8"
"@smithy/util-stream" "^4.5.10"
"@smithy/util-utf8" "^4.2.0"
tslib "^2.6.2"
"@aws-sdk/middleware-user-agent@3.970.0":
version "3.970.0"
resolved "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.970.0.tgz"
integrity sha512-dnSJGGUGSFGEX2NzvjwSefH+hmZQ347AwbLhAsi0cdnISSge+pcGfOFrJt2XfBIypwFe27chQhlfuf/gWdzpZg==
dependencies:
"@aws-sdk/core" "3.970.0"
"@aws-sdk/types" "3.969.0"
"@aws-sdk/util-endpoints" "3.970.0"
"@smithy/core" "^3.20.6"
"@smithy/protocol-http" "^5.3.8"
"@smithy/types" "^4.12.0"
tslib "^2.6.2"
"@aws-sdk/nested-clients@3.971.0":
version "3.971.0"
resolved "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.971.0.tgz"
integrity sha512-TWaILL8GyYlhGrxxnmbkazM4QsXatwQgoWUvo251FXmUOsiXDFDVX3hoGIfB3CaJhV2pJPfebHUNJtY6TjZ11g==
dependencies:
"@aws-crypto/sha256-browser" "5.2.0"
"@aws-crypto/sha256-js" "5.2.0"
"@aws-sdk/core" "3.970.0"
"@aws-sdk/middleware-host-header" "3.969.0"
"@aws-sdk/middleware-logger" "3.969.0"
"@aws-sdk/middleware-recursion-detection" "3.969.0"
"@aws-sdk/middleware-user-agent" "3.970.0"
"@aws-sdk/region-config-resolver" "3.969.0"
"@aws-sdk/types" "3.969.0"
"@aws-sdk/util-endpoints" "3.970.0"
"@aws-sdk/util-user-agent-browser" "3.969.0"
"@aws-sdk/util-user-agent-node" "3.971.0"
"@smithy/config-resolver" "^4.4.6"
"@smithy/core" "^3.20.6"
"@smithy/fetch-http-handler" "^5.3.9"
"@smithy/hash-node" "^4.2.8"
"@smithy/invalid-dependency" "^4.2.8"
"@smithy/middleware-content-length" "^4.2.8"
"@smithy/middleware-endpoint" "^4.4.7"
"@smithy/middleware-retry" "^4.4.23"
"@smithy/middleware-serde" "^4.2.9"
"@smithy/middleware-stack" "^4.2.8"
"@smithy/node-config-provider" "^4.3.8"
"@smithy/node-http-handler" "^4.4.8"
"@smithy/protocol-http" "^5.3.8"
"@smithy/smithy-client" "^4.10.8"
"@smithy/types" "^4.12.0"
"@smithy/url-parser" "^4.2.8"
"@smithy/util-base64" "^4.3.0"
"@smithy/util-body-length-browser" "^4.2.0"
"@smithy/util-body-length-node" "^4.2.1"
"@smithy/util-defaults-mode-browser" "^4.3.22"
"@smithy/util-defaults-mode-node" "^4.2.25"
"@smithy/util-endpoints" "^3.2.8"
"@smithy/util-middleware" "^4.2.8"
"@smithy/util-retry" "^4.2.8"
"@smithy/util-utf8" "^4.2.0"
tslib "^2.6.2"
"@aws-sdk/region-config-resolver@3.969.0":
version "3.969.0"
resolved "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.969.0.tgz"
integrity sha512-scj9OXqKpcjJ4jsFLtqYWz3IaNvNOQTFFvEY8XMJXTv+3qF5I7/x9SJtKzTRJEBF3spjzBUYPtGFbs9sj4fisQ==
dependencies:
"@aws-sdk/types" "3.969.0"
"@smithy/config-resolver" "^4.4.6"
"@smithy/node-config-provider" "^4.3.8"
"@smithy/types" "^4.12.0"
tslib "^2.6.2"
"@aws-sdk/signature-v4-multi-region@3.970.0":
version "3.970.0"
resolved "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.970.0.tgz"
integrity sha512-z3syXfuK/x/IsKf/AeYmgc2NT7fcJ+3fHaGO+fkghkV9WEba3fPyOwtTBX4KpFMNb2t50zDGZwbzW1/5ighcUQ==
dependencies:
"@aws-sdk/middleware-sdk-s3" "3.970.0"
"@aws-sdk/types" "3.969.0"
"@smithy/protocol-http" "^5.3.8"
"@smithy/signature-v4" "^5.3.8"
"@smithy/types" "^4.12.0"
tslib "^2.6.2"
"@aws-sdk/token-providers@3.971.0":
version "3.971.0"
resolved "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.971.0.tgz"
integrity sha512-4hKGWZbmuDdONMJV0HJ+9jwTDb0zLfKxcCLx2GEnBY31Gt9GeyIQ+DZ97Bb++0voawj6pnZToFikXTyrEq2x+w==
dependencies:
"@aws-sdk/core" "3.970.0"
"@aws-sdk/nested-clients" "3.971.0"
"@aws-sdk/types" "3.969.0"
"@smithy/property-provider" "^4.2.8"
"@smithy/shared-ini-file-loader" "^4.4.3"
"@smithy/types" "^4.12.0"
tslib "^2.6.2"
"@aws-sdk/types@^3.222.0", "@aws-sdk/types@3.969.0":
version "3.969.0"
resolved "https://registry.npmjs.org/@aws-sdk/types/-/types-3.969.0.tgz"
integrity sha512-7IIzM5TdiXn+VtgPdVLjmE6uUBUtnga0f4RiSEI1WW10RPuNvZ9U+pL3SwDiRDAdoGrOF9tSLJOFZmfuwYuVYQ==
dependencies:
"@smithy/types" "^4.12.0"
tslib "^2.6.2"
"@aws-sdk/util-arn-parser@3.968.0":
version "3.968.0"
resolved "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.968.0.tgz"
integrity sha512-gqqvYcitIIM2K4lrDX9de9YvOfXBcVdxfT/iLnvHJd4YHvSXlt+gs+AsL4FfPCxG4IG9A+FyulP9Sb1MEA75vw==
dependencies:
tslib "^2.6.2"
"@aws-sdk/util-endpoints@3.970.0":
version "3.970.0"
resolved "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.970.0.tgz"
integrity sha512-TZNZqFcMUtjvhZoZRtpEGQAdULYiy6rcGiXAbLU7e9LSpIYlRqpLa207oMNfgbzlL2PnHko+eVg8rajDiSOYCg==
dependencies:
"@aws-sdk/types" "3.969.0"
"@smithy/types" "^4.12.0"
"@smithy/url-parser" "^4.2.8"
"@smithy/util-endpoints" "^3.2.8"
tslib "^2.6.2"
"@aws-sdk/util-locate-window@^3.0.0":
version "3.965.2"
resolved "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.2.tgz"
integrity sha512-qKgO7wAYsXzhwCHhdbaKFyxd83Fgs8/1Ka+jjSPrv2Ll7mB55Wbwlo0kkfMLh993/yEc8aoDIAc1Fz9h4Spi4Q==
dependencies:
tslib "^2.6.2"
"@aws-sdk/util-user-agent-browser@3.969.0":
version "3.969.0"
resolved "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.969.0.tgz"
integrity sha512-bpJGjuKmFr0rA6UKUCmN8D19HQFMLXMx5hKBXqBlPFdalMhxJSjcxzX9DbQh0Fn6bJtxCguFmRGOBdQqNOt49g==
dependencies:
"@aws-sdk/types" "3.969.0"
"@smithy/types" "^4.12.0"
bowser "^2.11.0"
tslib "^2.6.2"
"@aws-sdk/util-user-agent-node@3.971.0":
version "3.971.0"
resolved "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.971.0.tgz"
integrity sha512-Eygjo9mFzQYjbGY3MYO6CsIhnTwAMd3WmuFalCykqEmj2r5zf0leWrhPaqvA5P68V5JdGfPYgj7vhNOd6CtRBQ==
dependencies:
"@aws-sdk/middleware-user-agent" "3.970.0"
"@aws-sdk/types" "3.969.0"
"@smithy/node-config-provider" "^4.3.8"
"@smithy/types" "^4.12.0"
tslib "^2.6.2"
"@aws-sdk/xml-builder@3.969.0":
version "3.969.0"
resolved "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.969.0.tgz"
integrity sha512-BSe4Lx/qdRQQdX8cSSI7Et20vqBspzAjBy8ZmXVoyLkol3y4sXBXzn+BiLtR+oh60ExQn6o2DU4QjdOZbXaKIQ==
dependencies:
"@smithy/types" "^4.12.0"
fast-xml-parser "5.2.5"
tslib "^2.6.2"
"@aws/lambda-invoke-store@^0.2.2":
version "0.2.3"
resolved "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.3.tgz"
integrity sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw==
"@babel/code-frame@^7.0.0": "@babel/code-frame@^7.0.0":
version "7.27.1" version "7.27.1"
resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz" resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz"
@@ -1162,409 +686,6 @@
resolved "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.13.0.tgz" resolved "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.13.0.tgz"
integrity sha512-2ih5qGw5SZJ+2fLZxP6Lr6Na2NTIgPRL/7Kmyuw0uIyBQnuhQ8fi8fzUTd38eIQmqp+GYLC00cI6WgtqHxBwmw== integrity sha512-2ih5qGw5SZJ+2fLZxP6Lr6Na2NTIgPRL/7Kmyuw0uIyBQnuhQ8fi8fzUTd38eIQmqp+GYLC00cI6WgtqHxBwmw==
"@smithy/abort-controller@^4.2.8":
version "4.2.8"
resolved "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.8.tgz"
integrity sha512-peuVfkYHAmS5ybKxWcfraK7WBBP0J+rkfUcbHJJKQ4ir3UAUNQI+Y4Vt/PqSzGqgloJ5O1dk7+WzNL8wcCSXbw==
dependencies:
"@smithy/types" "^4.12.0"
tslib "^2.6.2"
"@smithy/config-resolver@^4.4.6":
version "4.4.6"
resolved "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.6.tgz"
integrity sha512-qJpzYC64kaj3S0fueiu3kXm8xPrR3PcXDPEgnaNMRn0EjNSZFoFjvbUp0YUDsRhN1CB90EnHJtbxWKevnH99UQ==
dependencies:
"@smithy/node-config-provider" "^4.3.8"
"@smithy/types" "^4.12.0"
"@smithy/util-config-provider" "^4.2.0"
"@smithy/util-endpoints" "^3.2.8"
"@smithy/util-middleware" "^4.2.8"
tslib "^2.6.2"
"@smithy/core@^3.20.6", "@smithy/core@^3.20.8":
version "3.20.8"
resolved "https://registry.npmjs.org/@smithy/core/-/core-3.20.8.tgz"
integrity sha512-ZBOJENmvM/IrKbxUxRVavCcVCjD5VcPI1MbOURjj3jZgV/t7UaBF8Z+lLSEzoQHH7Lq/efmOhBpV5xCfPjNRZw==
dependencies:
"@smithy/middleware-serde" "^4.2.9"
"@smithy/protocol-http" "^5.3.8"
"@smithy/types" "^4.12.0"
"@smithy/util-base64" "^4.3.0"
"@smithy/util-body-length-browser" "^4.2.0"
"@smithy/util-middleware" "^4.2.8"
"@smithy/util-stream" "^4.5.10"
"@smithy/util-utf8" "^4.2.0"
"@smithy/uuid" "^1.1.0"
tslib "^2.6.2"
"@smithy/credential-provider-imds@^4.2.8":
version "4.2.8"
resolved "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.8.tgz"
integrity sha512-FNT0xHS1c/CPN8upqbMFP83+ul5YgdisfCfkZ86Jh2NSmnqw/AJ6x5pEogVCTVvSm7j9MopRU89bmDelxuDMYw==
dependencies:
"@smithy/node-config-provider" "^4.3.8"
"@smithy/property-provider" "^4.2.8"
"@smithy/types" "^4.12.0"
"@smithy/url-parser" "^4.2.8"
tslib "^2.6.2"
"@smithy/fetch-http-handler@^5.3.9":
version "5.3.9"
resolved "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.9.tgz"
integrity sha512-I4UhmcTYXBrct03rwzQX1Y/iqQlzVQaPxWjCjula++5EmWq9YGBrx6bbGqluGc1f0XEfhSkiY4jhLgbsJUMKRA==
dependencies:
"@smithy/protocol-http" "^5.3.8"
"@smithy/querystring-builder" "^4.2.8"
"@smithy/types" "^4.12.0"
"@smithy/util-base64" "^4.3.0"
tslib "^2.6.2"
"@smithy/hash-node@^4.2.8":
version "4.2.8"
resolved "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.8.tgz"
integrity sha512-7ZIlPbmaDGxVoxErDZnuFG18WekhbA/g2/i97wGj+wUBeS6pcUeAym8u4BXh/75RXWhgIJhyC11hBzig6MljwA==
dependencies:
"@smithy/types" "^4.12.0"
"@smithy/util-buffer-from" "^4.2.0"
"@smithy/util-utf8" "^4.2.0"
tslib "^2.6.2"
"@smithy/invalid-dependency@^4.2.8":
version "4.2.8"
resolved "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.8.tgz"
integrity sha512-N9iozRybwAQ2dn9Fot9kI6/w9vos2oTXLhtK7ovGqwZjlOcxu6XhPlpLpC+INsxktqHinn5gS2DXDjDF2kG5sQ==
dependencies:
"@smithy/types" "^4.12.0"
tslib "^2.6.2"
"@smithy/is-array-buffer@^2.2.0":
version "2.2.0"
resolved "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz"
integrity sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==
dependencies:
tslib "^2.6.2"
"@smithy/is-array-buffer@^4.2.0":
version "4.2.0"
resolved "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.0.tgz"
integrity sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==
dependencies:
tslib "^2.6.2"
"@smithy/middleware-content-length@^4.2.8":
version "4.2.8"
resolved "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.8.tgz"
integrity sha512-RO0jeoaYAB1qBRhfVyq0pMgBoUK34YEJxVxyjOWYZiOKOq2yMZ4MnVXMZCUDenpozHue207+9P5ilTV1zeda0A==
dependencies:
"@smithy/protocol-http" "^5.3.8"
"@smithy/types" "^4.12.0"
tslib "^2.6.2"
"@smithy/middleware-endpoint@^4.4.7", "@smithy/middleware-endpoint@^4.4.9":
version "4.4.9"
resolved "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.9.tgz"
integrity sha512-+2Rxc3zzIT8BhclKEyj/ZzbOh95c55qjKA4XKx5eIGEEGPb4qFwYrNk2NZ1AGuRY71dxmpy4mar+GwurMOLT0w==
dependencies:
"@smithy/core" "^3.20.8"
"@smithy/middleware-serde" "^4.2.9"
"@smithy/node-config-provider" "^4.3.8"
"@smithy/shared-ini-file-loader" "^4.4.3"
"@smithy/types" "^4.12.0"
"@smithy/url-parser" "^4.2.8"
"@smithy/util-middleware" "^4.2.8"
tslib "^2.6.2"
"@smithy/middleware-retry@^4.4.23":
version "4.4.25"
resolved "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.25.tgz"
integrity sha512-NZMnOw4jeNZ4O/bbqUWaLiK3xSpLzIq0QNJhxHZGvtLoywnKPnNJRCfyOE5w82tK3n2L3ZMIC5VtSVW6WvDBkQ==
dependencies:
"@smithy/node-config-provider" "^4.3.8"
"@smithy/protocol-http" "^5.3.8"
"@smithy/service-error-classification" "^4.2.8"
"@smithy/smithy-client" "^4.10.10"
"@smithy/types" "^4.12.0"
"@smithy/util-middleware" "^4.2.8"
"@smithy/util-retry" "^4.2.8"
"@smithy/uuid" "^1.1.0"
tslib "^2.6.2"
"@smithy/middleware-serde@^4.2.9":
version "4.2.9"
resolved "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.9.tgz"
integrity sha512-eMNiej0u/snzDvlqRGSN3Vl0ESn3838+nKyVfF2FKNXFbi4SERYT6PR392D39iczngbqqGG0Jl1DlCnp7tBbXQ==
dependencies:
"@smithy/protocol-http" "^5.3.8"
"@smithy/types" "^4.12.0"
tslib "^2.6.2"
"@smithy/middleware-stack@^4.2.8":
version "4.2.8"
resolved "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.8.tgz"
integrity sha512-w6LCfOviTYQjBctOKSwy6A8FIkQy7ICvglrZFl6Bw4FmcQ1Z420fUtIhxaUZZshRe0VCq4kvDiPiXrPZAe8oRA==
dependencies:
"@smithy/types" "^4.12.0"
tslib "^2.6.2"
"@smithy/node-config-provider@^4.3.8":
version "4.3.8"
resolved "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.8.tgz"
integrity sha512-aFP1ai4lrbVlWjfpAfRSL8KFcnJQYfTl5QxLJXY32vghJrDuFyPZ6LtUL+JEGYiFRG1PfPLHLoxj107ulncLIg==
dependencies:
"@smithy/property-provider" "^4.2.8"
"@smithy/shared-ini-file-loader" "^4.4.3"
"@smithy/types" "^4.12.0"
tslib "^2.6.2"
"@smithy/node-http-handler@^4.4.8":
version "4.4.8"
resolved "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.8.tgz"
integrity sha512-q9u+MSbJVIJ1QmJ4+1u+cERXkrhuILCBDsJUBAW1MPE6sFonbCNaegFuwW9ll8kh5UdyY3jOkoOGlc7BesoLpg==
dependencies:
"@smithy/abort-controller" "^4.2.8"
"@smithy/protocol-http" "^5.3.8"
"@smithy/querystring-builder" "^4.2.8"
"@smithy/types" "^4.12.0"
tslib "^2.6.2"
"@smithy/property-provider@^4.2.8":
version "4.2.8"
resolved "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.8.tgz"
integrity sha512-EtCTbyIveCKeOXDSWSdze3k612yCPq1YbXsbqX3UHhkOSW8zKsM9NOJG5gTIya0vbY2DIaieG8pKo1rITHYL0w==
dependencies:
"@smithy/types" "^4.12.0"
tslib "^2.6.2"
"@smithy/protocol-http@^5.3.8":
version "5.3.8"
resolved "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.8.tgz"
integrity sha512-QNINVDhxpZ5QnP3aviNHQFlRogQZDfYlCkQT+7tJnErPQbDhysondEjhikuANxgMsZrkGeiAxXy4jguEGsDrWQ==
dependencies:
"@smithy/types" "^4.12.0"
tslib "^2.6.2"
"@smithy/querystring-builder@^4.2.8":
version "4.2.8"
resolved "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.8.tgz"
integrity sha512-Xr83r31+DrE8CP3MqPgMJl+pQlLLmOfiEUnoyAlGzzJIrEsbKsPy1hqH0qySaQm4oWrCBlUqRt+idEgunKB+iw==
dependencies:
"@smithy/types" "^4.12.0"
"@smithy/util-uri-escape" "^4.2.0"
tslib "^2.6.2"
"@smithy/querystring-parser@^4.2.8":
version "4.2.8"
resolved "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.8.tgz"
integrity sha512-vUurovluVy50CUlazOiXkPq40KGvGWSdmusa3130MwrR1UNnNgKAlj58wlOe61XSHRpUfIIh6cE0zZ8mzKaDPA==
dependencies:
"@smithy/types" "^4.12.0"
tslib "^2.6.2"
"@smithy/service-error-classification@^4.2.8":
version "4.2.8"
resolved "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.8.tgz"
integrity sha512-mZ5xddodpJhEt3RkCjbmUQuXUOaPNTkbMGR0bcS8FE0bJDLMZlhmpgrvPNCYglVw5rsYTpSnv19womw9WWXKQQ==
dependencies:
"@smithy/types" "^4.12.0"
"@smithy/shared-ini-file-loader@^4.4.3":
version "4.4.3"
resolved "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.3.tgz"
integrity sha512-DfQjxXQnzC5UbCUPeC3Ie8u+rIWZTvuDPAGU/BxzrOGhRvgUanaP68kDZA+jaT3ZI+djOf+4dERGlm9mWfFDrg==
dependencies:
"@smithy/types" "^4.12.0"
tslib "^2.6.2"
"@smithy/signature-v4@^5.3.8":
version "5.3.8"
resolved "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.8.tgz"
integrity sha512-6A4vdGj7qKNRF16UIcO8HhHjKW27thsxYci+5r/uVRkdcBEkOEiY8OMPuydLX4QHSrJqGHPJzPRwwVTqbLZJhg==
dependencies:
"@smithy/is-array-buffer" "^4.2.0"
"@smithy/protocol-http" "^5.3.8"
"@smithy/types" "^4.12.0"
"@smithy/util-hex-encoding" "^4.2.0"
"@smithy/util-middleware" "^4.2.8"
"@smithy/util-uri-escape" "^4.2.0"
"@smithy/util-utf8" "^4.2.0"
tslib "^2.6.2"
"@smithy/smithy-client@^4.10.10", "@smithy/smithy-client@^4.10.8":
version "4.10.10"
resolved "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.10.10.tgz"
integrity sha512-yMT5cB4RdT3x+Etgc99aY/b4jmi++Bmwl7BtLAko+ma7zYZlSiFxbo58XgXouyrP8hDk2/peg55O3AyOm4GtEw==
dependencies:
"@smithy/core" "^3.20.8"
"@smithy/middleware-endpoint" "^4.4.9"
"@smithy/middleware-stack" "^4.2.8"
"@smithy/protocol-http" "^5.3.8"
"@smithy/types" "^4.12.0"
"@smithy/util-stream" "^4.5.10"
tslib "^2.6.2"
"@smithy/types@^4.12.0":
version "4.12.0"
resolved "https://registry.npmjs.org/@smithy/types/-/types-4.12.0.tgz"
integrity sha512-9YcuJVTOBDjg9LWo23Qp0lTQ3D7fQsQtwle0jVfpbUHy9qBwCEgKuVH4FqFB3VYu0nwdHKiEMA+oXz7oV8X1kw==
dependencies:
tslib "^2.6.2"
"@smithy/url-parser@^4.2.8":
version "4.2.8"
resolved "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.8.tgz"
integrity sha512-NQho9U68TGMEU639YkXnVMV3GEFFULmmaWdlu1E9qzyIePOHsoSnagTGSDv1Zi8DCNN6btxOSdgmy5E/hsZwhA==
dependencies:
"@smithy/querystring-parser" "^4.2.8"
"@smithy/types" "^4.12.0"
tslib "^2.6.2"
"@smithy/util-base64@^4.3.0":
version "4.3.0"
resolved "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.0.tgz"
integrity sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ==
dependencies:
"@smithy/util-buffer-from" "^4.2.0"
"@smithy/util-utf8" "^4.2.0"
tslib "^2.6.2"
"@smithy/util-body-length-browser@^4.2.0":
version "4.2.0"
resolved "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.0.tgz"
integrity sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg==
dependencies:
tslib "^2.6.2"
"@smithy/util-body-length-node@^4.2.1":
version "4.2.1"
resolved "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.1.tgz"
integrity sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA==
dependencies:
tslib "^2.6.2"
"@smithy/util-buffer-from@^2.2.0":
version "2.2.0"
resolved "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz"
integrity sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==
dependencies:
"@smithy/is-array-buffer" "^2.2.0"
tslib "^2.6.2"
"@smithy/util-buffer-from@^4.2.0":
version "4.2.0"
resolved "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.0.tgz"
integrity sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==
dependencies:
"@smithy/is-array-buffer" "^4.2.0"
tslib "^2.6.2"
"@smithy/util-config-provider@^4.2.0":
version "4.2.0"
resolved "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.0.tgz"
integrity sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q==
dependencies:
tslib "^2.6.2"
"@smithy/util-defaults-mode-browser@^4.3.22":
version "4.3.24"
resolved "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.24.tgz"
integrity sha512-k2GUY1hGk5JfrlYh7O0PgrS5//IFqbQ3nAldQYfiam+J82h6wmuPkagF+kgF6ldKGHvi7iFpyCb4yXm+P5QD0Q==
dependencies:
"@smithy/property-provider" "^4.2.8"
"@smithy/smithy-client" "^4.10.10"
"@smithy/types" "^4.12.0"
tslib "^2.6.2"
"@smithy/util-defaults-mode-node@^4.2.25":
version "4.2.27"
resolved "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.27.tgz"
integrity sha512-Pam2VH8BSfd9uITrepxYq8QcJKmXVBCq/KCO/3Af/f5zugVioih2rWfTi0+hRzF75DhjgNnwfwdGebItQdO9Mw==
dependencies:
"@smithy/config-resolver" "^4.4.6"
"@smithy/credential-provider-imds" "^4.2.8"
"@smithy/node-config-provider" "^4.3.8"
"@smithy/property-provider" "^4.2.8"
"@smithy/smithy-client" "^4.10.10"
"@smithy/types" "^4.12.0"
tslib "^2.6.2"
"@smithy/util-endpoints@^3.2.8":
version "3.2.8"
resolved "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.2.8.tgz"
integrity sha512-8JaVTn3pBDkhZgHQ8R0epwWt+BqPSLCjdjXXusK1onwJlRuN69fbvSK66aIKKO7SwVFM6x2J2ox5X8pOaWcUEw==
dependencies:
"@smithy/node-config-provider" "^4.3.8"
"@smithy/types" "^4.12.0"
tslib "^2.6.2"
"@smithy/util-hex-encoding@^4.2.0":
version "4.2.0"
resolved "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.0.tgz"
integrity sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw==
dependencies:
tslib "^2.6.2"
"@smithy/util-middleware@^4.2.8":
version "4.2.8"
resolved "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.8.tgz"
integrity sha512-PMqfeJxLcNPMDgvPbbLl/2Vpin+luxqTGPpW3NAQVLbRrFRzTa4rNAASYeIGjRV9Ytuhzny39SpyU04EQreF+A==
dependencies:
"@smithy/types" "^4.12.0"
tslib "^2.6.2"
"@smithy/util-retry@^4.2.8":
version "4.2.8"
resolved "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.8.tgz"
integrity sha512-CfJqwvoRY0kTGe5AkQokpURNCT1u/MkRzMTASWMPPo2hNSnKtF1D45dQl3DE2LKLr4m+PW9mCeBMJr5mCAVThg==
dependencies:
"@smithy/service-error-classification" "^4.2.8"
"@smithy/types" "^4.12.0"
tslib "^2.6.2"
"@smithy/util-stream@^4.5.10":
version "4.5.10"
resolved "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.10.tgz"
integrity sha512-jbqemy51UFSZSp2y0ZmRfckmrzuKww95zT9BYMmuJ8v3altGcqjwoV1tzpOwuHaKrwQrCjIzOib499ymr2f98g==
dependencies:
"@smithy/fetch-http-handler" "^5.3.9"
"@smithy/node-http-handler" "^4.4.8"
"@smithy/types" "^4.12.0"
"@smithy/util-base64" "^4.3.0"
"@smithy/util-buffer-from" "^4.2.0"
"@smithy/util-hex-encoding" "^4.2.0"
"@smithy/util-utf8" "^4.2.0"
tslib "^2.6.2"
"@smithy/util-uri-escape@^4.2.0":
version "4.2.0"
resolved "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.0.tgz"
integrity sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA==
dependencies:
tslib "^2.6.2"
"@smithy/util-utf8@^2.0.0":
version "2.3.0"
resolved "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz"
integrity sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==
dependencies:
"@smithy/util-buffer-from" "^2.2.0"
tslib "^2.6.2"
"@smithy/util-utf8@^4.2.0":
version "4.2.0"
resolved "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.0.tgz"
integrity sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==
dependencies:
"@smithy/util-buffer-from" "^4.2.0"
tslib "^2.6.2"
"@smithy/uuid@^1.1.0":
version "1.1.0"
resolved "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.0.tgz"
integrity sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==
dependencies:
tslib "^2.6.2"
"@swc/helpers@0.5.15": "@swc/helpers@0.5.15":
version "0.5.15" version "0.5.15"
resolved "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz" resolved "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz"
@@ -1616,14 +737,6 @@
dependencies: dependencies:
undici-types "~5.26.4" undici-types "~5.26.4"
"@types/nodemailer@^7.0.5":
version "7.0.5"
resolved "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-7.0.5.tgz"
integrity sha512-7WtR4MFJUNN2UFy0NIowBRJswj5KXjXDhlZY43Hmots5eGu5q/dTeFd/I6GgJA/qj3RqO6dDy4SvfcV3fOVeIA==
dependencies:
"@aws-sdk/client-sesv2" "^3.839.0"
"@types/node" "*"
"@types/normalize-package-data@^2.4.0": "@types/normalize-package-data@^2.4.0":
version "2.4.4" version "2.4.4"
resolved "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz" resolved "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz"
@@ -1845,11 +958,6 @@
resolved "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz" resolved "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz"
integrity sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g== integrity sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==
"@vercel/analytics@^1.1.1":
version "1.5.0"
resolved "https://registry.npmjs.org/@vercel/analytics/-/analytics-1.5.0.tgz"
integrity sha512-MYsBzfPki4gthY5HnYN7jgInhAZ7Ac1cYDoRWFomwGHWEX7odTEzbtg9kf/QSo7XEsEAqlQugA6gJ2WS2DEa3g==
abort-controller@^3.0.0: abort-controller@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz" resolved "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz"
@@ -2168,11 +1276,6 @@ binary-extensions@^2.0.0:
resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz" resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz"
integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==
bowser@^2.11.0:
version "2.13.1"
resolved "https://registry.npmjs.org/bowser/-/bowser-2.13.1.tgz"
integrity sha512-OHawaAbjwx6rqICCKgSG0SAnT05bzd7ppyKLVUITZpANBaaMFBAsaNkto3LoQ31tyFP5kNujE8Cdx85G9VzOkw==
brace-expansion@^1.1.7: brace-expansion@^1.1.7:
version "1.1.12" version "1.1.12"
resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz" resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz"
@@ -3108,13 +2211,6 @@ fast-uri@^3.0.1:
resolved "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz" resolved "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz"
integrity sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA== integrity sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==
fast-xml-parser@5.2.5:
version "5.2.5"
resolved "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz"
integrity sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==
dependencies:
strnum "^2.1.0"
fastq@^1.6.0: fastq@^1.6.0:
version "1.19.1" version "1.19.1"
resolved "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz" resolved "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz"
@@ -4363,7 +3459,7 @@ natural-compare@^1.4.0:
resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz" resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz"
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
next@^15.5.9, "next@>= 13": next@^15.5.9:
version "15.5.9" version "15.5.9"
resolved "https://registry.npmjs.org/next/-/next-15.5.9.tgz" resolved "https://registry.npmjs.org/next/-/next-15.5.9.tgz"
integrity sha512-agNLK89seZEtC5zUHwtut0+tNrc0Xw4FT/Dg+B/VLEo9pAcS9rtTKpek3V6kVcVwsB2YlqMaHdfZL4eLEVYuCg== integrity sha512-agNLK89seZEtC5zUHwtut0+tNrc0Xw4FT/Dg+B/VLEo9pAcS9rtTKpek3V6kVcVwsB2YlqMaHdfZL4eLEVYuCg==
@@ -4401,11 +3497,6 @@ node-releases@^2.0.21:
resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.23.tgz" resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.23.tgz"
integrity sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg== integrity sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==
nodemailer@^7.0.12:
version "7.0.12"
resolved "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.12.tgz"
integrity sha512-H+rnK5bX2Pi/6ms3sN4/jRQvYSMltV6vqup/0SFOrxYYY/qoNvhXPlYq3e+Pm9RFJRwrMGbMIwi81M4dxpomhA==
normalize-package-data@^2.5.0: normalize-package-data@^2.5.0:
version "2.5.0" version "2.5.0"
resolved "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz" resolved "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz"
@@ -4871,7 +3962,7 @@ react-is@^16.13.1:
resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz" resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
"react@^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc", "react@^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react@^16.8.0 || ^17 || ^18 || ^19", react@^18, "react@^18 || ^19 || ^19.0.0-rc", "react@^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", react@^18.3.1, "react@>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0": "react@^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc", "react@^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react@^16.8.0 || ^17 || ^18 || ^19", react@^18, "react@^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", react@^18.3.1, "react@>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0":
version "18.3.1" version "18.3.1"
resolved "https://registry.npmjs.org/react/-/react-18.3.1.tgz" resolved "https://registry.npmjs.org/react/-/react-18.3.1.tgz"
integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ== integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==
@@ -5536,11 +4627,6 @@ strip-json-comments@^3.1.1:
resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz" resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz"
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
strnum@^2.1.0:
version "2.1.2"
resolved "https://registry.npmjs.org/strnum/-/strnum-2.1.2.tgz"
integrity sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ==
styled-jsx@5.1.6: styled-jsx@5.1.6:
version "5.1.6" version "5.1.6"
resolved "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz" resolved "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz"
@@ -5728,7 +4814,7 @@ tsconfig-paths@^3.15.0:
minimist "^1.2.6" minimist "^1.2.6"
strip-bom "^3.0.0" strip-bom "^3.0.0"
tslib@^2.4.0, tslib@^2.6.2, tslib@^2.8.0: tslib@^2.4.0, tslib@^2.8.0:
version "2.8.1" version "2.8.1"
resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz" resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz"
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==