mirror of
https://github.com/luxfi/aml.git
synced 2026-09-24 00:25:20 +00:00
The serif is gone, but what replaced it was the machine's own sans — whatever
system-ui happens to resolve to, which is a different face on every desktop the
console is opened on. Geist Sans is the UI face and Geist Mono the monospace one
across every Hanzo property, so this console is set in them too, and fetches
them from cdn.hanzo.ai: one copy, one version, one cache entry shared with every
other property, and no request to a font host that is not ours.
The stacks are NOT written down here. They are read from @hanzo/gui's font
packages, because a stack copied into an app is a second source of truth that
does not fail when the kit's changes — it quietly stops matching the other
hundred properties. src/font.ts owns only DELIVERY: which release, which files,
which origin, and the @font-face rules. The face is registered under the family
its own stack leads with rather than under a name repeated beside it, so the
face installed and the family asked for cannot drift apart.
Three faults were found on the way and are each fixed and each pinned:
- The kit config had no `mono`, and ui.tsx asks it for `$mono`. A font token
the config does not define resolves to nothing.
- `.mono` lost to the kit. The kit dresses its own with
`.font_body, … { font-family: var(--f-family) }` — one class, in an adopted
stylesheet the cascade places after app.css — so a bare `.mono` tied on
specificity and lost on order, and machine text asked for through the kit
came out in the UI face: an id or a rate in proportional digits, in a
column, which is exactly where it shows. `:root :is(…)` settles it on
specificity, which no ordering can undo.
- The preload needed `crossorigin` even where the origin is our own. A font is
always fetched in CORS mode, and a preload that disagrees is a second
download rather than a saved one.
The policy gains one host in one directive — `font-src 'self'
https://cdn.hanzo.ai` — and is byte-identical otherwise. No unsafe-inline, no
unsafe-eval, no second directive, and the CDN may serve this console faces and
not code, style, or a place to send data. The faces go on through the CSSOM with
the kit's stylesheet, so style-src still sees no stylesheet parsed from markup.
An install that cannot reach us builds with `FONT_ORIGIN=` and serves the same
two files, at the same paths, from its own origin, with the OFL text beside
them — a build argument, not a fork. It is built and driven in the suite rather
than reasoned about, because an escape hatch nobody exercises is found broken by
a customer, on an install, with no network to debug it over.
Geist 1.7.2, Vercel and basement.studio, SIL Open Font License 1.1 — confirmed
against the licence shipped with the exact files hosted, which are the pinned
`geist` package's and not a copy of them.
Every assertion is mutation-proven: put back the serif-era selector, the old
policy, a drifted filename, proportional figures or a bare preload, and it is
the named test that fails. 22 green. Bundle +1,808 B; the two variable faces are
141 kB off-bundle, fetched once and cached for a year.
Co-authored-by: Hanzo Dev <[email protected]>
41 lines
1.5 KiB
TypeScript
41 lines
1.5 KiB
TypeScript
// A signed-in console, seeded the way the SDK stores a session.
|
|
//
|
|
// Shared by every spec that needs the signed-in tree, because the seeding has
|
|
// to match what @hanzo/iam actually reads. Two copies of it would drift, and a
|
|
// drifted copy does not fail — it silently tests the sign-in gate instead of
|
|
// the six screens behind it.
|
|
|
|
import type { Page } from '@playwright/test'
|
|
|
|
import { serveFonts } from './cdn'
|
|
|
|
export async function signIn(page: Page) {
|
|
// Every origin this console reaches is answered locally, so the run is
|
|
// offline and its result is about the console rather than about the network.
|
|
// The faces come first because they are fetched at first paint, before
|
|
// anything below has been read.
|
|
await serveFonts(page)
|
|
|
|
await page.addInitScript(() => {
|
|
const hour = Date.now() + 60 * 60 * 1000
|
|
sessionStorage.setItem('hanzo_iam_access_token', 'at-e2e')
|
|
sessionStorage.setItem('hanzo_iam_refresh_token', 'rt-e2e')
|
|
sessionStorage.setItem('hanzo_iam_expires_at', String(hour))
|
|
})
|
|
// The issuer is a real origin the policy allows and this machine cannot
|
|
// reach. Answering it here keeps the run offline without relaxing anything:
|
|
// the request is still made, and still has to pass connect-src to be made.
|
|
await page.route('https://hanzo.id/**', (route) =>
|
|
route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({
|
|
sub: 'u-1',
|
|
name: 'A. Mensah',
|
|
email: '[email protected]',
|
|
owner: 'hanzo/acme',
|
|
}),
|
|
}),
|
|
)
|
|
}
|