openpencil/vite/font-proxy.ts

84 lines
2.9 KiB
TypeScript
Raw Normal View History

2026-08-26 22:42:41 +00:00
import type { Plugin } from 'vite'
// Dev-only same-origin proxy for CORS-hostile online-font catalog endpoints.
//
// unifont's `google` provider reads `https://fonts.google.com/metadata/fonts` to
// build its family catalog. That endpoint never sends `Access-Control-Allow-Origin`
// and sets `cross-origin-resource-policy: same-site`, so a browser app served on
// its own origin (any origin that is not Google) cannot read it directly — the
// provider fails to initialize with "Could not initialize provider `google`".
//
// The browser relays such requests here instead (see `createFontProxyFetch()` in
// src/app/editor/fonts), which keeps the request same-origin and forwards it
// server-side to Google. This is a W4C fork delta; upstream openpencil does not
// ship it because Tauri/goes through the Rust command that is not CORS-bound.
export const FONT_PROXY_PREFIX = '/__font-proxy'
// Only these hosts are CORS-hostile from the browser; everything else
// (notably fonts.gstatic.com glyph files) already sends CORS headers and is
// fetched directly by the browser.
const PROXIED_HOSTS = new Set(['fonts.google.com'])
export function createDevFontProxyPlugin(): Plugin {
return {
name: 'open-pencil-dev-font-proxy',
configureServer(server) {
server.middlewares.use(async (req, res, next) => {
let pathname: string
try {
pathname = new URL(req.url ?? '/', 'http://dev').pathname
} catch {
next()
return
}
if (pathname !== FONT_PROXY_PREFIX) {
next()
return
}
const target = new URL(req.url ?? '/', 'http://dev').searchParams.get('url')
if (!target) {
res.statusCode = 400
res.end('missing url')
return
}
let targetURL: URL
try {
targetURL = new URL(decodeURIComponent(target))
} catch {
res.statusCode = 400
res.end('invalid url')
return
}
if (targetURL.protocol !== 'https:' || !PROXIED_HOSTS.has(targetURL.host)) {
res.statusCode = 403
res.end('proxied host denied')
return
}
try {
const upstream = await fetch(targetURL, {
headers: {
'user-agent':
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126 Safari/537.36',
accept: 'application/json, text/plain, */*',
},
redirect: 'follow',
})
const body = Buffer.from(await upstream.arrayBuffer())
res.statusCode = upstream.status
const type = upstream.headers.get('content-type')
if (type) res.setHeader('content-type', type)
res.setHeader('access-control-allow-origin', '*')
res.end(body)
} catch (e) {
res.statusCode = 502
res.end(String(e))
}
})
},
}
}