mirror of
https://github.com/luxfi/aml.git
synced 2026-09-24 00:25:20 +00:00
fix(type): the console carries its own faces, because the CDN never had them
The shipped default fetched Zen from cdn.hanzo.ai. Nothing publishes it there: every /fonts/zen/* path answers 404, while /fonts/geist/1.7.2/ still answers 200 because those bytes were put there by hand. So every first paint of this console asked for two files that are not there and fell to the system stack — in a compliance product whose own tests assert it is set in Zen. The self-hosting path already existed as FONT_ORIGIN=, and it is now the default: vite emits the two faces out of @hanzo/font at the same paths, served same-origin. The CDN stays reachable through FONT_ORIGIN for the day the fleet publishes there, and the URL contract is still pinned so a rename cannot pass unnoticed. Three things follow, and each is a tightening rather than a trade: font-src drops its host. A cross-origin face has to be named in the policy; a face we carry does not, so the cheapest possible widening of a CSP is now no widening. csp.txt and the chart's copy move together, as they must. The cross-origin assertion gets stronger. It used to allow exactly one origin, ours; it now expects the set to be EMPTY — no subresource loaded from anywhere but here. An origin nothing fetches from cannot serve the wrong bytes, go down, or be asked for a beacon. And the pin moves to @hanzo/font 1.9.2. 1.9.0 and 1.9.1 shipped a zen.css whose :root block was missing, so the two custom properties a non-Next consumer reads were undefined — which computes to an invalid value and falls to the OS stack, silently.
This commit is contained in:
+1
-1
@@ -1 +1 @@
|
||||
default-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'; object-src 'none'; script-src 'self'; style-src 'self' 'sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU='; img-src 'self' data:; font-src 'self' https://cdn.hanzo.ai; connect-src 'self' https://api.hanzo.ai https://hanzo.id; manifest-src 'self'
|
||||
default-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'; object-src 'none'; script-src 'self'; style-src 'self' 'sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU='; img-src 'self' data:; font-src 'self'; connect-src 'self' https://api.hanzo.ai https://hanzo.id; manifest-src 'self'
|
||||
|
||||
+30
-28
@@ -83,8 +83,9 @@ test.describe('the typeface', () => {
|
||||
// Once each. Both files are preloaded; a preload whose CORS mode disagrees
|
||||
// with the fetch that follows is not a saved request but a duplicated one,
|
||||
// and the only way to tell is to count.
|
||||
const self = new URL(page.url()).origin
|
||||
for (const f of faces) {
|
||||
const hits = seen.filter((r) => r.url === `${cdn}${dir}/${f.file}`)
|
||||
const hits = seen.filter((r) => r.url === `${self}${dir}/${f.file}`)
|
||||
expect(hits.length, `${f.file} was fetched ${hits.length} times`).toBe(1)
|
||||
expect(hits[0].type).toBe('font')
|
||||
}
|
||||
@@ -193,19 +194,20 @@ test.describe('the typeface', () => {
|
||||
const here = new URL(page.url()).origin
|
||||
const away = seen.filter((r) => new URL(r.url).origin !== here)
|
||||
|
||||
// Two kinds of cross-origin traffic, and the policy already draws the line
|
||||
// between them: `connect-src` governs what the console ASKS (the identity
|
||||
// it was already allowed to reach), `font-src` governs what it LOADS. This
|
||||
// change adds to the second, so that is what is pinned: every subresource
|
||||
// fetched from another origin is a font, and every one of them is ours.
|
||||
// Two kinds of cross-origin traffic, and the policy draws the line between
|
||||
// them: `connect-src` governs what the console ASKS (the identity it was
|
||||
// already allowed to reach), `font-src` governs what it LOADS. The console
|
||||
// carries its own faces now, so the second set is EMPTY — no subresource is
|
||||
// loaded from anywhere but here. That is a stronger claim than the one this
|
||||
// replaced, which allowed our CDN: an origin nothing fetches from cannot
|
||||
// serve the wrong bytes, go down, or be asked for a beacon.
|
||||
const loads = away.filter((r) => r.type !== 'xhr' && r.type !== 'fetch')
|
||||
expect([...new Set(loads.map((r) => new URL(r.url).origin))]).toEqual([cdn])
|
||||
expect([...new Set(loads.map((r) => r.type))]).toEqual(['font'])
|
||||
expect(loads.map((r) => r.url)).toEqual([])
|
||||
|
||||
// And the CDN is asked for the two files and for nothing else — no
|
||||
// stylesheet, no script, no beacon riding along on the host we just let in.
|
||||
const paths = [...new Set(away.filter((r) => new URL(r.url).origin === cdn).map((r) => new URL(r.url).pathname))]
|
||||
expect(paths.sort()).toEqual(faces.map((f) => `${dir}/${f.file}`).sort())
|
||||
// And the faces really are here, at the paths the bundle emits them to.
|
||||
const fonts = seen.filter((r) => r.type === 'font')
|
||||
expect([...new Set(fonts.map((r) => new URL(r.url).pathname))].sort())
|
||||
.toEqual(faces.map((f) => `${dir}/${f.file}`).sort())
|
||||
})
|
||||
|
||||
test('no face, and no preload, is refused by the policy', async ({ page }) => {
|
||||
@@ -235,27 +237,27 @@ test.describe('the typeface', () => {
|
||||
// Written out in full, on purpose, and this is the one place in the suite
|
||||
// that does not derive them from src/font.ts.
|
||||
//
|
||||
// These are not this console's to choose. They are the fleet's: the same
|
||||
// two files, at the same two URLs, cached once for every Hanzo property,
|
||||
// published to our CDN by the kit track. A console that quietly renames a
|
||||
// file still passes every other test here — its own mock serves whatever it
|
||||
// asks for — and then 404s in production against the real CDN. So the
|
||||
// contract is stated where changing it means editing the agreement.
|
||||
// Not fetched by a shipped build — the console self-hosts — but this is the
|
||||
// agreement `FONT_ORIGIN=https://cdn.hanzo.ai` would be pointed at, and it
|
||||
// is pinned here so a rename cannot go unnoticed. Nothing publishes Zen to
|
||||
// that CDN today: every /fonts/zen/* path 404s while /fonts/geist/1.7.2/
|
||||
// still answers, which is why the default moved off it.
|
||||
//
|
||||
// Immutable by version: nothing at these paths is ever rewritten, which is
|
||||
// what earns them `max-age=31536000, immutable`. A new Zen is a new
|
||||
// directory beside this one, never a new file inside it.
|
||||
expect(faces.map((f) => `${cdn}${dir}/${f.file}`)).toEqual([
|
||||
'https://cdn.hanzo.ai/fonts/zen/1.9.1/ZenVariable.woff2',
|
||||
'https://cdn.hanzo.ai/fonts/zen/1.9.1/ZenMonoVariable.woff2',
|
||||
'https://cdn.hanzo.ai/fonts/zen/1.9.2/ZenVariable.woff2',
|
||||
'https://cdn.hanzo.ai/fonts/zen/1.9.2/ZenMonoVariable.woff2',
|
||||
])
|
||||
})
|
||||
|
||||
test('the CDN costs the policy exactly one host, in exactly one directive', () => {
|
||||
test('the policy names no font host at all', () => {
|
||||
// The policy is the deployed one (e2e/serve.mjs sends this file verbatim),
|
||||
// so this is a check on what ships. A font host is the cheapest possible
|
||||
// widening of a CSP and also the easiest to widen by more than intended —
|
||||
// one `*` or one extra directive and the fix has cost more than the bug.
|
||||
// so this is a check on what ships. It used to widen `font-src` by one host
|
||||
// for the CDN; the console carries its own faces, so the cheapest widening
|
||||
// of a CSP is now no widening — and a host nothing fetches from cannot
|
||||
// serve the wrong bytes or be asked for a beacon.
|
||||
const directives = new Map(
|
||||
csp
|
||||
.split(';')
|
||||
@@ -267,7 +269,7 @@ test.describe('the typeface', () => {
|
||||
}),
|
||||
)
|
||||
|
||||
expect(directives.get('font-src')).toBe("'self' https://cdn.hanzo.ai")
|
||||
expect(directives.get('font-src')).toBe("'self'")
|
||||
expect(directives.get('default-src')).toBe("'none'")
|
||||
expect(directives.get('style-src')).toBe("'self' 'sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU='")
|
||||
expect(directives.get('script-src')).toBe("'self'")
|
||||
@@ -275,10 +277,10 @@ test.describe('the typeface', () => {
|
||||
expect(csp).not.toContain('unsafe-eval')
|
||||
expect(csp).not.toContain('*')
|
||||
|
||||
// The CDN is in font-src and nowhere else: it may serve this console faces
|
||||
// and it may not serve it code, style, or a place to send data.
|
||||
// And the CDN is in no directive at all — not font-src, not anywhere. The
|
||||
// build fetches nothing from it, so naming it would be a permission granted
|
||||
// to a host nobody asks.
|
||||
for (const [name, value] of directives) {
|
||||
if (name === 'font-src') continue
|
||||
expect(value, `${name} names the CDN`).not.toContain('cdn.hanzo.ai')
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@
|
||||
"e2e:browser": "playwright install --with-deps chromium"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hanzo/font": "^1.9.1",
|
||||
"@hanzo/font": "^1.9.2",
|
||||
"@hanzo/gui": "8.0.0",
|
||||
"@hanzo/iam": "^0.21.5",
|
||||
"@hanzogui/button": "8.0.0",
|
||||
|
||||
+9
-8
@@ -19,14 +19,15 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* Our own CDN, and the only origin this console will fetch a face from unless
|
||||
* it is told otherwise.
|
||||
* Our own CDN — an ORIGIN this console can be pointed at, and no longer the one
|
||||
* it uses by default.
|
||||
*
|
||||
* It is ours — the `cdn` app in front of our S3 — which is the whole difference
|
||||
* between this and a third-party font host: one copy, one version, one cache,
|
||||
* every property, and no request to anybody else's infrastructure. It already
|
||||
* answers with `access-control-allow-origin: *`, which a cross-origin face
|
||||
* requires and which is the reason a font host cannot simply be any static host.
|
||||
* It is ours, the `cdn` app in front of our S3, and it answers with
|
||||
* `access-control-allow-origin: *`, which a cross-origin face requires. What it
|
||||
* does not do is carry Zen: every `/fonts/zen/*` path answers 404 today, while
|
||||
* `/fonts/geist/1.7.2/` still answers 200, because the bytes there were put
|
||||
* there by hand and nothing publishes ours. So the default is self-hosting from
|
||||
* the package (see vite.config.ts) and this stays for `FONT_ORIGIN`.
|
||||
*/
|
||||
export const cdn = 'https://cdn.hanzo.ai'
|
||||
|
||||
@@ -37,7 +38,7 @@ export const cdn = 'https://cdn.hanzo.ai'
|
||||
* at an old one. That is what lets the files be cached for a year: nothing at
|
||||
* a given path is ever rewritten, so nothing cached from it can go stale.
|
||||
*/
|
||||
export const version = '1.9.1'
|
||||
export const version = '1.9.2'
|
||||
|
||||
/** The immutable directory the two faces live in, on whichever origin serves them. */
|
||||
export const dir = `/fonts/zen/${version}`
|
||||
|
||||
+17
-10
@@ -5,26 +5,33 @@ import { dirname, join } from 'node:path'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import { defineConfig, type Plugin } from 'vite'
|
||||
|
||||
import { cdn, dir, faces, url } from './src/font.ts'
|
||||
import { dir, faces, url } from './src/font.ts'
|
||||
|
||||
/**
|
||||
* Where this build fetches its typeface from.
|
||||
*
|
||||
* Unset — every build we ship — is our own CDN: one copy of Zen, one version,
|
||||
* one cache, shared by every Hanzo property, and never a request to a font host
|
||||
* that is not ours.
|
||||
* SELF-HOSTED by default: vite emits the two faces out of `@hanzo/font` into
|
||||
* this bundle and serves them same-origin, which `font-src 'self'` already
|
||||
* covers. That is what every other Hanzo surface does — the package IS the one
|
||||
* copy of Zen, and a build that carries its own faces cannot be broken by
|
||||
* anything outside it.
|
||||
*
|
||||
* `FONT_ORIGIN=` (set, empty) is the escape hatch, and it is a build argument
|
||||
* rather than a fork: an on-prem install that cannot reach us serves the same
|
||||
* two files, at the same paths, from its own origin. Same family names, same
|
||||
* token names, same @font-face rules, same stylesheet — only the origin moves,
|
||||
* and `font-src 'self'` already covers it.
|
||||
* The default USED to be `cdn`, on the reasoning that one shared copy beats one
|
||||
* per property. The reasoning is sound and the path does not exist: nothing
|
||||
* publishes Zen to it, and every `/fonts/zen/*` URL answers 404 today while
|
||||
* `/fonts/geist/1.7.2/` still answers 200. So the shipped default fetched two
|
||||
* faces that are not there and every first paint fell to the system stack. The
|
||||
* kit reached the same conclusion and removed its own CDN rules for it.
|
||||
*
|
||||
* `FONT_ORIGIN=https://cdn.hanzo.ai` opts back in, unchanged, for the day the
|
||||
* fleet publishes that path — a build argument rather than a fork, same family
|
||||
* names, same token names, same @font-face rules, only the origin moves.
|
||||
*
|
||||
* It is fixed at build because a face is needed for the first paint, and
|
||||
* anything the console would have to ask for first arrives too late to set the
|
||||
* first frame in.
|
||||
*/
|
||||
const origin = process.env.FONT_ORIGIN ?? cdn
|
||||
const origin = process.env.FONT_ORIGIN ?? ''
|
||||
|
||||
/**
|
||||
* The typeface, delivered.
|
||||
|
||||
Reference in New Issue
Block a user