1import * as _ from 'lodash-es' 6// claim TXT records live at _claim*.<domain> with content "owner=<cluster>;hb=<ts>;ttl=<sec>" 7const claimPrefix = '_claim' 9export const wcClaimKey = (wcHostname: string): string => wcHostname.replace(/^\*\./, `${claimPrefix}.`) 10export const validatorClaimKey = (ethValidatorNum: number | string, domainName: string): string => 11 `${claimPrefix}-validator-${ethValidatorNum}.${domainName}` 13const claimTxt = (claim: Claim): string => `owner=${claim.owner};hb=${claim.hb};ttl=${claim.ttlSec}` 15const parseClaimTxt = (content: string): Claim | null => { 16 const parts = Object.fromEntries(content.replace(/"/g, '').split(';').map(p => p.split('='))) 17 const owner = parts.owner, hb = Number(parts.hb), ttlSec = Number(parts.ttl) 18 if (!owner || !hb || !ttlSec) return null 19 return {owner, hb, ttlSec} 22export const mkCfTxtClaimStore = ({headers, domainNames}: {headers: Record<string, string>, domainNames: string[]}): ClaimStore => { 24 const getZones = async () => zonesCache ??= await cfFetch('/zones', headers) 25 const findZone = async (claimKey: string) => { 26 const zoneName = _.maxBy(domainNames.filter(d => claimKey === d || claimKey.endsWith('.' + d)), d => d.length) 27 const zones = await getZones() 28 return zones.find((z: any) => z.name === zoneName) 31 readClaim: async (claimKey) => { 32 const zone = await findZone(claimKey) 33 if (!zone) return null 34 const rec = _.first(await cfFetch(`/zones/${zone.id}/dns_records?type=TXT&name=${encodeURIComponent(claimKey)}`, headers)) as {content: string} | undefined 35 return rec ? parseClaimTxt(rec.content) : null 37 putClaim: async (claimKey, claim) => { 38 const zone = await findZone(claimKey) 39 if (!zone) throPtErr('claimZoneNotFound', {claimKey, domainNames}) 40 const body = JSON.stringify({type: 'TXT', name: claimKey, content: claimTxt(claim), ttl: 1}) 41 const existing = _.first(await cfFetch(`/zones/${zone.id}/dns_records?type=TXT&name=${encodeURIComponent(claimKey)}`, headers)) as {id: string} | undefined 42 if (existing) await cfFetch(`/zones/${zone.id}/dns_records/${existing.id}`, headers, {method: 'PATCH', body}) 43 else await cfFetch(`/zones/${zone.id}/dns_records`, headers, {method: 'POST', body}) 45 listClaims: async () => { 46 const zones = await getZones() 47 const out: Array<{claimKey: string, claim: Claim}> = [] 48 for (const zone of zones.filter((z: any) => domainNames.includes(z.name))) { 49 const records = await cfFetch(`/zones/${zone.id}/dns_records?type=TXT&per_page=1000`, headers) 50 for (const r of records) { 51 if (!r.name.split('.')[0].startsWith(claimPrefix)) continue 52 const claim = parseClaimTxt(r.content) 53 if (claim) out.push({claimKey: r.name, claim})