mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-28 07:32:52 +00:00
148 lines
6.9 KiB
JavaScript
148 lines
6.9 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Write hanzo.ai's edge rules into hanzoai/universe.
|
|
*
|
|
* lib/edge.ts is the rules; this renders them as the Middlewares and
|
|
* IngressRoutes of charts/app/values/hanzo/static-sites.yaml, between the two
|
|
* pairs of markers there, and leaves the rest of that file alone. Run it after
|
|
* MOVED, SHELVES or lib/data/retired.json change, commit universe, and sync the
|
|
* resources it names (it prints them).
|
|
*
|
|
* Usage: node scripts/edge.mjs <universe>/charts/app/values/hanzo/static-sites.yaml [--check]
|
|
*/
|
|
import { readFileSync, writeFileSync } from 'node:fs'
|
|
import { APP, CANONICAL, DATA, FRAME, MISSING, NOREFERRER, STUDIO_EDGE, WWW, kept, moved, shelves } from '../lib/edge.ts'
|
|
import { SHELVES } from '../lib/publish.ts'
|
|
|
|
const [file, mode] = process.argv.slice(2)
|
|
if (!file) {
|
|
console.error('usage: node scripts/edge.mjs <static-sites.yaml> [--check]')
|
|
process.exit(2)
|
|
}
|
|
|
|
const BEGIN = '# BEGIN hanzo.ai edge'
|
|
const END = '# END hanzo.ai edge'
|
|
const NOTE = `${BEGIN}: written by hanzo-apps/ai scripts/edge.mjs from lib/publish.ts and lib/data/retired.json. Regenerate, never edit.`
|
|
const HOSTS = '(Host(`hanzo.ai`) || Host(`www.hanzo.ai`))'
|
|
const q = (s) => `'${s.replace(/'/g, "''")}'`
|
|
|
|
const redirect = ({ name, regex, replacement }) =>
|
|
[`- name: ${name}`, ' spec:', ' redirectRegex:', ` regex: ${q(regex)}`, ` replacement: ${q(replacement)}`, ' permanent: true']
|
|
const chain = (name, members) => [`- name: ${name}`, ' spec:', ' chain:', ' middlewares:', ...members.map((m) => ` - name: ${m}`)]
|
|
const route = (name, match, priority, middlewares) => [
|
|
`- name: ${name}`,
|
|
' spec:',
|
|
' entryPoints:',
|
|
' - websecure',
|
|
' routes:',
|
|
` - match: ${q(match)}`,
|
|
' kind: Rule',
|
|
` priority: ${priority}`,
|
|
' middlewares:',
|
|
...middlewares.map((m) => ` - name: ${m}`),
|
|
' services:',
|
|
' - name: static-sink',
|
|
' port: 80',
|
|
' tls:',
|
|
' secretName: wildcard-hanzo-ai-tls',
|
|
]
|
|
|
|
const rules = moved()
|
|
const shelved = shelves()
|
|
const middlewares = [
|
|
NOTE,
|
|
'# A retired address answers with a 301 to its page in one hop, query kept; every',
|
|
'# page has one address (https, no www, no trailing slash, no .html); Next\'s own',
|
|
'# not-found files answer 404; every path under /dev is Dev\'s one page and every',
|
|
'# conversation under /chat is Chat\'s. hanzo-ai-edge',
|
|
'# is the chain the apex routes run.',
|
|
`- name: ${MISSING.name}`,
|
|
' spec:',
|
|
' replacePathRegex:',
|
|
` regex: ${q(MISSING.regex)}`,
|
|
` replacement: ${MISSING.replacement}`,
|
|
...APP.flatMap(({ name, regex, replacement }) => [`- name: ${name}`, ' spec:', ' replacePathRegex:', ` regex: ${q(regex)}`, ` replacement: ${replacement}`]),
|
|
...[CANONICAL, WWW, ...rules, ...shelved].flatMap(redirect),
|
|
...chain('hanzo-ai-moved', rules.map((r) => r.name)),
|
|
...chain('hanzo-ai-edge', [MISSING.name, 'hanzo-ai-moved', CANONICAL.name, WWW.name, ...APP.map((r) => r.name), 'hanzo-ai-csp', 'hanzo-ai-static']),
|
|
'# hanzo-ai-edge with Studio\'s policy, for /studio alone (studio-on-hanzo-ai).',
|
|
...chain(STUDIO_EDGE.name, [MISSING.name, 'hanzo-ai-moved', CANONICAL.name, WWW.name, ...APP.map((r) => r.name), STUDIO_EDGE.csp, 'hanzo-ai-static']),
|
|
'# The markdown twin of each page and Next\'s RSC payloads are for agents and the',
|
|
'# router, not the index: without this every page had an indexable duplicate.',
|
|
'- name: hanzo-ai-noindex',
|
|
' spec:',
|
|
' headers:',
|
|
' customResponseHeaders:',
|
|
' X-Robots-Tag: noindex',
|
|
'# An /auth/ page\'s address carries a code or a login hint: no Referer carries it on.',
|
|
`- name: ${NOREFERRER.name}`,
|
|
' spec:',
|
|
' headers:',
|
|
' customResponseHeaders:',
|
|
...Object.entries(NOREFERRER.headers).map(([k, v]) => ` ${k}: ${v}`),
|
|
'# A sign-in page is never framed, whatever the report-only policy says.',
|
|
`- name: ${FRAME.name}`,
|
|
' spec:',
|
|
' headers:',
|
|
' customResponseHeaders:',
|
|
...Object.entries(FRAME.headers).map(([k, v]) => ` ${k}: ${q(v)}`),
|
|
END,
|
|
]
|
|
|
|
const routes = [
|
|
NOTE,
|
|
'# `go get hanzo.ai/<module>` asks with ?go-get=1 and reads the page\'s tag, so it',
|
|
'# is served as asked, never redirected.',
|
|
...route('hanzo-ai-go', `${HOSTS} && Query(\`go-get\`, \`1\`)`, 170, ['hanzo-ai-csp', 'hanzo-ai-static']),
|
|
'# /chat and /dev answer every path under them with their page (dev-on-hanzo-ai at',
|
|
'# 300, chat-on-hanzo-ai at 200); their own data files and twins are served as',
|
|
'# they are, so this stands above both.',
|
|
...route(DATA.name, `${HOSTS} && PathRegexp(\`${DATA.regex}\`)`, DATA.priority, [WWW.name, 'hanzo-ai-noindex', 'hanzo-ai-csp', 'hanzo-ai-static']),
|
|
...route('hanzo-ai-twins', `${HOSTS} && PathRegexp(\`\\.(md|txt)$\`)`, 160, [WWW.name, 'hanzo-ai-noindex', 'hanzo-ai-csp', 'hanzo-ai-static']),
|
|
// Below the twins (160), so an /auth/ page's .txt keeps its noindex.
|
|
...route('hanzo-ai-auth', `${HOSTS} && PathPrefix(\`${NOREFERRER.prefix}\`)`, 155, [NOREFERRER.name, FRAME.name, 'hanzo-ai-edge']),
|
|
// The sign-in pages, each at its own address and every path under it.
|
|
...route('hanzo-ai-signin', `${HOSTS} && PathRegexp(\`^/(${FRAME.pages.map((p) => p.slice(1)).join('|')})(/|$)\`)`, 155, [FRAME.name, 'hanzo-ai-edge']),
|
|
...SHELVES.flatMap(({ from, keep }, i) =>
|
|
route(
|
|
shelved[i].name,
|
|
`${HOSTS} && PathPrefix(\`${from}\`)${keep.length ? ` && !PathRegexp(\`${kept(from, keep)}\`)` : ''}`,
|
|
150,
|
|
['hanzo-ai-moved', shelved[i].name],
|
|
),
|
|
),
|
|
END,
|
|
]
|
|
|
|
const text = readFileSync(file, 'utf8')
|
|
const blocks = text.split(BEGIN)
|
|
if (blocks.length !== 3) throw new Error(`${file} needs two ${BEGIN} markers, one in middlewares: and one in routes:`)
|
|
/** Replace the marked block at or after `from`; answer the text and where the block ends. */
|
|
const splice = (source, from, lines) => {
|
|
const at = source.indexOf(BEGIN, from)
|
|
const stop = source.indexOf(END, at) + END.length
|
|
const block = lines.join('\n')
|
|
return [source.slice(0, at) + block + source.slice(stop), at + block.length]
|
|
}
|
|
const [once, after] = splice(text, 0, middlewares)
|
|
const [next] = splice(once, after, routes)
|
|
|
|
const names = [
|
|
...middlewares.filter((l) => l.startsWith('- name: ')).map((l) => ['Middleware', l.slice(8)]),
|
|
...routes.filter((l) => l.startsWith('- name: ')).map((l) => ['IngressRoute', l.slice(8)]),
|
|
]
|
|
const dupes = names.map((k) => k.join('/')).filter((n, i, all) => all.indexOf(n) !== i)
|
|
if (dupes.length) throw new Error(`two rules share a name: ${dupes.join(', ')}`)
|
|
|
|
if (mode === '--check') {
|
|
if (next !== text) {
|
|
console.error(`${file} is not what lib/edge.ts renders: run node scripts/edge.mjs ${file}`)
|
|
process.exit(1)
|
|
}
|
|
console.log(`edge: ${file} is current`)
|
|
} else {
|
|
writeFileSync(file, next)
|
|
console.log(`edge: ${rules.length} redirect rules, ${shelved.length} shelves -> ${file}`)
|
|
}
|
|
console.log(JSON.stringify(names.map(([kind, name]) => ({ group: 'hanzo.ai', kind, name, namespace: 'hanzo' }))))
|