Files
zeekay afbe7410a9 The sign-in email's mark, and the link it carries, gated
IAM's sign-in mail loads its mark from the org's site at
/email/mark.png, so the export serves one: @hanzo/brand's five paths,
white, on an opaque black tile, 132px for a 44px place.  An email client
shows no SVG, and an opaque tile reads on a light page, a dark one, and
in a client that inverts the page but never an image.  It is derived by
scripts/email-mark.mjs, never drawn.

e2e/gates/link.spec.ts drives the link the mail carries, the way a
person meets it: opened beside the waiting tab, which signs in and the
link's tab follows; opened in the browser that asked with no tab
waiting, which signs in where it opened; opened in another browser,
which shows the code and spends nothing, and the code typed where the
sign-in began, in a field the browser fills (one-time-code, numeric).
The mark is held served, byte for byte.
2026-10-22 18:50:55 -07:00

47 lines
2.0 KiB
JavaScript

/**
* The mark Hanzo's emails show, at https://hanzo.ai/email/mark.png.
*
* IAM's sign-in mail (hanzoai/iam `internal/otp/signin.go`, `MarkPath`) loads
* the mark from the org's site at /email/mark.png. An email client shows no SVG
* (Gmail drops it), so this is a PNG: @hanzo/brand's five paths, white, on an
* opaque black tile with its corners cut, 132px square for a 44px place. The
* tile is opaque so the mark reads on a light page, on a dark one, and in a
* client that inverts the page's colours but never an image's.
*
* Derived, never drawn: run `node scripts/email-mark.mjs` when @hanzo/brand's
* mark changes, and commit the PNG it writes.
*/
import { readFileSync } from 'node:fs'
import { createRequire } from 'node:module'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { chromium } from '@playwright/test'
const SIZE = 132
const RADIUS = 30
const GLYPH = 72
const here = dirname(fileURLToPath(import.meta.url))
const OUT = join(here, '..', 'public', 'email', 'mark.png')
const FROM = createRequire(import.meta.url).resolve('@hanzo/brand/assets/logo/favicon.svg')
const svg = readFileSync(FROM, 'utf8')
const paths = svg.match(/<path [^>]*\/>/g) ?? []
if (paths.length !== 5) throw new Error(`the mark should be five paths, found ${paths.length}`)
if (/stroke/.test(svg)) throw new Error('the mark carries a stroke, which bridges its gaps')
const page = `<!doctype html><html><body style="margin:0;background:transparent">
<div id="mark" style="width:${SIZE}px;height:${SIZE}px;border-radius:${RADIUS}px;background:#000;display:grid;place-items:center">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 67 67" width="${GLYPH}" height="${GLYPH}" fill="#fff">${paths.join('')}</svg>
</div></body></html>`
const browser = await chromium.launch()
try {
const tab = await browser.newPage({ deviceScaleFactor: 1 })
await tab.setContent(page)
await tab.locator('#mark').screenshot({ path: OUT, omitBackground: true })
} finally {
await browser.close()
}
console.log(`wrote ${OUT}`)