From 926005d2da338aee8fb8876227980b3a358757d3 Mon Sep 17 00:00:00 2001 From: Pulkit Pareek Date: Sat, 16 May 2026 16:24:47 +0530 Subject: [PATCH] landing: gated whitepaper, bigger logo, taller demo, footer socials, drop status MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Frontend (public/index.html) - Hero: drop the "ZeroAuth · Live in production" eyebrow; the serif headline leads directly now. - Brand mark: 32 → 44 px in header + footer (34 px on the smallest mobile breakpoint). - Live demo iframe height bumped from clamp(440, 64vh, 680) to clamp(600, 82vh, 880) so more of the simulation is visible without scrolling. - Remove the "Running locally · No real biometric collected" caption under the iframe. - Replace the direct whitepaper download buttons with an email-gate form: input + "Send PDF" → posts to /api/leads/whitepaper. - Drop the "All systems operational" link in the footer-bottom. - Add LinkedIn / GitHub / X social icon row in the footer-bottom, pointing at linkedin.com/company/zeroauth-dev, github.com/zeroauth-dev, and x.com/zeroauth-dev respectively. Backend - src/services/email.ts: extend SendMailInput with an optional `attachments` array (filename + path|content + contentType). Pass through to nodemailer's mail options. - src/services/email-templates.ts: add whitepaperEmail() returning the subject + html + text body for the gated-download mailer. - src/routes/leads.ts: after persisting the lead, resolve the PDF on disk across dev (website/static or docs) and prod (website/build) and fire-and-forget sendMail with the attachment. Response now reads "Whitepaper sent. Check your inbox in a minute." The downloadUrl is still returned for accessibility / fallback. - tests/leads.test.ts: assert the inbox-message wording and the new filename (ZeroAuth_Whitepaper.pdf). Trade-offs - The email is best-effort; if SMTP is unconfigured (dev) the call is a no-op but the request still 201s — the response is reflective of intent, not delivery guarantee. Tracking ACK comes later via a delivery webhook when one is wired. --- public/index.html | 177 +++++++++++++++++++++++++------- src/routes/leads.ts | 60 ++++++++++- src/services/email-templates.ts | 59 +++++++++++ src/services/email.ts | 14 +++ tests/leads.test.ts | 3 +- 5 files changed, 274 insertions(+), 39 deletions(-) diff --git a/public/index.html b/public/index.html index 67ff78e..d628357 100644 --- a/public/index.html +++ b/public/index.html @@ -201,7 +201,7 @@ letter-spacing: -0.01em; color: var(--ink); } - .brand img { width: 32px; height: 32px; } + .brand img { width: 44px; height: 44px; } .nav-links { display: flex; @@ -246,7 +246,7 @@ } @media (max-width: 480px) { .brand { font-size: 18px; gap: 10px; } - .brand img { width: 28px; height: 28px; } + .brand img { width: 34px; height: 34px; } .nav-cta { height: 36px; padding-inline: 14px; font-size: 11px; letter-spacing: 0.08em; } } @@ -263,7 +263,7 @@ border-radius: 999px; background: var(--status); } - .hero h1 { max-width: 18ch; margin-top: 20px; } + .hero h1 { max-width: 18ch; margin-top: 0; } .hero-lede { margin-top: 28px; max-width: 56ch; } .hero-actions { margin-top: 40px; @@ -332,28 +332,17 @@ } .demo-iframe { width: 100%; - height: clamp(440px, 64vh, 680px); + height: clamp(600px, 82vh, 880px); border: 0; } .demo-meta { display: flex; align-items: center; - justify-content: space-between; + justify-content: flex-end; margin-top: 20px; gap: 16px; flex-wrap: wrap; } - .demo-meta-left { - display: inline-flex; - align-items: center; - gap: 12px; - font-family: var(--font-mono); - font-size: 11px; - letter-spacing: 0.08em; - text-transform: uppercase; - color: var(--ink-3); - } - .demo-meta-left .dot { width: 6px; height: 6px; border-radius: 999px; background: var(--status); } /* ───── Quickstart with code tabs ───── */ @@ -808,7 +797,55 @@ margin-bottom: 6px; } .whitepaper-meta dd { margin: 0; font-size: 14px; color: var(--ink-inverse); } - .whitepaper-actions { margin-top: 28px; display: flex; flex-wrap: wrap; gap: 14px; } + .wp-form { margin-top: 28px; } + .wp-form-label { + display: block; + font-size: 11px; + letter-spacing: 0.18em; + text-transform: uppercase; + font-weight: 500; + color: var(--ink-inverse-2); + margin-bottom: 10px; + } + .wp-form-row { + display: flex; + gap: 12px; + flex-wrap: wrap; + align-items: stretch; + } + .wp-form input { + flex: 1 1 240px; + height: 48px; + padding: 0 16px; + background: transparent; + border: 1px solid var(--line-inverse); + color: var(--ink-inverse); + font-size: 14px; + font-family: var(--font-body); + border-radius: 0; + outline: none; + transition: border-color 180ms ease; + } + .wp-form input::placeholder { color: var(--ink-inverse-2); opacity: 0.7; } + .wp-form input:focus { border-color: var(--ink-inverse); } + .wp-form-submit { white-space: nowrap; } + .wp-form-note { + margin-top: 12px; + font-family: var(--font-body); + font-size: 12px; + color: var(--ink-inverse-2); + } + .wp-form-note.error { color: #fca5a5; } + .wp-success h4 { + font-family: var(--font-display); + font-weight: 400; + font-variation-settings: "opsz" 60; + font-size: 1.5rem; + letter-spacing: -0.02em; + margin-bottom: 8px; + color: var(--ink-inverse); + } + .wp-success p { color: var(--ink-inverse-2); } /* ───── Footer ───── */ @@ -845,11 +882,23 @@ flex-wrap: wrap; gap: 14px 28px; justify-content: space-between; + align-items: center; font-size: 12px; color: var(--ink-3); } - .footer-bottom .status { display: inline-flex; align-items: center; gap: 8px; } - .footer-bottom .status .dot { width: 6px; height: 6px; border-radius: 999px; background: var(--status); } + .footer-socials { display: inline-flex; align-items: center; gap: 12px; } + .footer-socials a { + display: inline-flex; + align-items: center; + justify-content: center; + width: 34px; + height: 34px; + border: 1px solid var(--line); + color: var(--ink-2); + transition: color 180ms ease, border-color 180ms ease, background 180ms ease; + } + .footer-socials a:hover { color: var(--bg); background: var(--ink); border-color: var(--ink); } + .footer-socials svg { width: 16px; height: 16px; } @@ -882,10 +931,6 @@
-
- - ZeroAuth · Live in production -

