Send email through Hanzo notify

sendMail posts to /v1/notify/send/email as the app's own IAM identity
(client credentials), and notify delivers with the email provider and
sender configured for that org. SMTP settings and nodemailer are gone.
notify carries no attachments, so the e-sign completion email links the
signed document with a seven-day presigned URL instead of attaching it.
This commit is contained in:
zooqueen
2026-09-15 12:06:01 -07:00
parent 6804b03542
commit e155a2bfef
15 changed files with 136 additions and 117 deletions
-4
View File
@@ -35,10 +35,6 @@ IAM_CLIENT_SECRET=""
# GOOGLE_CLIENT_ID=""
# GOOGLE_CLIENT_SECRET=""
# SMTP
EMAIL_FROM="'Captable, Inc.' <[email protected]>"
EMAIL_SERVER=smtp://captable:password@localhost:1025
# Uploads
UPLOAD_ENDPOINT="http://127.0.0.1:9002"
NEXT_PUBLIC_UPLOAD_DOMAIN="http://127.0.0.1:9002"
-1
View File
@@ -186,7 +186,6 @@ When contributing to <strong>Hanzo Captable</strong>, whether on GitHub or in ot
- Install latest version of node and pnpm
- Install latest version of postgres database
- Install [mailpit](https://mailpit.axllent.org/docs/install/) for SMTP and email interception
- Create database `captable` in postgres database
- Update `.env` file's `DATABASE_URL` with database credentials
- For a quick start, you can use [Supabase database](https://supabase.com/) or [Neon](https://neon.tech/) as well.
+8 -7
View File
@@ -26,11 +26,11 @@ NEXTAUTH_SECRET="xxx" # Generated by `openssl rand -base64 32`
NEXTAUTH_URL="https://your-domain.com"
NEXT_PUBLIC_BASE_URL="https://your-domain.com"
# Email server environment variables
EMAIL_FROM="[email protected]"
# Please use encrypted/TLS connection for SMTP server.
EMAIL_SERVER="smtp://username:password@host:port"
# Hanzo IAM application: sign-in, and the identity that sends email through
# Hanzo notify with the org's email provider
IAM_URL="https://hanzo.id"
IAM_CLIENT_ID="xxx"
IAM_CLIENT_SECRET="xxx"
# File uplod environment variables
UPLOAD_REGION="us-west-1" # auto when using Cloudflare R2
@@ -58,8 +58,9 @@ docker run -d \
-e NEXTAUTH_SECRET="replace" \
-e NEXTAUTH_URL="replace" \
-e NEXT_PUBLIC_BASE_URL="replace" \
-e EMAIL_FROM="replace" \
-e EMAIL_SERVER="replace" \
-e IAM_URL="replace" \
-e IAM_CLIENT_ID="replace" \
-e IAM_CLIENT_SECRET="replace" \
-e UPLOAD_REGION="replace" \
-e UPLOAD_ENDPOINT="replace" \
-e NEXT_PUBLIC_UPLOAD_DOMAIN="replace" \
-9
View File
@@ -8,7 +8,6 @@ services:
container_name: captable-app
depends_on:
- pg
- smtp
- minio
restart: always
ports:
@@ -31,7 +30,6 @@ services:
container_name: captable-studio
depends_on:
- pg
- smtp
- minio
restart: always
ports:
@@ -59,13 +57,6 @@ services:
volumes:
- postgres:/var/lib/postgresql/data
smtp:
image: axllent/mailpit
container_name: captable-smtp
ports:
- 8025:8025
- 1025:1025
minio:
image: minio/minio
container_name: captable-minio
-3
View File
@@ -23,7 +23,6 @@
"postinstall": "prisma generate",
"prepare": "husky",
"lint": "biome check --apply ./src",
"email:dev": "mailpit",
"format": "biome format --write ./src && prisma format",
"email:preview": "email preview ./src/emails",
"copy:pdfjs": "node scripts/copy-pdfjs-worker.cjs",
@@ -93,7 +92,6 @@
"next": "^15.1.0",
"next-auth": "^4.24.7",
"next-nprogress-bar": "^2.3.13",
"nodemailer": "^6.9.14",
"papaparse": "^5.4.1",
"pdf-lib": "^1.17.1",
"pg-boss": "^10.1.1",
@@ -131,7 +129,6 @@
"@types/inquirer": "^9.0.7",
"@types/lodash-es": "^4.17.12",
"@types/node": "^20.12.12",
"@types/nodemailer": "^6.4.14",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.2.23",
"@types/ua-parser-js": "^0.7.39",
+50 -25
View File
@@ -194,9 +194,6 @@ importers:
next-nprogress-bar:
specifier: ^2.3.13
version: 2.3.13
nodemailer:
specifier: ^6.9.14
version: 6.9.14
papaparse:
specifier: ^5.4.1
version: 5.4.1
@@ -303,9 +300,6 @@ importers:
'@types/node':
specifier: ^20.12.12
version: 20.12.12
'@types/nodemailer':
specifier: ^6.4.14
version: 6.4.15
'@types/react':
specifier: ^18.3.3
version: 18.3.3
@@ -367,6 +361,7 @@ packages:
'@ark-ui/[email protected]':
resolution: {integrity: sha512-pfHDkuFRFdzOnZxqNcAld1b03ehvFrOqXery9JHBTchP8+VmAmxg6ZDn4Se1ie4HvcmXD6IU61jltJB/aXYpJg==}
deprecated: This package is deprecated. Please import directly from @ark-ui/<framework>
'@ark-ui/[email protected]':
resolution: {integrity: sha512-W43rbimG1f7v1jk8J+PG/3um8/PaL1lQZEnRPzaXtQjEoiBlXH4yFyJakoEldxoZEQGB8x7SUkjHxZ93cRceuQ==}
@@ -771,6 +766,7 @@ packages:
'@ericcornelissen/[email protected]':
resolution: {integrity: sha512-4pIMTa1nEFfMXitv7oaNEWOdM+zpOZavesa5GaiWTgda6Zk32CFGxjUp/iIaN0PwgUW1yTq/fztSjbpE8SLGZQ==}
engines: {node: '>=4'}
deprecated: Support for this package will stop 2025-12-31
'@esbuild/[email protected]':
resolution: {integrity: sha512-FnzU0LyE3ySQk7UntJO4+qIiQgI7KoODnZg5xzXIrFJlKd2P2gwHsHY4927xj9y5PJmJSzULiUCWmv7iWnNa7g==}
@@ -2844,95 +2840,111 @@ packages:
'@react-email/[email protected]':
resolution: {integrity: sha512-dMJyL9aU25ieatdPtVjCyQ/WHZYHwNc+Hy/XpF8Cc18gu21cUynVEeYQzFSeigDRMeBQ3PGAyjVDPIob7YlGwA==}
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
peerDependencies:
react: ^18.0 || ^19.0 || ^19.0.0-rc
'@react-email/[email protected]':
resolution: {integrity: sha512-ioHdsk+BpGS/PqjU6JS7tUrVy9yvbUx92Z+Cem2+MbYp55oEwQ9VHf7u4f5NoM0gdhfKSehBwRdYlHt/frEMcg==}
engines: {node: '>=18.0.0'}
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
peerDependencies:
react: ^18.0 || ^19.0 || ^19.0.0-rc
'@react-email/[email protected]':
resolution: {integrity: sha512-Zrhc71VYrSC1fVXJuaViKoB/dBjxLw6nbE53Bm/eUuZPdnnZ1+ZUIh8jfaRKC5MzMjgnLGQTweGXVnfIrhyxtQ==}
engines: {node: '>=18.0.0'}
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
peerDependencies:
react: ^18.0 || ^19.0 || ^19.0.0-rc
'@react-email/[email protected]':
resolution: {integrity: sha512-zj3oMQiiUCZbddSNt3k0zNfIBFK0ZNDIzzDyBaJKy6ZASTtWfB+1WFX0cpTX8q0gUiYK+A94rk5Qp68L6YXjXQ==}
engines: {node: '>=18.0.0'}
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
peerDependencies:
react: ^18.0 || ^19.0 || ^19.0.0-rc
'@react-email/[email protected]':
resolution: {integrity: sha512-Rsl7iSdDaeHZO938xb+0wR5ud0Z3MVfdtPbNKJNojZi2hApwLAQXmDrnn/AcPDM5Lpl331ZljJS8vHTWxxkvKw==}
engines: {node: '>=18.0.0'}
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
peerDependencies:
react: ^18.0 || ^19.0 || ^19.0.0-rc
'@react-email/[email protected]':
resolution: {integrity: sha512-lnfVVrThEcET5NPoeaXvrz9UxtWpGRcut2a07dLbyKgNbP7vj/cXTI5TuHtanCvhCddFpMDnElNRghDOfPzwUg==}
engines: {node: '>=18.0.0'}
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
peerDependencies:
react: ^18.0 || ^19.0 || ^19.0.0-rc
'@react-email/[email protected]':
resolution: {integrity: sha512-NgoaJJd9tTtsrveL86Ocr/AYLkGyN3prdXKd/zm5fQpfDhy/NXezyT3iF6VlwAOEUIu64ErHpAJd+P6ygR+vjg==}
engines: {node: '>=18.0.0'}
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
peerDependencies:
react: ^18.0 || ^19.0 || ^19.0.0-rc
'@react-email/[email protected]':
resolution: {integrity: sha512-fSBEqYyVPAyyACBBHcs3wEYzNknpHMuwcSAAKE8fOoDfGqURr/vSxKPdh4tOa9z7G4hlcEfgGrCYEa2iPT22cw==}
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
peerDependencies:
react: ^18.0 || ^19.0 || ^19.0.0-rc
'@react-email/[email protected]':
resolution: {integrity: sha512-skw5FUgyamIMK+LN+fZQ5WIKQYf0dPiRAvsUAUR2eYoZp9oRsfkIpFHr0GWPkKAYjFEj+uJjaxQ/0VzQH7svVg==}
engines: {node: '>=18.0.0'}
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
peerDependencies:
react: ^18.0 || ^19.0 || ^19.0.0-rc
'@react-email/[email protected]':
resolution: {integrity: sha512-jZM7IVuZOXa0G110ES8OkxajPTypIKlzlO1K1RIe1auk76ukQRiCg1IRV4HZlWk1GGUbec5hNxsvZa2kU8cb9w==}
engines: {node: '>=18.0.0'}
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
peerDependencies:
react: ^18.0 || ^19.0 || ^19.0.0-rc
'@react-email/[email protected]':
resolution: {integrity: sha512-3AA4Yjgl3zEid/KVx6uf6TuLJHVZvUc2cG9Wm9ZpWeAX4ODA+8g9HyuC0tfnjbRsVMhMcCGiECuWWXINi+60vA==}
engines: {node: '>=18.0.0'}
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
peerDependencies:
react: ^18.0 || ^19.0 || ^19.0.0-rc
'@react-email/[email protected]':
resolution: {integrity: sha512-06uiuSKJBWQJfhCKv4MPupELei4Lepyz9Sth7Yq7Fq29CAeB1ejLgKkGqn1I+FZ72hQxPLdYF4iq4yloKv3JCg==}
engines: {node: '>=18.0.0'}
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
peerDependencies:
react: ^18.0 || ^19.0 || ^19.0.0-rc
'@react-email/[email protected]':
resolution: {integrity: sha512-pJ8glJjDNaJ53qoM95pvX9SK05yh0bNQY/oyBKmxlBDdUII6ixuMc3SCwYXPMl+tgkQUyDgwEBpSTrLAnjL3hA==}
engines: {node: '>=18.0.0'}
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
peerDependencies:
react: ^18.0 || ^19.0 || ^19.0.0-rc
'@react-email/[email protected]':
resolution: {integrity: sha512-tva3wvAWSR10lMJa9fVA09yRn7pbEki0ZZpHE6GD1jKbFhmzt38VgLO9B797/prqoDZdAr4rVK7LJFcdPx3GwA==}
engines: {node: '>=18.0.0'}
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
peerDependencies:
react: ^18.0 || ^19.0 || ^19.0.0-rc
'@react-email/[email protected]':
resolution: {integrity: sha512-wsuvj1XAb6O63aizCLNEeqVgKR3oFjAwt9vjfg2y2oh4G1dZeo8zonZM2x1fmkEkBZhzwSHraNi70jSXhA3A9w==}
engines: {node: '>=18.0.0'}
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
peerDependencies:
react: ^18.0 || ^19.0 || ^19.0.0-rc
'@react-email/[email protected]':
resolution: {integrity: sha512-7O/CT4b16YlSGrj18htTPx3Vbhu2suCGv/cSe5c+fuSrIM/nMiBSZ3Js16Vj0XJbAmmmlVmYFZw9L20wXJ+LjQ==}
engines: {node: '>=18.0.0'}
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
peerDependencies:
react: ^18.0 || ^19.0 || ^19.0.0-rc
@@ -2946,24 +2958,28 @@ packages:
'@react-email/[email protected]':
resolution: {integrity: sha512-jPyEhG3gsLX+Eb9U+A30fh0gK6hXJwF4ghJ+ZtFQtlKAKqHX+eCpWlqB3Xschd/ARJLod8WAswg0FB+JD9d0/A==}
engines: {node: '>=18.0.0'}
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
peerDependencies:
react: ^18.0 || ^19.0 || ^19.0.0-rc
'@react-email/[email protected]':
resolution: {integrity: sha512-+fYWLb4tPU1A/+GE5J1+SEMA7/wR3V30lQ+OR9t2kAJqNrARDbMx0bLnYnR1QL5TiFRz0pCF05SQUobk6gHEDQ==}
engines: {node: '>=18.0.0'}
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
peerDependencies:
react: ^18.0 || ^19.0 || ^19.0.0-rc
'@react-email/[email protected]':
resolution: {integrity: sha512-qysVUEY+M3SKUvu35XDpzn7yokhqFOT3tPU6Mj/pgc62TL5tQFj6msEbBtwoKs2qO3WZvai0DIHdLhaOxBQSow==}
engines: {node: '>=18.0.0'}
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
peerDependencies:
react: ^18.0 || ^19.0 || ^19.0.0-rc
'@react-email/[email protected]':
resolution: {integrity: sha512-wNAnxeEAiFs6N+SxS0y6wTJWfewEzUETuyS2aZmT00xk50VijwyFRuhm4sYSjusMyshevomFwz5jNISCxRsGWw==}
engines: {node: '>=18.0.0'}
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
peerDependencies:
react: ^18.0 || ^19.0 || ^19.0.0-rc
@@ -3121,6 +3137,7 @@ packages:
'@scalar/[email protected]':
resolution: {integrity: sha512-7edLTztM7HKNOJ20rMd8YyzQUeoVuabSSlXuCxk6q+tzeSELMbAUlv3ZXZMINVZAWMIdvVsP3ktdpNAzWHNHHQ==}
engines: {node: '>=18'}
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
'@scalar/[email protected]':
resolution: {integrity: sha512-GAVDclMEWerm6hlSU8dse+bzqxj4tzHCZLRVa6UtfCkdxmOqAaP46TDKasJAfuMPfWrVDNgGnIRwLBAz0u/zsQ==}
@@ -3204,6 +3221,7 @@ packages:
'@scalar/[email protected]':
resolution: {integrity: sha512-OccT/4vWnz3MFoPRIRlFDFWFBca3GCxXQ9LjFaOTzb47W9Fb8l4mwsonZ/h8pCYNq0tZqaRul6G5J8AFzAgKrg==}
engines: {node: '>=18'}
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
'@selderee/[email protected]':
resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==}
@@ -3343,6 +3361,7 @@ packages:
'@simplewebauthn/[email protected]':
resolution: {integrity: sha512-SFXke7xkgPRowY2E+8djKbdEznTVnD5R6GO7GPTthpHrokLvNKw8C3lFZypTxLI7KkCfGPfhtqB3d7OVGGa9jQ==}
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
'@sinclair/[email protected]':
resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==}
@@ -3549,6 +3568,7 @@ packages:
'@snyk/[email protected]':
resolution: {integrity: sha512-lGFf08pbkEac0NYgVf4hdANpAgApRjNByLXB+WBip3qj1iendOIyAwP2GKkKbQMNVy2r1xxDf0ssfWscoiC+Vw==}
engines: {node: '>=8.10'}
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
hasBin: true
'@socket.io/[email protected]':
@@ -3970,9 +3990,6 @@ packages:
'@types/[email protected]':
resolution: {integrity: sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==}
'@types/[email protected]':
resolution: {integrity: sha512-0EBJxawVNjPkng1zm2vopRctuWVCxk34JcIlRuXSf54habUWdz1FB7wHDqOqvDa8Mtpt0Q3LTXQkAs2LNyK5jQ==}
'@types/[email protected]':
resolution: {integrity: sha512-LxJ4iEFcpqc6METwp9f6BV6VVc43m6MfH0VqFosHvrUgfXiFe6ww7R3itkOQ+TCK6Y+Iv/+RnnvtRZnkc5Kc9g==}
@@ -4033,6 +4050,7 @@ packages:
'@ungap/[email protected]':
resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==}
deprecated: Potential CWE-502 - Update to 1.3.1 or higher
'@unhead/[email protected]':
resolution: {integrity: sha512-XZSZ2Wmm1Sv7k9scSFGrarbteSIl3p3I3oOUprKPDboBTvuG5q81Qz8O99NKUGKGJ8BKUkxCqE982eH3S8DKJA==}
@@ -4375,6 +4393,7 @@ packages:
[email protected]:
resolution: {integrity: sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==}
deprecated: package has been renamed to acorn-import-attributes
peerDependencies:
acorn: ^8
@@ -4840,6 +4859,7 @@ packages:
[email protected]:
resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==}
engines: {node: '>=12.0.0'}
deprecated: v4 is no longer maintained, upgrade to v5
[email protected]:
resolution: {integrity: sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==}
@@ -5361,19 +5381,22 @@ packages:
[email protected]:
resolution: {integrity: sha512-6LFElP3A+i/Q8XQKEvZjkEWEOTgAIALR9AO2rwT8bgPhDd1anmqDJDZ6lLddI4ehxxxR1S5RIqKe1uapMQfYaQ==}
engines: {node: '>=16 || 14 >=14.17'}
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting [email protected]
hasBin: true
[email protected]:
resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==}
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting [email protected]
hasBin: true
[email protected]:
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
deprecated: Glob versions prior to v9 are no longer supported
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting [email protected]
[email protected]:
resolution: {integrity: sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==}
engines: {node: '>=16 || 14 >=14.17'}
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting [email protected]
[email protected]:
resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==}
@@ -6484,6 +6507,7 @@ packages:
[email protected]:
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
engines: {node: '>=10.5.0'}
deprecated: Use your platform's native DOMException instead
[email protected]:
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
@@ -7196,6 +7220,7 @@ packages:
[email protected]:
resolution: {integrity: sha512-hlLJMhPQfv4/3NBSAyq3gzGg4h2v69RJh6KU7b3pXYNNAELs9kEoXOjbkxdXpALqKBoVmVptGfLpxdaVYqjmXQ==}
engines: {node: '>=14'}
deprecated: 1.x and 2.x branches are no longer active. Bump to Recharts v3 to receive latest features and bugfixes. See https://github.com/recharts/recharts/wiki/3.0-migration-guide
peerDependencies:
react: ^16.0.0 || ^17.0.0 || ^18.0.0
react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0
@@ -7654,6 +7679,7 @@ packages:
[email protected]:
resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==}
engines: {node: '>=10'}
deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting [email protected]
[email protected]:
resolution: {integrity: sha512-1QTEcJkJEhc8OnStBx/ILRu5J2p0GjvWsBx56bmZRqnrkdBMUe+nX92jxV+p3dB4CP6PZCdJMQJwCggkNBMzkQ==}
@@ -7952,10 +7978,12 @@ packages:
[email protected]:
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).
hasBin: true
[email protected]:
resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).
hasBin: true
[email protected]:
@@ -8436,8 +8464,8 @@ snapshots:
'@aws-crypto/sha1-browser': 3.0.0
'@aws-crypto/sha256-browser': 3.0.0
'@aws-crypto/sha256-js': 3.0.0
'@aws-sdk/client-sso-oidc': 3.577.0(@aws-sdk/[email protected])
'@aws-sdk/client-sts': 3.577.0
'@aws-sdk/client-sso-oidc': 3.577.0
'@aws-sdk/client-sts': 3.577.0(@aws-sdk/[email protected])
'@aws-sdk/core': 3.576.0
'@aws-sdk/credential-provider-node': 3.577.0(@aws-sdk/[email protected])(@aws-sdk/[email protected])
'@aws-sdk/middleware-bucket-endpoint': 3.577.0
@@ -8494,11 +8522,11 @@ snapshots:
transitivePeerDependencies:
- aws-crt
'@aws-sdk/[email protected](@aws-sdk/[email protected])':
'@aws-sdk/[email protected]':
dependencies:
'@aws-crypto/sha256-browser': 3.0.0
'@aws-crypto/sha256-js': 3.0.0
'@aws-sdk/client-sts': 3.577.0
'@aws-sdk/client-sts': 3.577.0(@aws-sdk/[email protected])
'@aws-sdk/core': 3.576.0
'@aws-sdk/credential-provider-node': 3.577.0(@aws-sdk/[email protected])(@aws-sdk/[email protected])
'@aws-sdk/middleware-host-header': 3.577.0
@@ -8537,7 +8565,6 @@ snapshots:
'@smithy/util-utf8': 3.0.0
tslib: 2.8.1
transitivePeerDependencies:
- '@aws-sdk/client-sts'
- aws-crt
'@aws-sdk/[email protected]':
@@ -8583,11 +8610,11 @@ snapshots:
transitivePeerDependencies:
- aws-crt
'@aws-sdk/[email protected]':
'@aws-sdk/[email protected](@aws-sdk/[email protected])':
dependencies:
'@aws-crypto/sha256-browser': 3.0.0
'@aws-crypto/sha256-js': 3.0.0
'@aws-sdk/client-sso-oidc': 3.577.0(@aws-sdk/[email protected])
'@aws-sdk/client-sso-oidc': 3.577.0
'@aws-sdk/core': 3.576.0
'@aws-sdk/credential-provider-node': 3.577.0(@aws-sdk/[email protected])(@aws-sdk/[email protected])
'@aws-sdk/middleware-host-header': 3.577.0
@@ -8626,6 +8653,7 @@ snapshots:
'@smithy/util-utf8': 3.0.0
tslib: 2.8.1
transitivePeerDependencies:
- '@aws-sdk/client-sso-oidc'
- aws-crt
'@aws-sdk/[email protected]':
@@ -8659,7 +8687,7 @@ snapshots:
'@aws-sdk/[email protected](@aws-sdk/[email protected])(@aws-sdk/[email protected])':
dependencies:
'@aws-sdk/client-sts': 3.577.0
'@aws-sdk/client-sts': 3.577.0(@aws-sdk/[email protected])
'@aws-sdk/credential-provider-env': 3.577.0
'@aws-sdk/credential-provider-process': 3.577.0
'@aws-sdk/credential-provider-sso': 3.577.0(@aws-sdk/[email protected])
@@ -8716,7 +8744,7 @@ snapshots:
'@aws-sdk/[email protected](@aws-sdk/[email protected])':
dependencies:
'@aws-sdk/client-sts': 3.577.0
'@aws-sdk/client-sts': 3.577.0(@aws-sdk/[email protected])
'@aws-sdk/types': 3.577.0
'@smithy/property-provider': 3.0.0
'@smithy/types': 3.0.0
@@ -8843,7 +8871,7 @@ snapshots:
'@aws-sdk/[email protected](@aws-sdk/[email protected])':
dependencies:
'@aws-sdk/client-sso-oidc': 3.577.0(@aws-sdk/[email protected])
'@aws-sdk/client-sso-oidc': 3.577.0
'@aws-sdk/types': 3.577.0
'@smithy/property-provider': 3.0.0
'@smithy/shared-ini-file-loader': 3.0.0
@@ -12771,10 +12799,6 @@ snapshots:
dependencies:
undici-types: 5.26.5
'@types/[email protected]':
dependencies:
'@types/node': 20.12.12
'@types/[email protected]':
dependencies:
'@types/node': 20.12.12
@@ -16124,7 +16148,8 @@ snapshots:
[email protected]: {}
[email protected]: {}
[email protected]:
optional: true
[email protected]:
dependencies:
+16 -5
View File
@@ -1,25 +1,26 @@
import {
Body,
Button,
Container,
Head,
Heading,
Hr,
Html,
Preview,
Section,
Tailwind,
Text,
} from "@react-email/components";
import type { TSchema } from "../jobs/esign-confirmation-email";
type Payload = Omit<TSchema, "fileUrl">;
const ESignConfirmationEmail = ({
documentName,
recipient,
senderName,
company,
}: Payload) => {
fileUrl,
}: TSchema) => {
return (
<Html>
<Head />
@@ -40,10 +41,19 @@ const ESignConfirmationEmail = ({
<Text className="mt-5 text-[14px] leading-[24px] text-black">
All parties have completed and signed the document -{" "}
<strong>{documentName}</strong>. Please find the attached
document.
<strong>{documentName}</strong>. The download link below works
for 7 days.
</Text>
<Section className="mx-auto my-[32px] text-center">
<Button
className="rounded bg-black px-5 py-3 text-center text-[12px] font-semibold text-white no-underline"
href={fileUrl}
>
Download the signed document
</Button>
</Section>
<Text className="mt-[20px] !text-[14px] leading-[24px] text-black">
- {senderName}
</Text>
@@ -60,6 +70,7 @@ const ESignConfirmationEmail = ({
};
ESignConfirmationEmail.PreviewProps = {
fileUrl: "https://example.com/document.pdf",
documentName: "Document Name",
recipient: { name: "Recipient Name", email: "" },
senderName: "Sender Name",
-4
View File
@@ -38,8 +38,6 @@ export const env = createEnv({
.transform((s) => s !== "false" && s !== "0"),
NEXTAUTH_URL: z.string(),
NEXTAUTH_SECRET: z.string(),
EMAIL_SERVER: z.string().optional(),
EMAIL_FROM: z.string(),
// upload
UPLOAD_ENDPOINT: z.string(),
@@ -89,8 +87,6 @@ export const env = createEnv({
DATABASE_URL: process.env.DATABASE_URL,
NEXTAUTH_URL: process.env.NEXTAUTH_URL,
NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET,
EMAIL_SERVER: process.env.EMAIL_SERVER,
EMAIL_FROM: process.env.EMAIL_FROM,
UPLOAD_ENDPOINT: process.env.UPLOAD_ENDPOINT,
UPLOAD_REGION: process.env.UPLOAD_REGION,
+2 -1
View File
@@ -66,7 +66,8 @@ export const eSignCompletePDFWorker = defineWorker(config, async (job) => {
});
});
const file = await getPresignedGetUrl(bucketData.key);
// The emailed download link: seven days is the longest a SigV4 URL lives.
const file = await getPresignedGetUrl(bucketData.key, 7 * 24 * 60 * 60);
await eSignConfirmationEmailJob.bulkEmit(
recipients.map((recipient) => ({
+1 -11
View File
@@ -39,23 +39,13 @@ export const eSignConfirmationEmailWorker = defineWorker(
senderName: payload.senderName,
senderEmail: payload.senderEmail,
company: payload.company,
fileUrl: payload.fileUrl,
}),
);
await sendMail({
to: payload.recipient.email,
...(payload.senderEmail && { replyTo: payload.senderEmail }),
subject: "Completed e-signed documents from all parties",
html,
attachments: [
{
filename: payload.documentName,
path: payload.fileUrl,
},
],
headers: {
"X-From-Name": payload.senderName || "Captable",
},
});
},
);
-4
View File
@@ -98,12 +98,8 @@ export const eSignNotificationEmailWorker = defineWorker(
);
await sendMail({
to: email,
...(sender?.email && { replyTo: sender.email }),
subject: "eSign Document Request",
html,
headers: {
"X-From-Name": sender?.name || "Captable",
},
});
},
);
-6
View File
@@ -27,11 +27,9 @@ export const shareDataRoomEmailWorker = defineWorker(config, async (job) => {
recipientName,
senderName,
email,
senderEmail,
} = job.data;
await sendMail({
to: email,
...(senderEmail && { replyTo: senderEmail }),
subject: `${senderName} shared a data room - ${dataRoom}`,
html: await render(
ShareDataRoomEmail({
@@ -42,9 +40,5 @@ export const shareDataRoomEmailWorker = defineWorker(config, async (job) => {
link,
}),
),
headers: {
"X-From-Name": senderName,
},
});
});
-6
View File
@@ -29,11 +29,9 @@ export const shareUpdateEmailWorker = defineWorker(config, async (job) => {
recipientName,
senderName,
email,
senderEmail,
} = job.data;
await sendMail({
to: email,
...(senderEmail && { replyTo: senderEmail }),
subject: `${senderName} shared an update - ${update.title}`,
html: await render(
ShareUpdateEmail({
@@ -44,9 +42,5 @@ export const shareUpdateEmailWorker = defineWorker(config, async (job) => {
link,
}),
),
headers: {
"X-From-Name": senderName,
},
});
});
+5 -4
View File
@@ -89,7 +89,10 @@ export const getPresignedPutUrl = async ({
return { url, key: Key, bucketUrl: bucketUrl.toString() };
};
export const getPresignedGetUrl = async (key: string) => {
export const getPresignedGetUrl = async (
key: string,
expiresIn = TEN_MINUTES_IN_SECONDS,
) => {
const getObjectCommand = new GetObjectCommand({
Bucket: PrivateBucket,
Key: key,
@@ -97,9 +100,7 @@ export const getPresignedGetUrl = async (key: string) => {
ResponseContentDisposition: "inline",
});
const url = await getSignedUrl(S3, getObjectCommand, {
expiresIn: TEN_MINUTES_IN_SECONDS,
});
const url = await getSignedUrl(S3, getObjectCommand, { expiresIn });
return { key, url };
};
+54 -27
View File
@@ -1,32 +1,59 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-return */
import { captableApiBase } from "@/server/captable-api";
import { env } from "@/env";
import { type SendMailOptions, createTransport } from "nodemailer";
type Mail = { to: string; subject: string; html: string };
const getTransport = () => {
return createTransport({
url: env.EMAIL_SERVER,
const IAM_URL = process.env.IAM_URL || "https://hanzo.id";
/** This app's own IAM identity, minted with the client-credentials grant. */
async function appToken(): Promise<string> {
const basic = Buffer.from(
`${process.env.IAM_CLIENT_ID}:${process.env.IAM_CLIENT_SECRET}`,
).toString("base64");
const res = await fetch(`${IAM_URL}/v1/iam/oauth/token`, {
method: "POST",
headers: {
authorization: `Basic ${basic}`,
"content-type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({ grant_type: "client_credentials" }),
cache: "no-store",
});
};
type RecordType = Record<string, string | undefined>;
export const sendMail = (options: Omit<SendMailOptions, "from">) => {
let from = `Captable <${env.EMAIL_FROM}>`;
const headers = (options.headers || {}) as RecordType;
const senderName = headers["X-From-Name"];
if (senderName) {
from = `${senderName} <${env.EMAIL_FROM}>`;
const body = (await res.json().catch(() => ({}))) as {
access_token?: string;
error?: string;
};
if (!body.access_token) {
throw new Error(`IAM token refused (${res.status}): ${body.error ?? ""}`);
}
return body.access_token;
}
const transport = getTransport();
return transport.sendMail({
from,
...options,
});
};
/**
* Delivers one email through Hanzo notify (POST /v1/notify/send/email). notify
* sends with the email provider and sender address configured for this app's
* org, so there is no SMTP server or from-address here.
*/
export async function sendMail({ to, subject, html }: Mail): Promise<void> {
const res = await fetch(
`${captableApiBase()}/v1/notify/send/email?sync=true`,
{
method: "POST",
headers: {
authorization: `Bearer ${await appToken()}`,
"content-type": "application/json",
},
body: JSON.stringify({ to: [to], subject, body: html, sync: "true" }),
cache: "no-store",
},
);
const out = (await res.json().catch(() => ({}))) as {
status?: string;
error?: string;
detail?: string;
};
if (!res.ok || out.status === "failed") {
throw new Error(
`notify did not send "${subject}" (${res.status}): ${out.error ?? out.detail ?? ""}`,
);
}
}