From 08e7d97cd2988db20e05f91506b98a24067c6703 Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Fri, 17 Jul 2026 20:47:37 -0400 Subject: [PATCH 01/23] Add local email testing --- backend/.env.example | 10 ++++++++-- backend/api/settings.py | 22 ++++++++++++++++------ 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/backend/.env.example b/backend/.env.example index bc5e29484..9633e2903 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -6,12 +6,18 @@ DB_PASSWORD=password DB_HOST=hostname DB_PORT=port -# Email server configuration with AWS SES, can be left as placeholders if not sending emails +# Email configuration +DEFAULT_FROM_EMAIL=email +# AWS SES for production, can be left as placeholders in development AWS_SES_ACCESS_KEY_ID=access_key_id AWS_SES_SECRET_ACCESS_KEY=secret_access_key AWS_SES_REGION_NAME=region_name AWS_SES_REGION_ENDPOINT=region_endpoint -DEFAULT_FROM_EMAIL=email +# SMTP for development, can also be left as placeholders if SEND_EMAILS is False +SMTP_HOST=host +SMTP_PORT=port +SMTP_HOST_USER=user +SMTP_HOST_PASSWORD=password # The URL of the main site, used for generating links (no trailing slash) BASE_URL=https://example.com diff --git a/backend/api/settings.py b/backend/api/settings.py index c15ebe4c2..d2360672d 100644 --- a/backend/api/settings.py +++ b/backend/api/settings.py @@ -148,12 +148,22 @@ class ThrottleScopes: {"error": {"general": ["An unknown error has occurred."]}}, status=500 ) -# AWS SES Credentials -EMAIL_BACKEND = "django_ses.SESBackend" -AWS_SES_ACCESS_KEY_ID = env("AWS_SES_ACCESS_KEY_ID") -AWS_SES_SECRET_ACCESS_KEY = env("AWS_SES_SECRET_ACCESS_KEY") -AWS_SES_REGION_NAME = env("AWS_SES_REGION_NAME") -AWS_SES_REGION_ENDPOINT = env("AWS_SES_REGION_ENDPOINT") +# Email settings +if DEBUG: + # For use with something like Mailpit + EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" + EMAIL_HOST = env("SMTP_HOST") + EMAIL_PORT = env.int("SMTP_PORT") + EMAIL_USE_TLS = False + EMAIL_HOST_USER = env("SMTP_HOST_USER") + EMAIL_HOST_PASSWORD = env("SMTP_HOST_PASSWORD") +else: + # For production, uses AWS SES + EMAIL_BACKEND = "django_ses.SESBackend" + AWS_SES_ACCESS_KEY_ID = env("AWS_SES_ACCESS_KEY_ID") + AWS_SES_SECRET_ACCESS_KEY = env("AWS_SES_SECRET_ACCESS_KEY") + AWS_SES_REGION_NAME = env("AWS_SES_REGION_NAME") + AWS_SES_REGION_ENDPOINT = env("AWS_SES_REGION_ENDPOINT") DEFAULT_FROM_EMAIL = env("DEFAULT_FROM_EMAIL") ADMIN_EMAILS = env.list("ADMIN_EMAILS", default=[]) SEND_EMAILS = env.bool("SEND_EMAILS", default=False) From c8c5fdaadab88ff65e5df2c2a4528a27e1c18f05 Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Fri, 17 Jul 2026 20:56:56 -0400 Subject: [PATCH 02/23] Add EMAIL_BRIDGE_SECRET to .env files --- backend/.env.example | 4 ++++ backend/api/settings.py | 1 + frontend/.env.example | 7 ++++++- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/backend/.env.example b/backend/.env.example index 9633e2903..e99011143 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -18,6 +18,10 @@ SMTP_HOST=host SMTP_PORT=port SMTP_HOST_USER=user SMTP_HOST_PASSWORD=password +# Secret key for authorization into the Next.js email template renderer +# Must be the same as the one in the frontend .env file +# Can be left as a placeholder for development +EMAIL_BRIDGE_SECRET=secret # The URL of the main site, used for generating links (no trailing slash) BASE_URL=https://example.com diff --git a/backend/api/settings.py b/backend/api/settings.py index d2360672d..e7d298257 100644 --- a/backend/api/settings.py +++ b/backend/api/settings.py @@ -165,6 +165,7 @@ class ThrottleScopes: AWS_SES_REGION_NAME = env("AWS_SES_REGION_NAME") AWS_SES_REGION_ENDPOINT = env("AWS_SES_REGION_ENDPOINT") DEFAULT_FROM_EMAIL = env("DEFAULT_FROM_EMAIL") +EMAIL_BRIDGE_SECRET = env("EMAIL_BRIDGE_SECRET") ADMIN_EMAILS = env.list("ADMIN_EMAILS", default=[]) SEND_EMAILS = env.bool("SEND_EMAILS", default=False) CRITICAL_EMAIL_INTERVAL_SECONDS = 1800 # 30 minutes diff --git a/frontend/.env.example b/frontend/.env.example index b6b2ea8fa..7a67132fe 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -6,4 +6,9 @@ NEXT_PUBLIC_DEBUG=true NEXT_PUBLIC_API_URL=https://example.com # The URL for the feedback form -NEXT_PUBLIC_FEEDBACK_FORM_URL=https://forms.example.com/feedback \ No newline at end of file +NEXT_PUBLIC_FEEDBACK_FORM_URL=https://forms.example.com/feedback + +# Secret key for authorization into the email template renderer +# Must be the same as the one in the backend .env file +# Can be left as a placeholder for development +EMAIL_BRIDGE_SECRET=secret From 4766054311ac1d5966c1ef33b47506bf7b96d361 Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Mon, 20 Jul 2026 02:06:10 -0400 Subject: [PATCH 03/23] Establish basic email rendering logic --- frontend/.env.example | 3 + frontend/package-lock.json | 1648 ++++++++++++++++- frontend/package.json | 1 + frontend/src/app/api/render-email/route.tsx | 26 + frontend/src/features/email/lib/notes.ts | 5 + frontend/src/features/email/lib/render.tsx | 70 + .../features/email/templates/email-in-use.tsx | 32 + frontend/src/features/email/theme.ts | 13 + frontend/src/features/email/type.ts | 5 + 9 files changed, 1744 insertions(+), 59 deletions(-) create mode 100644 frontend/src/app/api/render-email/route.tsx create mode 100644 frontend/src/features/email/lib/notes.ts create mode 100644 frontend/src/features/email/lib/render.tsx create mode 100644 frontend/src/features/email/templates/email-in-use.tsx create mode 100644 frontend/src/features/email/theme.ts create mode 100644 frontend/src/features/email/type.ts diff --git a/frontend/.env.example b/frontend/.env.example index 7a67132fe..8eaaf7dea 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -1,6 +1,9 @@ # Enable debug mode, important to have this false in production for security NEXT_PUBLIC_DEBUG=true +# The base URL for the frontend, used for generating links in emails +BASE_URL=http://localhost:3000 + # The API URL for both the client and the Next.js server to communicate with the backend # Note the lack of a trailing slash NEXT_PUBLIC_API_URL=https://example.com diff --git a/frontend/package-lock.json b/frontend/package-lock.json index c4f8d7778..b93bab6f5 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -32,6 +32,7 @@ "react": "^19.0.0", "react-day-picker": "^9.6.7", "react-dom": "^19.0.0", + "react-email": "^6.9.0", "react-select": "^5.10.2", "react-timezone-select": "^3.2.8", "tailwind-merge": "^3.2.0", @@ -68,11 +69,12 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", + "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -81,12 +83,13 @@ } }, "node_modules/@babel/generator": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", - "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.3", - "@babel/types": "^7.28.2", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -95,6 +98,21 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/generator/node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@babel/helper-globals": { "version": "7.28.0", "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", @@ -116,27 +134,30 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", - "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "license": "MIT", "dependencies": { - "@babel/types": "^7.28.4" + "@babel/types": "^7.29.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -154,29 +175,46 @@ } }, "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, + "node_modules/@babel/template/node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@babel/traverse": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", - "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.3", + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.4", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", "debug": "^4.3.1" }, "engines": { @@ -184,12 +222,13 @@ } }, "node_modules/@babel/types": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", - "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -329,6 +368,422 @@ "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==" }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", @@ -994,6 +1449,7 @@ "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" @@ -1003,6 +1459,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", "engines": { "node": ">=6.0.0" } @@ -1010,12 +1467,14 @@ "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==" + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -3607,6 +4066,25 @@ "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==" }, + "node_modules/@react-email/render": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@react-email/render/-/render-2.1.0.tgz", + "integrity": "sha512-F+zE3O6d6sW6Aj2UjvZAA17R7tJKM7kcq2mgV6k4HCT8jeLLFaVP2txMtH1lgqYFRMZ0Gxsd37q2PRyiXLXXxA==", + "license": "MIT", + "dependencies": { + "entities": "^4.5.0", + "html-to-text": "^9.0.5", + "html5parser": "^3.0.0", + "prettier": "^3.5.3" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -3618,6 +4096,25 @@ "integrity": "sha512-zxnHvoMQVqewTJr/W4pKjF0bMGiKJv1WX7bSrkl46Hg0QjESbzBROWK0Wg4RphzSOS5Jiy7eFimmM3UgMrMZbQ==", "dev": true }, + "node_modules/@selderee/plugin-htmlparser2": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz", + "integrity": "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==", + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "selderee": "^0.11.0" + }, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -3889,6 +4386,15 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", @@ -3908,7 +4414,6 @@ "version": "20.17.32", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.32.tgz", "integrity": "sha512-zeMXFn8zQ+UkjK4ws0RiOC9EWByyW1CcVmLe+2rQocXRsGEDxUCwPEIVgpsGcLHS/P8JkT0oa3839BRABS0oPw==", - "dev": true, "dependencies": { "undici-types": "~6.19.2" } @@ -3943,6 +4448,15 @@ "@types/react": "*" } }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.31.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.31.1.tgz", @@ -4445,6 +4959,40 @@ "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", "license": "BSD-2-Clause" }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -4461,24 +5009,63 @@ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } } }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" }, "funding": { "type": "github", "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -4687,6 +5274,16 @@ "node": ">= 0.4" } }, + "node_modules/atomically": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/atomically/-/atomically-2.1.1.tgz", + "integrity": "sha512-P4w9o2dqARji6P7MHprklbfiArZAWvo07yW7qs3pdljb3BWr12FIB7W+p0zJiuiVsUpRO0iZn1kFFcpPegg0tQ==", + "license": "MIT", + "dependencies": { + "stubborn-fs": "^2.0.0", + "when-exit": "^2.1.4" + } + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -4738,6 +5335,15 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -4844,6 +5450,21 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/ci-info": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", @@ -4859,6 +5480,12 @@ "node": ">=8" } }, + "node_modules/citty": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.2.tgz", + "integrity": "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==", + "license": "MIT" + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -4888,16 +5515,96 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, + "node_modules/commander": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, + "node_modules/conf": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/conf/-/conf-15.1.0.tgz", + "integrity": "sha512-Uy5YN9KEu0WWDaZAVJ5FAmZoaJt9rdK6kH+utItPyGsCqCgaTKkrmZx3zoE0/3q6S3bcp3Ihkk+ZqPxWxFK5og==", + "license": "MIT", + "dependencies": { + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "atomically": "^2.0.3", + "debounce-fn": "^6.0.0", + "dot-prop": "^10.0.0", + "env-paths": "^3.0.0", + "json-schema-typed": "^8.0.1", + "semver": "^7.7.2", + "uint8array-extras": "^1.5.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/conf/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/conf/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, "node_modules/convert-source-map": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/cosmiconfig": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", @@ -4926,6 +5633,19 @@ "node": ">= 8" } }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", @@ -5007,10 +5727,38 @@ "date-fns": "^3.0.0 || ^4.0.0" } }, + "node_modules/debounce": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-2.2.0.tgz", + "integrity": "sha512-Xks6RUDLZFdz8LIdR6q0MTH44k7FikOmnh5xkSjMig6ch45afc8sjTjRQf3P6ax8dMgcQrYO/AR2RGWURrruqw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/debounce-fn": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/debounce-fn/-/debounce-fn-6.0.0.tgz", + "integrity": "sha512-rBMW+F2TXryBwB54Q0d8drNEI+TfoS9JpNTAoVpukbWEhjXQq4rySFYLaqXMFXwdv61Zb2OHtj5bviSoimqxRQ==", + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", "dependencies": { "ms": "^2.1.3" }, @@ -5028,6 +5776,15 @@ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -5095,6 +5852,91 @@ "csstype": "^3.0.2" } }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dot-prop": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-10.1.0.tgz", + "integrity": "sha512-MVUtAugQMOff5RnBy2d9N31iG0lNwg1qAoAOn7pOK5wf94WIaE3My2p3uwTQuvS2AcqchkcR3bHByjaM0mmi7Q==", + "license": "MIT", + "dependencies": { + "type-fest": "^5.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dot-prop/node_modules/type-fest": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.8.0.tgz", + "integrity": "sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA==", + "license": "(MIT OR CC0-1.0)", + "dependencies": { + "tagged-tag": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -5114,6 +5956,36 @@ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true }, + "node_modules/engine.io": { + "version": "6.6.9", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.9.tgz", + "integrity": "sha512-clKkw4C7nJ22mGgoVcCg6V/W/TxdNyIOTr89k2ONZu81qqkddPFDF0LXcbAwhzPD8DjkiRCjzuiO6Y+fkpD4vg==", + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "@types/ws": "^8.5.12", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.21.0" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.18.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", @@ -5127,6 +5999,30 @@ "node": ">=10.13.0" } }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", + "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/error-ex": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", @@ -5297,6 +6193,47 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -5764,6 +6701,22 @@ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" }, + "node_modules/fast-uri": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz", + "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/fastq": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", @@ -6176,6 +7129,47 @@ "react-is": "^16.7.0" } }, + "node_modules/html-to-text": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-9.0.5.tgz", + "integrity": "sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==", + "license": "MIT", + "dependencies": { + "@selderee/plugin-htmlparser2": "^0.11.0", + "deepmerge": "^4.3.1", + "dom-serializer": "^2.0.0", + "htmlparser2": "^8.0.2", + "selderee": "^0.11.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/html5parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html5parser/-/html5parser-3.0.0.tgz", + "integrity": "sha512-iNpSopa+4YHX50UOk825tBy7MghmXHo/ZpLskBYN0kAr1xhH8GlIMk5bLRXcZlfP3AnLUcSuFMu8C4MdOUxA8A==", + "license": "MIT" + }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -6576,6 +7570,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-weakmap": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", @@ -6656,10 +7662,10 @@ } }, "node_modules/jiti": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", - "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", - "devOptional": true, + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" } @@ -6693,6 +7699,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", "bin": { "jsesc": "bin/jsesc" }, @@ -6715,6 +7722,12 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, "node_modules/json-stable-stringify": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.3.0.tgz", @@ -6803,6 +7816,15 @@ "graceful-fs": "^4.1.11" } }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/language-subtag-registry": { "version": "0.3.23", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", @@ -6821,6 +7843,15 @@ "node": ">=0.10" } }, + "node_modules/leac": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/leac/-/leac-0.6.0.tgz", + "integrity": "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==", + "license": "MIT", + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -7091,6 +8122,22 @@ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" }, + "node_modules/log-symbols": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", + "integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==", + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -7102,6 +8149,15 @@ "loose-envify": "cli.js" } }, + "node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/lucide-react": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.3.0.tgz", @@ -7111,6 +8167,18 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/marked": { + "version": "15.0.12", + "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", + "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -7119,6 +8187,12 @@ "node": ">= 0.4" } }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "license": "CC0-1.0" + }, "node_modules/memoize-one": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", @@ -7145,6 +8219,43 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -7164,6 +8275,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/motion-dom": { "version": "12.27.0", "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.27.0.tgz", @@ -7221,6 +8341,15 @@ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/next": { "version": "15.4.10", "resolved": "https://registry.npmjs.org/next/-/next-15.4.10.tgz", @@ -7309,6 +8438,32 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nypm": { + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.6.tgz", + "integrity": "sha512-vRyr0r4cbBapw07Xw8xrj9Teq3o7MUD35rSaTcanDbW+aK2XHDgJFiU6ZTj2GBw7Q12ysdsyFss+Vdz4hQ0Y6Q==", + "license": "MIT", + "dependencies": { + "citty": "^0.2.2", + "pathe": "^2.0.3", + "tinyexec": "^1.1.1" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -7530,6 +8685,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parseley": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/parseley/-/parseley-0.12.1.tgz", + "integrity": "sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==", + "license": "MIT", + "dependencies": { + "leac": "^0.6.0", + "peberminta": "^0.9.0" + }, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, "node_modules/patch-package": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.1.tgz", @@ -7604,6 +8772,22 @@ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -7612,6 +8796,21 @@ "node": ">=8" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "license": "MIT" + }, + "node_modules/peberminta": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/peberminta/-/peberminta-0.9.0.tgz", + "integrity": "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==", + "license": "MIT", + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -7628,6 +8827,15 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/picospinner": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/picospinner/-/picospinner-3.0.0.tgz", + "integrity": "sha512-lGA1TNsmy2bxvRsTI2cV01kfTwKzZjnZSDmF9llYNyMHMrU4sP87lQ5taiIKm88L3cbswjl008nwyGc3WpNvzg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -7676,7 +8884,6 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", - "dev": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -7773,6 +8980,28 @@ } } }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -7874,6 +9103,131 @@ "react": "^19.1.0" } }, + "node_modules/react-email": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/react-email/-/react-email-6.9.0.tgz", + "integrity": "sha512-72jV+VkeeXgNWDycNDn2tIlHFLg4Hevi3pC77g63FAY1+bDgTs4b56jbZfHF1t5d+GVGb02sGS8bOwoptgvI1w==", + "license": "MIT", + "dependencies": { + "@babel/parser": "7.29.2", + "@babel/traverse": "7.29.0", + "@react-email/render": ">=2.1.0", + "chokidar": "^4.0.3", + "commander": "^13.0.0", + "conf": "^15.0.2", + "css-tree": "3.2.1", + "debounce": "^2.0.0", + "esbuild": "^0.28.0", + "glob": "^13.0.6", + "jiti": "2.6.1", + "log-symbols": "^7.0.0", + "marked": "^15.0.12", + "mime-types": "^3.0.0", + "normalize-path": "^3.0.0", + "nypm": "0.6.6", + "picospinner": "^3.0.0", + "prismjs": "^1.30.0", + "prompts": "2.4.2", + "socket.io": "^4.8.1", + "tailwindcss": "^4.1.18", + "tsconfig-paths": "4.2.0" + }, + "bin": { + "email": "dist/cli/index.mjs" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/react-email/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/react-email/node_modules/brace-expansion": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/react-email/node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/react-email/node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/react-email/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/react-email/node_modules/tailwindcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", + "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", + "license": "MIT" + }, + "node_modules/react-email/node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "license": "MIT", + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -7994,6 +9348,19 @@ "react-dom": ">=16.6.0" } }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -8034,6 +9401,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -8173,6 +9549,18 @@ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==" }, + "node_modules/selderee": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/selderee/-/selderee-0.11.0.tgz", + "integrity": "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==", + "license": "MIT", + "dependencies": { + "parseley": "^0.12.0" + }, + "funding": { + "url": "https://ko-fi.com/killymxi" + } + }, "node_modules/semver": { "version": "7.7.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", @@ -8358,6 +9746,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" + }, "node_modules/slash": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", @@ -8367,6 +9761,47 @@ "node": ">=6" } }, + "node_modules/socket.io": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.3.tgz", + "integrity": "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.8", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.8.tgz", + "integrity": "sha512-6Oy52pbg+kvdCVvjcN+FnY7BvxZ7cIHNScbvztT/It5d0vbwoJoVZmF2gjJmnV0/4WlXRfG15zc45ySk9Ah8bw==", + "license": "MIT", + "dependencies": { + "debug": "~4.4.1", + "ws": "~8.21.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.7.tgz", + "integrity": "sha512-IH/iSeO9T6gz1KkFleGDWkG9N3dl4jXVYUtMhIqH10Md0ttMer8nUNWiP1DKuNrybD2xBrixLJdCC9J6ECoYkg==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -8541,6 +9976,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/stubborn-fs": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/stubborn-fs/-/stubborn-fs-2.0.0.tgz", + "integrity": "sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA==", + "license": "MIT", + "dependencies": { + "stubborn-utils": "^1.0.1" + } + }, + "node_modules/stubborn-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stubborn-utils/-/stubborn-utils-1.0.2.tgz", + "integrity": "sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg==", + "license": "MIT" + }, "node_modules/styled-jsx": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", @@ -8590,6 +10040,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tagged-tag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", + "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/tailwind-merge": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.2.0.tgz", @@ -8625,6 +10087,15 @@ "resolved": "https://registry.npmjs.org/timezone-soft/-/timezone-soft-1.5.2.tgz", "integrity": "sha512-BUr+CfBfeWXJwFAuEzPO9uF+v6sy3pL5SKLkDg4vdEhsyXgbBnpFoBCW8oEKSNTqNq9YHbVOjNb31xE7WyGmrA==" }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", @@ -8821,6 +10292,18 @@ "node": ">=14.17" } }, + "node_modules/uint8array-extras": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", @@ -8841,8 +10324,7 @@ "node_modules/undici-types": { "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", - "dev": true + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" }, "node_modules/universalify": { "version": "2.0.1", @@ -8974,6 +10456,15 @@ "integrity": "sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ==", "dev": true }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/vaul": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.2.tgz", @@ -8987,6 +10478,12 @@ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, + "node_modules/when-exit": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/when-exit/-/when-exit-2.1.5.tgz", + "integrity": "sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg==", + "license": "MIT" + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -9096,6 +10593,27 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, + "node_modules/ws": { + "version": "8.21.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz", + "integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/yaml": { "version": "1.10.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", @@ -9114,6 +10632,18 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/yoctocolors": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", + "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/frontend/package.json b/frontend/package.json index 14a5a9d52..1129eade9 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -36,6 +36,7 @@ "react": "^19.0.0", "react-day-picker": "^9.6.7", "react-dom": "^19.0.0", + "react-email": "^6.9.0", "react-select": "^5.10.2", "react-timezone-select": "^3.2.8", "tailwind-merge": "^3.2.0", diff --git a/frontend/src/app/api/render-email/route.tsx b/frontend/src/app/api/render-email/route.tsx new file mode 100644 index 000000000..be27c8dff --- /dev/null +++ b/frontend/src/app/api/render-email/route.tsx @@ -0,0 +1,26 @@ +import { emailInUse } from "@/features/email/templates/email-in-use"; +import { EmailRenderResponse } from "@/features/email/type"; + +export async function POST(req: Request) { + const authHeader = req.headers.get("Authorization"); + if ( + !authHeader || + authHeader !== `Bearer ${process.env.EMAIL_BRIDGE_SECRET}` + ) { + return new Response("Forbidden", { status: 403 }); + } + + const { template_key: templateKey, context } = await req.json(); + + let data: EmailRenderResponse; + + switch (templateKey) { + case "email_in_use": + data = await emailInUse(); + break; + default: + return new Response("Template not found", { status: 404 }); + } + + return Response.json(data); +} diff --git a/frontend/src/features/email/lib/notes.ts b/frontend/src/features/email/lib/notes.ts new file mode 100644 index 000000000..e0969b662 --- /dev/null +++ b/frontend/src/features/email/lib/notes.ts @@ -0,0 +1,5 @@ +const NOTES = { + IGNORE_EMAIL: "Not you? You can ignore this email.", +} as const; + +export default NOTES; diff --git a/frontend/src/features/email/lib/render.tsx b/frontend/src/features/email/lib/render.tsx new file mode 100644 index 000000000..79f0f3b85 --- /dev/null +++ b/frontend/src/features/email/lib/render.tsx @@ -0,0 +1,70 @@ +import { + Body, + Container, + Font, + Head, + Html, + Preview, + render, + Tailwind, + Text, +} from "react-email"; + +import { emailTheme } from "@/features/email/theme"; + +export default function renderEmail({ + previewText, + title, + content, + footerText, +}: { + previewText: string; + title: string; + content: React.ReactNode; + footerText?: string; +}) { + return render( + + + + + + + + + {previewText} + + + + {title} + {content} + {footerText && ( + {footerText} + )} + + + + , + ); +} + +function NunitoFont({ url, weight }: { url: string; weight: number }) { + return ( + + ); +} diff --git a/frontend/src/features/email/templates/email-in-use.tsx b/frontend/src/features/email/templates/email-in-use.tsx new file mode 100644 index 000000000..4dca484d4 --- /dev/null +++ b/frontend/src/features/email/templates/email-in-use.tsx @@ -0,0 +1,32 @@ +import { Link, Text } from "react-email"; + +import NOTES from "@/features/email/lib/notes"; +import renderEmail from "@/features/email/lib/render"; +import { EmailRenderResponse } from "@/features/email/type"; + +const CONTENT_TEXT = + "You just tried to register a Plancake account using this email, but it looks like it was already used for one."; +const LOGIN_URL = `${process.env.BASE_URL}/login`; + +export async function emailInUse(): Promise { + return { + subject: "Plancake - Email in Use", + html: await renderEmail({ + previewText: "Your email is already associated with an account.", + title: "Email in Use", + content: ( + <> + + {CONTENT_TEXT} Log in{" "} + + here + {" "} + instead. + + + ), + footerText: NOTES.IGNORE_EMAIL, + }), + text: `${CONTENT_TEXT} Log in at ${LOGIN_URL} instead.\n\n${NOTES.IGNORE_EMAIL}`, + }; +} diff --git a/frontend/src/features/email/theme.ts b/frontend/src/features/email/theme.ts new file mode 100644 index 000000000..e78a3cb67 --- /dev/null +++ b/frontend/src/features/email/theme.ts @@ -0,0 +1,13 @@ +export const emailTheme = { + theme: { + extend: { + colors: { + // These values are the oklch values from tailwind.css converted to hex + background: "#f5f5f5", + foreground: "#3e3c53", + accent: "#3d73a3", + panel: "#ffffff", + }, + }, + }, +}; diff --git a/frontend/src/features/email/type.ts b/frontend/src/features/email/type.ts new file mode 100644 index 000000000..a09ca18f8 --- /dev/null +++ b/frontend/src/features/email/type.ts @@ -0,0 +1,5 @@ +export type EmailRenderResponse = { + subject: `Plancake - ${string}`; + html: string; + text: string; +}; From 6cb9620ab076998daf02a445cb14ee658de60197 Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Mon, 20 Jul 2026 12:55:52 -0400 Subject: [PATCH 04/23] Remove dark mode support --- frontend/src/features/email/lib/render.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/features/email/lib/render.tsx b/frontend/src/features/email/lib/render.tsx index 79f0f3b85..f1c3a6b5e 100644 --- a/frontend/src/features/email/lib/render.tsx +++ b/frontend/src/features/email/lib/render.tsx @@ -27,8 +27,8 @@ export default function renderEmail({ - - + + Date: Mon, 20 Jul 2026 12:56:24 -0400 Subject: [PATCH 05/23] Center text content --- frontend/src/features/email/lib/render.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/features/email/lib/render.tsx b/frontend/src/features/email/lib/render.tsx index f1c3a6b5e..38d988baa 100644 --- a/frontend/src/features/email/lib/render.tsx +++ b/frontend/src/features/email/lib/render.tsx @@ -41,8 +41,8 @@ export default function renderEmail({ {previewText} - - {title} + + {title} {content} {footerText && ( {footerText} From 3a2d73c7a3d8d0e31d05db9426b9b7aace90665f Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Mon, 20 Jul 2026 12:58:39 -0400 Subject: [PATCH 06/23] Add email verification message with error handling --- frontend/src/app/api/render-email/route.tsx | 28 ++++++++++---- .../src/features/email/components/button.tsx | 17 +++++++++ .../email/templates/email-verification.tsx | 37 +++++++++++++++++++ frontend/src/features/email/type.ts | 2 + 4 files changed, 77 insertions(+), 7 deletions(-) create mode 100644 frontend/src/features/email/components/button.tsx create mode 100644 frontend/src/features/email/templates/email-verification.tsx diff --git a/frontend/src/app/api/render-email/route.tsx b/frontend/src/app/api/render-email/route.tsx index be27c8dff..ed61258ae 100644 --- a/frontend/src/app/api/render-email/route.tsx +++ b/frontend/src/app/api/render-email/route.tsx @@ -1,5 +1,6 @@ import { emailInUse } from "@/features/email/templates/email-in-use"; -import { EmailRenderResponse } from "@/features/email/type"; +import { emailVerification } from "@/features/email/templates/email-verification"; +import { EmailContextError, EmailRenderResponse } from "@/features/email/type"; export async function POST(req: Request) { const authHeader = req.headers.get("Authorization"); @@ -14,12 +15,25 @@ export async function POST(req: Request) { let data: EmailRenderResponse; - switch (templateKey) { - case "email_in_use": - data = await emailInUse(); - break; - default: - return new Response("Template not found", { status: 404 }); + try { + switch (templateKey) { + case "email_in_use": + data = await emailInUse(); + break; + case "email_verification": + data = await emailVerification(context); + break; + default: + return new Response("Template not found", { status: 404 }); + } + } catch (e) { + if (e instanceof EmailContextError) { + return new Response(e.message, { status: 400 }); + } else { + return new Response("Internal Server Error: " + (e as Error).message, { + status: 500, + }); + } } return Response.json(data); diff --git a/frontend/src/features/email/components/button.tsx b/frontend/src/features/email/components/button.tsx new file mode 100644 index 000000000..cf5965cfc --- /dev/null +++ b/frontend/src/features/email/components/button.tsx @@ -0,0 +1,17 @@ +import { Button } from "react-email"; + +import { cn } from "@/lib/utils/classname"; + +export function EmailButton({ href, label }: { href: string; label: string }) { + return ( + + ); +} diff --git a/frontend/src/features/email/templates/email-verification.tsx b/frontend/src/features/email/templates/email-verification.tsx new file mode 100644 index 000000000..39fec1298 --- /dev/null +++ b/frontend/src/features/email/templates/email-verification.tsx @@ -0,0 +1,37 @@ +import { Text } from "react-email"; + +import { EmailButton } from "@/features/email/components/button"; +import NOTES from "@/features/email/lib/notes"; +import renderEmail from "@/features/email/lib/render"; +import { EmailContextError, EmailRenderResponse } from "@/features/email/type"; + +const CONTENT_TEXT = + "Welcome to Plancake! Verify your email address to get started."; + +export async function emailVerification({ + code, +}: { + code: string; +}): Promise { + if (!code) { + throw new EmailContextError("Missing verification code."); + } + + const VERIFY_URL = `${process.env.BASE_URL}/verify-email?code=${code}`; + + return { + subject: "Plancake - Email Verification", + html: await renderEmail({ + previewText: CONTENT_TEXT, + title: "Email Verification", + content: ( + <> + {CONTENT_TEXT} + + + ), + footerText: NOTES.IGNORE_EMAIL, + }), + text: `${CONTENT_TEXT} Click this link to verify your email: ${VERIFY_URL}\n\n${NOTES.IGNORE_EMAIL}`, + }; +} diff --git a/frontend/src/features/email/type.ts b/frontend/src/features/email/type.ts index a09ca18f8..84ce3cd38 100644 --- a/frontend/src/features/email/type.ts +++ b/frontend/src/features/email/type.ts @@ -3,3 +3,5 @@ export type EmailRenderResponse = { html: string; text: string; }; + +export class EmailContextError extends Error {} From b1b848b25b7da37e450617888062dc0ca6f3ae3b Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Mon, 20 Jul 2026 12:59:40 -0400 Subject: [PATCH 07/23] Rename email notes to content --- frontend/src/features/email/lib/content.ts | 5 +++++ frontend/src/features/email/lib/notes.ts | 5 ----- frontend/src/features/email/templates/email-in-use.tsx | 6 +++--- .../src/features/email/templates/email-verification.tsx | 6 +++--- 4 files changed, 11 insertions(+), 11 deletions(-) create mode 100644 frontend/src/features/email/lib/content.ts delete mode 100644 frontend/src/features/email/lib/notes.ts diff --git a/frontend/src/features/email/lib/content.ts b/frontend/src/features/email/lib/content.ts new file mode 100644 index 000000000..bde275e2c --- /dev/null +++ b/frontend/src/features/email/lib/content.ts @@ -0,0 +1,5 @@ +const CONTENT = { + CAN_IGNORE: "Not you? You can ignore this email.", +} as const; + +export default CONTENT; diff --git a/frontend/src/features/email/lib/notes.ts b/frontend/src/features/email/lib/notes.ts deleted file mode 100644 index e0969b662..000000000 --- a/frontend/src/features/email/lib/notes.ts +++ /dev/null @@ -1,5 +0,0 @@ -const NOTES = { - IGNORE_EMAIL: "Not you? You can ignore this email.", -} as const; - -export default NOTES; diff --git a/frontend/src/features/email/templates/email-in-use.tsx b/frontend/src/features/email/templates/email-in-use.tsx index 4dca484d4..1b04ccf32 100644 --- a/frontend/src/features/email/templates/email-in-use.tsx +++ b/frontend/src/features/email/templates/email-in-use.tsx @@ -1,6 +1,6 @@ import { Link, Text } from "react-email"; -import NOTES from "@/features/email/lib/notes"; +import CONTENT from "@/features/email/lib/content"; import renderEmail from "@/features/email/lib/render"; import { EmailRenderResponse } from "@/features/email/type"; @@ -25,8 +25,8 @@ export async function emailInUse(): Promise { ), - footerText: NOTES.IGNORE_EMAIL, + footerText: CONTENT.CAN_IGNORE, }), - text: `${CONTENT_TEXT} Log in at ${LOGIN_URL} instead.\n\n${NOTES.IGNORE_EMAIL}`, + text: `${CONTENT_TEXT} Log in at ${LOGIN_URL} instead.\n\n${CONTENT.CAN_IGNORE}`, }; } diff --git a/frontend/src/features/email/templates/email-verification.tsx b/frontend/src/features/email/templates/email-verification.tsx index 39fec1298..90fbb5367 100644 --- a/frontend/src/features/email/templates/email-verification.tsx +++ b/frontend/src/features/email/templates/email-verification.tsx @@ -1,7 +1,7 @@ import { Text } from "react-email"; import { EmailButton } from "@/features/email/components/button"; -import NOTES from "@/features/email/lib/notes"; +import CONTENT from "@/features/email/lib/content"; import renderEmail from "@/features/email/lib/render"; import { EmailContextError, EmailRenderResponse } from "@/features/email/type"; @@ -30,8 +30,8 @@ export async function emailVerification({ ), - footerText: NOTES.IGNORE_EMAIL, + footerText: CONTENT.CAN_IGNORE, }), - text: `${CONTENT_TEXT} Click this link to verify your email: ${VERIFY_URL}\n\n${NOTES.IGNORE_EMAIL}`, + text: `${CONTENT_TEXT} Click this link to verify your email: ${VERIFY_URL}\n\n${CONTENT.CAN_IGNORE}`, }; } From c5499443d800d38f4e20af85d17d6cc68c8c8d9e Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Mon, 20 Jul 2026 13:02:12 -0400 Subject: [PATCH 08/23] Restructure text content --- frontend/src/features/email/templates/email-in-use.tsx | 5 ++++- .../src/features/email/templates/email-verification.tsx | 7 ++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/frontend/src/features/email/templates/email-in-use.tsx b/frontend/src/features/email/templates/email-in-use.tsx index 1b04ccf32..27f9f804e 100644 --- a/frontend/src/features/email/templates/email-in-use.tsx +++ b/frontend/src/features/email/templates/email-in-use.tsx @@ -27,6 +27,9 @@ export async function emailInUse(): Promise { ), footerText: CONTENT.CAN_IGNORE, }), - text: `${CONTENT_TEXT} Log in at ${LOGIN_URL} instead.\n\n${CONTENT.CAN_IGNORE}`, + text: + `${CONTENT_TEXT} Log in at ${LOGIN_URL} instead.` + + "\n\n" + + CONTENT.CAN_IGNORE, }; } diff --git a/frontend/src/features/email/templates/email-verification.tsx b/frontend/src/features/email/templates/email-verification.tsx index 90fbb5367..d4eddc5d9 100644 --- a/frontend/src/features/email/templates/email-verification.tsx +++ b/frontend/src/features/email/templates/email-verification.tsx @@ -32,6 +32,11 @@ export async function emailVerification({ ), footerText: CONTENT.CAN_IGNORE, }), - text: `${CONTENT_TEXT} Click this link to verify your email: ${VERIFY_URL}\n\n${CONTENT.CAN_IGNORE}`, + text: + CONTENT_TEXT + + "\n\n" + + `Click this link to verify your email: ${VERIFY_URL}` + + "\n\n" + + CONTENT.CAN_IGNORE, }; } From c709a65a6ec08c8f5377e965cdd3d32ea370f3b2 Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Mon, 20 Jul 2026 13:17:54 -0400 Subject: [PATCH 09/23] Add password reset email --- frontend/src/app/api/render-email/route.tsx | 4 ++ .../email/templates/password-reset.tsx | 38 +++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 frontend/src/features/email/templates/password-reset.tsx diff --git a/frontend/src/app/api/render-email/route.tsx b/frontend/src/app/api/render-email/route.tsx index ed61258ae..fa39ab445 100644 --- a/frontend/src/app/api/render-email/route.tsx +++ b/frontend/src/app/api/render-email/route.tsx @@ -1,5 +1,6 @@ import { emailInUse } from "@/features/email/templates/email-in-use"; import { emailVerification } from "@/features/email/templates/email-verification"; +import { passwordReset } from "@/features/email/templates/password-reset"; import { EmailContextError, EmailRenderResponse } from "@/features/email/type"; export async function POST(req: Request) { @@ -23,6 +24,9 @@ export async function POST(req: Request) { case "email_verification": data = await emailVerification(context); break; + case "password_reset": + data = await passwordReset(context); + break; default: return new Response("Template not found", { status: 404 }); } diff --git a/frontend/src/features/email/templates/password-reset.tsx b/frontend/src/features/email/templates/password-reset.tsx new file mode 100644 index 000000000..a7d080619 --- /dev/null +++ b/frontend/src/features/email/templates/password-reset.tsx @@ -0,0 +1,38 @@ +import { Text } from "react-email"; + +import { EmailButton } from "@/features/email/components/button"; +import CONTENT from "@/features/email/lib/content"; +import renderEmail from "@/features/email/lib/render"; +import { EmailContextError, EmailRenderResponse } from "@/features/email/type"; + +export async function passwordReset({ + token, +}: { + token: string; +}): Promise { + if (!token) { + throw new EmailContextError("Missing reset token."); + } + + const RESET_URL = `${process.env.BASE_URL}/reset-password?token=${token}`; + + return { + subject: "Plancake - Password Reset", + html: await renderEmail({ + previewText: + "Reset your Plancake account password with the link in this email.", + title: "Password Reset", + content: ( + <> + Forgot your password? Click the button below to reset it. + + + ), + footerText: CONTENT.CAN_IGNORE, + }), + text: + `Forgot your password? Reset it at this link: ${RESET_URL}` + + "\n\n" + + CONTENT.CAN_IGNORE, + }; +} From b1735f2b9a5934b6d4737f88be5c7b5492019cf2 Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Mon, 20 Jul 2026 13:40:21 -0400 Subject: [PATCH 10/23] Add password reset code email --- frontend/src/app/api/render-email/route.tsx | 4 ++ frontend/src/features/email/lib/content.ts | 2 + .../email/templates/password-reset-code.tsx | 37 +++++++++++++++++++ 3 files changed, 43 insertions(+) create mode 100644 frontend/src/features/email/templates/password-reset-code.tsx diff --git a/frontend/src/app/api/render-email/route.tsx b/frontend/src/app/api/render-email/route.tsx index fa39ab445..7a9fc6045 100644 --- a/frontend/src/app/api/render-email/route.tsx +++ b/frontend/src/app/api/render-email/route.tsx @@ -1,6 +1,7 @@ import { emailInUse } from "@/features/email/templates/email-in-use"; import { emailVerification } from "@/features/email/templates/email-verification"; import { passwordReset } from "@/features/email/templates/password-reset"; +import { passwordResetCode } from "@/features/email/templates/password-reset-code"; import { EmailContextError, EmailRenderResponse } from "@/features/email/type"; export async function POST(req: Request) { @@ -27,6 +28,9 @@ export async function POST(req: Request) { case "password_reset": data = await passwordReset(context); break; + case "password_reset_code": + data = await passwordResetCode(context); + break; default: return new Response("Template not found", { status: 404 }); } diff --git a/frontend/src/features/email/lib/content.ts b/frontend/src/features/email/lib/content.ts index bde275e2c..55b9e83d9 100644 --- a/frontend/src/features/email/lib/content.ts +++ b/frontend/src/features/email/lib/content.ts @@ -1,5 +1,7 @@ const CONTENT = { CAN_IGNORE: "Not you? You can ignore this email.", + CHANGE_PASSWORD: + "If this was not you, change your password immediately to protect your account.", } as const; export default CONTENT; diff --git a/frontend/src/features/email/templates/password-reset-code.tsx b/frontend/src/features/email/templates/password-reset-code.tsx new file mode 100644 index 000000000..ac09e6bba --- /dev/null +++ b/frontend/src/features/email/templates/password-reset-code.tsx @@ -0,0 +1,37 @@ +import { Section, Text } from "react-email"; + +import CONTENT from "@/features/email/lib/content"; +import renderEmail from "@/features/email/lib/render"; +import { EmailContextError, EmailRenderResponse } from "@/features/email/type"; + +const CONTENT_TEXT = + "Enter the following code to reset your Plancake account password:"; + +export async function passwordResetCode({ + code, +}: { + code: string; +}): Promise { + if (!code) { + throw new EmailContextError("Missing reset code."); + } + + return { + subject: "Plancake - Password Reset Code", + html: await renderEmail({ + previewText: + "Reset your Plancake account password with the code in this email.", + title: "Password Reset Code", + content: ( + <> + {CONTENT_TEXT} +
+ {code} +
+ {CONTENT.CHANGE_PASSWORD} + + ), + }), + text: CONTENT_TEXT + "\n\n" + code + "\n\n" + CONTENT.CHANGE_PASSWORD, + }; +} From f63bbea352c43671cbb982f9d133ed62ea93d119 Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Mon, 20 Jul 2026 13:47:07 -0400 Subject: [PATCH 11/23] Create send_templated_email function --- backend/api/utils.py | 56 ++++++++++++++++++++++++++++++++++++++++ backend/requirements.txt | 4 +++ 2 files changed, 60 insertions(+) diff --git a/backend/api/utils.py b/backend/api/utils.py index 76e580dda..0e1734211 100644 --- a/backend/api/utils.py +++ b/backend/api/utils.py @@ -5,6 +5,8 @@ from enum import Enum from zoneinfo import ZoneInfo, ZoneInfoNotFoundError +import requests +from django.core.mail import EmailMultiAlternatives from django.db.models import Q from ipware import get_client_ip from redis import Redis @@ -17,8 +19,10 @@ from api.redis_pools import sync_pool from api.settings import ( ACCOUNT_COOKIE_NAME, + BASE_URL, COOKIE_DOMAIN, DEBUG, + EMAIL_BRIDGE_SECRET, GUEST_COOKIE_NAME, LONG_SESS_EXP_SECONDS, SESS_EXP_SECONDS, @@ -472,3 +476,55 @@ def notify_live_update(event: LiveUpdateEvent): f"event_{event.event_code}", event.dumps(), ) + + +def send_templated_email(to_email, template_key, context) -> bool: + """ + Fetches rendered HTML from Next.js and sends it as an email. + + Returns True if the email was sent successfully, False otherwise. + """ + url = BASE_URL + headers = { + "Content-Type": "application/json", + "Authorization": f"Bearer {EMAIL_BRIDGE_SECRET}", + } + payload = { + "template_key": template_key, + "context": context, + } + + html_content = None + text_fallback = None + msg_subject = None + success = False + + try: + response = requests.post( + url + "/api/render-email/", json=payload, headers=headers, timeout=5 + ) + + if response.status_code == 200: + data = response.json() + html_content = data.get("html") + text_fallback = data.get("text") + msg_subject = data.get("subject") + success = True + logger.info(f"Successfully rendered email for template '{template_key}'") + else: + logger.error( + f"Failed to render email for template '{template_key}'. " + f"Status code: {response.status_code}, Response: {response.text}" + ) + except requests.RequestException as e: + logger.error(f"Error rendering email for template '{template_key}': {e}") + + if html_content: + EmailMultiAlternatives( + subject=msg_subject, + body=text_fallback, + alternatives=[(html_content, "text/html")], + to=[to_email], + ).send() + + return success diff --git a/backend/requirements.txt b/backend/requirements.txt index ba4dbd214..5da21fb57 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -6,6 +6,8 @@ billiard==4.2.1 boto3==1.39.14 botocore==1.39.14 celery==5.5.3 +certifi==2026.6.17 +charset-normalizer==3.4.9 click==8.2.1 click-didyoumean==0.3.1 click-plugins==1.1.1.2 @@ -19,6 +21,7 @@ django-ipware==7.0.1 django-ses==4.4.0 djangorestframework==3.16.0 h11==0.16.0 +idna==3.18 isort==8.0.1 jmespath==1.0.1 kombu==5.5.4 @@ -31,6 +34,7 @@ python-ipware==3.0.0 PyYAML==6.0.3 redis==6.2.0 regex==2026.4.4 +requests==2.34.2 s3transfer==0.13.1 six==1.17.0 sqlparse==0.5.3 From 9483a0b7c650650f099a998432c010d9b4f546db Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Mon, 20 Jul 2026 13:48:19 -0400 Subject: [PATCH 12/23] Add SEND_EMAILS check to email send function --- backend/api/utils.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/backend/api/utils.py b/backend/api/utils.py index 0e1734211..352edbd42 100644 --- a/backend/api/utils.py +++ b/backend/api/utils.py @@ -25,6 +25,7 @@ EMAIL_BRIDGE_SECRET, GUEST_COOKIE_NAME, LONG_SESS_EXP_SECONDS, + SEND_EMAILS, SESS_EXP_SECONDS, TEST_ENVIRONMENT, ThrottleScope, @@ -484,6 +485,9 @@ def send_templated_email(to_email, template_key, context) -> bool: Returns True if the email was sent successfully, False otherwise. """ + if not SEND_EMAILS: + return True + url = BASE_URL headers = { "Content-Type": "application/json", From 5aa6ab068a93105eed1d865150a46386a3f71233 Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Mon, 20 Jul 2026 13:49:35 -0400 Subject: [PATCH 13/23] Create email template key enum --- backend/api/utils.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/backend/api/utils.py b/backend/api/utils.py index 352edbd42..0f0e227a7 100644 --- a/backend/api/utils.py +++ b/backend/api/utils.py @@ -479,7 +479,14 @@ def notify_live_update(event: LiveUpdateEvent): ) -def send_templated_email(to_email, template_key, context) -> bool: +class EmailTemplateKey(str, Enum): + EMAIL_IN_USE = "email_in_use" + EMAIL_VERIFICATION = "email_verification" + PASSWORD_RESET = "password_reset" + PASSWORD_RESET_CODE = "password_reset_code" + + +def send_templated_email(to_email, template_key: EmailTemplateKey, context) -> bool: """ Fetches rendered HTML from Next.js and sends it as an email. From b5780b6d24e188290126fa1b3535adb637940641 Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Mon, 20 Jul 2026 13:55:03 -0400 Subject: [PATCH 14/23] Replace password reset code email --- backend/api/account/views.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/backend/api/account/views.py b/backend/api/account/views.py index 5a18c7e15..c4be48bab 100644 --- a/backend/api/account/views.py +++ b/backend/api/account/views.py @@ -35,10 +35,12 @@ ThrottleScopes, ) from api.utils import ( + EmailTemplateKey, MessageOutputSerializer, check_rate_limit, delete_session_cookie, prune_account_sessions, + send_templated_email, ) logger = logging.getLogger("api") @@ -244,14 +246,11 @@ def start_authed_password_reset(request): ) logger.debug("Authed password reset code for %s: %s", user.email, reset_code) - if SEND_EMAILS: - send_mail( - subject="Plancake - Password Reset Code", - message=f"Your password reset code is: {reset_code}.\n\nNot you? You should change your password immediately to protect your account.", - from_email=None, # Use the default from settings - recipient_list=[user.email], - fail_silently=False, - ) + send_templated_email( + to_email=user.email, + template_key=EmailTemplateKey.PASSWORD_RESET_CODE, + context={"code": reset_code}, + ) return Response( {"message": ["An email has been sent to your address with the reset code."]}, From 21558bf1675ce53a218a92c822967d6e0bdb6422 Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Mon, 20 Jul 2026 13:56:59 -0400 Subject: [PATCH 15/23] Update email function docstring --- backend/api/utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/backend/api/utils.py b/backend/api/utils.py index 0f0e227a7..6ff7d813d 100644 --- a/backend/api/utils.py +++ b/backend/api/utils.py @@ -491,6 +491,9 @@ def send_templated_email(to_email, template_key: EmailTemplateKey, context) -> b Fetches rendered HTML from Next.js and sends it as an email. Returns True if the email was sent successfully, False otherwise. + + This function will check the SEND_EMAILS environment variable, and if set to False + then the email will not be sent and True will be returned every time. """ if not SEND_EMAILS: return True From efe03948939cfa189b8a16416313adcd41364906 Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Mon, 20 Jul 2026 14:02:07 -0400 Subject: [PATCH 16/23] Remove unused imports --- backend/api/account/views.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/backend/api/account/views.py b/backend/api/account/views.py index c4be48bab..b224e2828 100644 --- a/backend/api/account/views.py +++ b/backend/api/account/views.py @@ -5,7 +5,6 @@ import bcrypt from device_detector import DeviceDetector -from django.core.mail import send_mail from django.db import transaction from django.db.models import Q from rest_framework.response import Response @@ -30,7 +29,6 @@ ACCOUNT_COOKIE_NAME, AUTHED_PWD_RESET_EXP_SECONDS, LONG_SESS_EXP_SECONDS, - SEND_EMAILS, SESS_EXP_SECONDS, ThrottleScopes, ) From fc57fa7f391b1cab69c7e8f139f8411149f4962b Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Mon, 20 Jul 2026 14:02:18 -0400 Subject: [PATCH 17/23] Replace auth endpoint emails --- backend/api/auth/views.py | 57 +++++++++++++++------------------------ 1 file changed, 22 insertions(+), 35 deletions(-) diff --git a/backend/api/auth/views.py b/backend/api/auth/views.py index a219c9a28..31f34a310 100644 --- a/backend/api/auth/views.py +++ b/backend/api/auth/views.py @@ -3,7 +3,6 @@ from datetime import datetime, timedelta import bcrypt -from django.core.mail import send_mail from django.db import transaction from rest_framework.response import Response @@ -31,19 +30,19 @@ ) from api.settings import ( ACCOUNT_COOKIE_NAME, - BASE_URL, EMAIL_CODE_EXP_SECONDS, PWD_RESET_EXP_SECONDS, - SEND_EMAILS, ThrottleScopes, ) from api.utils import ( + EmailTemplateKey, MessageOutputSerializer, check_rate_limit, delete_session_cookie, get_client_ip_address, get_client_user_agent, get_session, + send_templated_email, set_session_cookie, ) @@ -83,14 +82,11 @@ def register(request): # Check if the email already exists if UserAccount.objects.filter(email=email).exists(): logger.info("Email %s is already in use!", email) - if SEND_EMAILS: - send_mail( - subject="Plancake - Email in Use", - message=f"Looks like your email was already used for a Plancake account.\n\nNot you? Nothing to worry about, just ignore this email.", - from_email=None, # Use the default from settings - recipient_list=[email], - fail_silently=False, - ) + send_templated_email( + to_email=email, + template_key=EmailTemplateKey.EMAIL_IN_USE, + context={}, + ) else: # Create an unverified user account ver_code = str(uuid.uuid4()) @@ -103,14 +99,11 @@ def register(request): ) logger.debug("Verification code for %s: %s", email, ver_code) - if SEND_EMAILS: - send_mail( - subject="Plancake - Email Verification", - message=f"Welcome to Plancake!\n\nClick this link to verify your email:\n{BASE_URL}/verify-email?code={ver_code}\n\nNot you? Nothing to worry about, just ignore this email.", - from_email=None, # Use the default from settings - recipient_list=[email], - fail_silently=False, - ) + send_templated_email( + to_email=email, + template_key=EmailTemplateKey.EMAIL_VERIFICATION, + context={"code": ver_code}, + ) return Response( {"message": ["An email has been sent to your address for verification."]}, @@ -147,14 +140,11 @@ def resend_register_email(request): unverified_user.verification_code, ) - if SEND_EMAILS: - send_mail( - subject="Plancake - Email Verification", - message=f"Welcome to Plancake!\n\nClick this link to verify your email:\n{BASE_URL}/verify-email?code={unverified_user.verification_code}\n\nNot you? Nothing to worry about, just ignore this email.", - from_email=None, # Use the default from settings - recipient_list=[email], - fail_silently=False, - ) + send_templated_email( + to_email=email, + template_key=EmailTemplateKey.EMAIL_VERIFICATION, + context={"code": unverified_user.verification_code}, + ) except UnverifiedUserAccount.DoesNotExist: logger.info("Unverified user with email %s does not exist!", email) @@ -333,14 +323,11 @@ def start_password_reset(request): ) logger.debug("Password reset token for %s: %s", email, reset_token) - if SEND_EMAILS: - send_mail( - subject="Plancake - Reset Password", - message=f"Click this link to reset your password:\n{BASE_URL}/reset-password?token={reset_token}\n\nNot you? Nothing to worry about, just ignore this email.", - from_email=None, # Use the default from settings - recipient_list=[email], - fail_silently=False, - ) + send_templated_email( + to_email=email, + template_key=EmailTemplateKey.PASSWORD_RESET, + context={"token": reset_token}, + ) except UserAccount.DoesNotExist: logger.info("Password reset failed for %s: User does not exist.", email) From 8d16a06b271e534f1b7688e805a55702dab2b42e Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Mon, 20 Jul 2026 14:08:25 -0400 Subject: [PATCH 18/23] Add expiration info to codes and links --- frontend/src/features/email/lib/content.ts | 2 ++ frontend/src/features/email/templates/email-verification.tsx | 1 + frontend/src/features/email/templates/password-reset-code.tsx | 1 + frontend/src/features/email/templates/password-reset.tsx | 1 + 4 files changed, 5 insertions(+) diff --git a/frontend/src/features/email/lib/content.ts b/frontend/src/features/email/lib/content.ts index 55b9e83d9..8a781fa38 100644 --- a/frontend/src/features/email/lib/content.ts +++ b/frontend/src/features/email/lib/content.ts @@ -2,6 +2,8 @@ const CONTENT = { CAN_IGNORE: "Not you? You can ignore this email.", CHANGE_PASSWORD: "If this was not you, change your password immediately to protect your account.", + LINK_EXPIRE: "This link will expire in 10 minutes.", + CODE_EXPIRE: "This code will expire in 10 minutes.", } as const; export default CONTENT; diff --git a/frontend/src/features/email/templates/email-verification.tsx b/frontend/src/features/email/templates/email-verification.tsx index d4eddc5d9..1645adc3e 100644 --- a/frontend/src/features/email/templates/email-verification.tsx +++ b/frontend/src/features/email/templates/email-verification.tsx @@ -28,6 +28,7 @@ export async function emailVerification({ <> {CONTENT_TEXT} + {CONTENT.LINK_EXPIRE} ), footerText: CONTENT.CAN_IGNORE, diff --git a/frontend/src/features/email/templates/password-reset-code.tsx b/frontend/src/features/email/templates/password-reset-code.tsx index ac09e6bba..63a39cf38 100644 --- a/frontend/src/features/email/templates/password-reset-code.tsx +++ b/frontend/src/features/email/templates/password-reset-code.tsx @@ -28,6 +28,7 @@ export async function passwordResetCode({
{code}
+ {CONTENT.CODE_EXPIRE} {CONTENT.CHANGE_PASSWORD} ), diff --git a/frontend/src/features/email/templates/password-reset.tsx b/frontend/src/features/email/templates/password-reset.tsx index a7d080619..af7552834 100644 --- a/frontend/src/features/email/templates/password-reset.tsx +++ b/frontend/src/features/email/templates/password-reset.tsx @@ -26,6 +26,7 @@ export async function passwordReset({ <> Forgot your password? Click the button below to reset it. + {CONTENT.LINK_EXPIRE} ), footerText: CONTENT.CAN_IGNORE, From 583a56ccaecff5ce2ed5a4d3732e15fc57bb3c7a Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Mon, 20 Jul 2026 14:53:02 -0400 Subject: [PATCH 19/23] Add EMAIL_TIMEOUT --- backend/api/settings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/api/settings.py b/backend/api/settings.py index e7d298257..35db4a01a 100644 --- a/backend/api/settings.py +++ b/backend/api/settings.py @@ -166,6 +166,7 @@ class ThrottleScopes: AWS_SES_REGION_ENDPOINT = env("AWS_SES_REGION_ENDPOINT") DEFAULT_FROM_EMAIL = env("DEFAULT_FROM_EMAIL") EMAIL_BRIDGE_SECRET = env("EMAIL_BRIDGE_SECRET") +EMAIL_TIMEOUT = 10 # seconds ADMIN_EMAILS = env.list("ADMIN_EMAILS", default=[]) SEND_EMAILS = env.bool("SEND_EMAILS", default=False) CRITICAL_EMAIL_INTERVAL_SECONDS = 1800 # 30 minutes From 97538945bf16b0f74378a743150a7e2edab3548e Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Mon, 20 Jul 2026 14:55:36 -0400 Subject: [PATCH 20/23] Add error handling for email sending --- backend/api/utils.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/backend/api/utils.py b/backend/api/utils.py index 6ff7d813d..270d4cc46 100644 --- a/backend/api/utils.py +++ b/backend/api/utils.py @@ -533,12 +533,16 @@ def send_templated_email(to_email, template_key: EmailTemplateKey, context) -> b except requests.RequestException as e: logger.error(f"Error rendering email for template '{template_key}': {e}") - if html_content: - EmailMultiAlternatives( - subject=msg_subject, - body=text_fallback, - alternatives=[(html_content, "text/html")], - to=[to_email], - ).send() + try: + if html_content: + EmailMultiAlternatives( + subject=msg_subject, + body=text_fallback, + alternatives=[(html_content, "text/html")], + to=[to_email], + ).send() + except Exception as e: + logger.error(f"Error sending email: {e}") + success = False return success From a9bdaf939676b6d51ea6b022f82b5259b0c5b23e Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Mon, 20 Jul 2026 14:56:20 -0400 Subject: [PATCH 21/23] Add check for secret being set --- frontend/src/app/api/render-email/route.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/app/api/render-email/route.tsx b/frontend/src/app/api/render-email/route.tsx index 7a9fc6045..47621b813 100644 --- a/frontend/src/app/api/render-email/route.tsx +++ b/frontend/src/app/api/render-email/route.tsx @@ -7,6 +7,7 @@ import { EmailContextError, EmailRenderResponse } from "@/features/email/type"; export async function POST(req: Request) { const authHeader = req.headers.get("Authorization"); if ( + !process.env.EMAIL_BRIDGE_SECRET || !authHeader || authHeader !== `Bearer ${process.env.EMAIL_BRIDGE_SECRET}` ) { From d0210ec63150857af71782f8204711cf139baadf Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Mon, 20 Jul 2026 14:57:23 -0400 Subject: [PATCH 22/23] Add trailing slash comments on frontend .env --- frontend/.env.example | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/.env.example b/frontend/.env.example index 8eaaf7dea..65ee104fa 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -1,11 +1,11 @@ # Enable debug mode, important to have this false in production for security NEXT_PUBLIC_DEBUG=true -# The base URL for the frontend, used for generating links in emails +# The base URL for the frontend, used for generating links in emails (no trailing slash) BASE_URL=http://localhost:3000 # The API URL for both the client and the Next.js server to communicate with the backend -# Note the lack of a trailing slash +# (no trailing slash) NEXT_PUBLIC_API_URL=https://example.com # The URL for the feedback form From 988ec3294e1117ef8d267f19187eb1ef78351f67 Mon Sep 17 00:00:00 2001 From: jzgom067 Date: Mon, 20 Jul 2026 14:58:15 -0400 Subject: [PATCH 23/23] Replace invalid Tailwind class --- frontend/src/features/email/components/button.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/features/email/components/button.tsx b/frontend/src/features/email/components/button.tsx index cf5965cfc..692f90a4f 100644 --- a/frontend/src/features/email/components/button.tsx +++ b/frontend/src/features/email/components/button.tsx @@ -8,7 +8,7 @@ export function EmailButton({ href, label }: { href: string; label: string }) { href={href} className={cn( "bg-accent inline-block rounded-full px-4 py-2", - "text-background text-md text-center", + "text-background text-center text-base", )} > {label}