Authentication where a breach exposes nothing.

@@ -938,10 +983,6 @@

A signup, end to end.

- - - Running locally · No real biometric collected - Open fullscreen
@@ -1386,11 +1427,26 @@

The cryptographic foundation.

-
- - Download white paper - - View as HTML +
+ +
+ + +
+

36-page PDF, delivered to your inbox. We never share emails.

+
+
@@ -1449,10 +1505,23 @@

Company

@@ -1491,6 +1560,42 @@

Company

} })(); + // Whitepaper form — POST email to /api/leads/whitepaper, backend emails the PDF. + (function () { + var form = document.getElementById('wpForm'); + if (!form) return; + form.addEventListener('submit', async function (e) { + e.preventDefault(); + var emailInput = document.getElementById('wpEmail'); + var note = document.getElementById('wpFormNote'); + var btn = form.querySelector('button[type="submit"]'); + var email = (emailInput.value || '').trim(); + if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { + note.classList.add('error'); + note.textContent = 'Please enter a valid email address.'; + emailInput.focus(); + return; + } + note.classList.remove('error'); + note.textContent = 'Sending…'; + btn.disabled = true; + try { + var res = await fetch('/api/leads/whitepaper', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email: email }) + }); + if (!res.ok) throw new Error('http ' + res.status); + form.classList.add('hidden'); + document.getElementById('wpSuccess').classList.remove('hidden'); + } catch (err) { + btn.disabled = false; + note.classList.add('error'); + note.textContent = 'Could not send. Try again in a moment.'; + } + }); + })(); + // Pilot form submission (function () { var form = document.getElementById('pilotForm'); diff --git a/src/routes/leads.ts b/src/routes/leads.ts index d87386a..a8db450 100644 --- a/src/routes/leads.ts +++ b/src/routes/leads.ts @@ -1,10 +1,38 @@ import { Router, Request, Response } from 'express'; +import fs from 'fs'; +import path from 'path'; import { authenticateAdmin } from '../middleware/auth'; import { logger } from '../services/logger'; import { getPoolOrNull } from '../services/db'; +import { sendMail } from '../services/email'; +import { whitepaperEmail } from '../services/email-templates'; const router = Router(); +/** + * Resolve the whitepaper PDF path across environments. The Dockerfile builds + * the docs site into website/build/, so production reads from there. In dev + * we fall back to the source PDFs in website/static or docs/. Resolved once + * at first use; null means no PDF is shipped with this build. + */ +let whitepaperPathCache: string | null | undefined; +function resolveWhitepaperPath(): string | null { + if (whitepaperPathCache !== undefined) return whitepaperPathCache; + const candidates = [ + path.resolve(__dirname, '..', '..', 'website', 'build', 'whitepaper.pdf'), + path.resolve(__dirname, '..', '..', 'website', 'static', 'whitepaper.pdf'), + path.resolve(__dirname, '..', '..', 'docs', 'whitepaper.pdf'), + ]; + for (const p of candidates) { + if (fs.existsSync(p)) { + whitepaperPathCache = p; + return p; + } + } + whitepaperPathCache = null; + return null; +} + function isValidEmail(email: string): boolean { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); } @@ -83,11 +111,39 @@ router.post('/whitepaper', async (req: Request, res: Response) => { } logger.info('Whitepaper lead submitted', { email: trimmedEmail }); + + // Best-effort mail delivery — non-blocking for the response. If SMTP is + // unconfigured (dev) the call no-ops; the downloadUrl in the response is + // the fallback so the user can still read the paper. + const pdfPath = resolveWhitepaperPath(); + if (pdfPath) { + const { subject, html, text } = whitepaperEmail(); + void sendMail({ + to: trimmedEmail, + subject, + html, + text, + attachments: [ + { + filename: 'ZeroAuth_Whitepaper.pdf', + path: pdfPath, + contentType: 'application/pdf', + }, + ], + }).then((result) => { + if (!result.ok && !result.skipped) { + logger.warn('Whitepaper email send failed', { error: result.error }); + } + }); + } else { + logger.warn('Whitepaper PDF not found on disk — email will not include attachment'); + } + res.status(201).json({ success: true, - message: 'Whitepaper access granted.', + message: 'Whitepaper sent. Check your inbox in a minute.', downloadUrl: '/docs/whitepaper.pdf', - filename: 'Pramaan_Whitepaper.pdf', + filename: 'ZeroAuth_Whitepaper.pdf', }); }); diff --git a/src/services/email-templates.ts b/src/services/email-templates.ts index e58df58..dee7c01 100644 --- a/src/services/email-templates.ts +++ b/src/services/email-templates.ts @@ -136,6 +136,65 @@ ${input.attemptIp ? `\nAttempt source IP: ${input.attemptIp}\n` : ''}${FOOTER_TE return { subject, html, text }; } +/** + * Sent when someone requests the whitepaper from the landing page. The PDF + * is attached so the recipient never has to come back to a download page; + * the email itself is the delivery channel. + */ +export function whitepaperEmail(): { subject: string; html: string; text: string } { + const subject = 'Your ZeroAuth white paper'; + + const html = ` +
+

