🌳
pt0/deployF/claimLockF/cfTxtClaimStoreAI.mts
1import * as _ from 'lodash-es'
4import type { Claim, ClaimStore } from './claimLockAI.mts'
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 => {
23 let zonesCache: any[]
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)
29 }
30 return {
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
36 },
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})
44 },
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})
54 }
55 }
56 return out
57 },
58 }