Your white paper is attached.

+

+ Thanks for requesting the ZeroAuth white paper. The PDF is attached to this + message — 36 pages covering the cryptographic construction, the security + reductions, the on-chain anchoring model, and the recommended deployment + topology. +

+

+ The underlying protocol is Pramaan (Indian Patent IN202311041001), + the zero-knowledge biometric identity scheme that powers ZeroAuth. +

+

+ If you have questions after reading, reply to this email or open an issue + at github.com/zeroauth-dev/ZeroAuth/issues. +

+

+ Useful next steps: +

+ + ${FOOTER_HTML} +
+ `; + + const text = `Your white paper is attached. + +Thanks for requesting the ZeroAuth white paper. The PDF is attached to this +message — 36 pages covering the cryptographic construction, the security +reductions, the on-chain anchoring model, and the recommended deployment +topology. + +The underlying protocol is Pramaan (Indian Patent IN202311041001), the +zero-knowledge biometric identity scheme that powers ZeroAuth. + +If you have questions after reading, reply to this email or open an issue at +https://github.com/zeroauth-dev/ZeroAuth/issues + +Useful next steps: +- Read the Quickstart: https://zeroauth.dev/docs/getting-started/quickstart/ +- Browse the API reference: https://zeroauth.dev/docs/reference/api-reference +- Self-host the reference implementation: https://github.com/zeroauth-dev/ZeroAuth +${FOOTER_TEXT}`; + + return { subject, html, text }; +} + /** * Minimal HTML escape for user-supplied strings landing in templates. * Don't use a full library for this — the surface is tiny (operator email diff --git a/src/services/email.ts b/src/services/email.ts index 8c780df..1e3d63b 100644 --- a/src/services/email.ts +++ b/src/services/email.ts @@ -41,6 +41,17 @@ function getTransporter(): Transporter | null { return transporter; } +export interface SendMailAttachment { + /** File name as the recipient will see it. */ + filename: string; + /** Absolute path on disk, OR provide `content` instead. */ + path?: string; + /** Raw bytes / string if path is unavailable. */ + content?: Buffer | string; + /** MIME type. Inferred when possible; pass explicitly for non-obvious files. */ + contentType?: string; +} + export interface SendMailInput { to: string; subject: string; @@ -49,6 +60,8 @@ export interface SendMailInput { text: string; /** Optional Reply-To override. Defaults to EMAIL_FROM. */ replyTo?: string; + /** Optional file attachments — used for whitepaper delivery, audit-pack exports, etc. */ + attachments?: SendMailAttachment[]; } export interface SendMailResult { @@ -86,6 +99,7 @@ export async function sendMail(input: SendMailInput): Promise { subject: input.subject, text: input.text, html: input.html, + attachments: input.attachments, }); logger.info('Email: sent', { messageId: info.messageId, diff --git a/tests/leads.test.ts b/tests/leads.test.ts index 1c463fb..823560b 100644 --- a/tests/leads.test.ts +++ b/tests/leads.test.ts @@ -133,8 +133,9 @@ describe('routes/leads — POST /api/leads/whitepaper', () => { expect(res.body).toMatchObject({ success: true, downloadUrl: '/docs/whitepaper.pdf', - filename: 'Pramaan_Whitepaper.pdf', + filename: 'ZeroAuth_Whitepaper.pdf', }); + expect(res.body.message).toMatch(/inbox/i); }); it('400s on missing email', async () => {