nx_porkbun_domain.nx source
↩ module page · 320 lines · 16765 B
1// nx_porkbun_domain.nx -- Porkbun DOMAIN lifecycle client: pricing / availability / SSL-bundle
2// retrieve / REGISTER (purchase). Completes the existing Porkbun family (nx_acme_porkbun = DNS
3// records; nx_acme_issue = ACME cert issuance; this = the domain PURCHASE + golive side).
4// Composes nx_acme_http's nx_acme_req transport (TLS 1.3 with the proven 1.2 fallback that
5// api.porkbun.com needs) + nx_acme_porkbun's pk_* builders/checkers. Creds come from the sovereign
6// vault decrypt output (line1=apikey, line2=secretapikey; NEVER printed, NEVER hardcoded).
7//
8// SPEND SAFETY BY CONSTRUCTION (the register path):
9// R1 the endpoint must be VERIFIED against banked documentation bytes on disk
10// (knowledge/library/pb_api_docs.txt must contain the endpoint path) -- an unverified
11// spend endpoint REFUSES before any network I/O (assumption-free APIs, rule 4).
12// R2 creds file must load (both lines nonempty).
13// R3 the caller must pass the LITERAL token confirm-spend (no default-yes anywhere).
14// R4 a price quote (cents) and a spend CAP (cents) are required; quote>cap REFUSES.
15// R5 only after R1-R4 does the request go out; the raw response is printed for the operator.
16// CLI modes:
17// docs -- bank the API documentation page (GET over the 1.2-capable
18// transport) -> knowledge/library/pb_api_docs.txt
19// pricing -- keyless POST /pricing/get (public price list)
20// ping <credfile> -- authed POST /ping (read-only; prints yourIp body)
21// check <domain> <credfile> -- POST /domain/checkDomain/<domain> (availability+price)
22// ssl <domain> <credfile> <outfile> -- POST /ssl/retrieve/<domain>; bundle SAVED 0600, never printed
23// register <domain> <credfile> <price_cents> <cap_cents> confirm-spend
24// license_tier: ORIGINAL
25import "nx_acme_porkbun.nx"
26import "nx_itoa_lib.nx" // shared MSB-first emitter (zero-alloc)
27const PBD_MAGIC_4194304: i64 = 4194304
28const PBD_MAGIC_2048: i64 = 2048
29const PBD_MAGIC_1200: i64 = 1200
30const PBD_MAGIC_1048576: i64 = 1048576
31
32const PBD_RESP_CAP: i64 = 262144
33const PBD_DOCS_PATH: *u8 = "knowledge/library/pb_api_docs.txt\x00"
34const PBD_SPEC_PATH: *u8 = "knowledge/library/pb_api_spec.txt\x00"
35
36func pbd_w(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(1,s,n); return 0 }
37// MIGRATED to the shared emitter (debt 1785563586). The old body mmapped a scratch buffer
38// per call and never freed it. At PAGE granularity that is 4096B leaked PER CALL -- the
39// defect that took 28.5GB of a 36GB host in nx_ts_lumadiff (2MB input, ~3.66M calls).
40// nxi_* is MSB-first, allocates NOTHING, and emits identical bytes including the sign.
41func pbd_n(v: i64) -> i64 { nxi_out(v); return 0 }
42func pbd_slen(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} return n }
43func pbd_streq(a: *u8, b: *u8) -> i64 {
44 var i: i64=0
45 while a[i]!=(0 as u8) { if a[i]!=b[i] { return 0 } i=i+1 }
46 if b[i]!=(0 as u8) { return 0 }
47 return 1
48}
49// substring scan (bounded)
50func pbd_find(buf: *u8, n: i64, needle: *u8) -> i64 {
51 let m: i64 = pbd_slen(needle)
52 if m==0 { return 0 }
53 var i: i64=0
54 while i+m<=n {
55 var j: i64=0
56 var hit: i64=1
57 while j<m { if buf[i+j]!=needle[j] { hit=0; break } j=j+1 }
58 if hit==1 { return 1 }
59 i=i+1
60 }
61 return 0
62}
63// bounded whole-file read
64func pbd_read_file(path: *u8, buf: *u8, cap: i64) -> i64 {
65 let fd: i64 = sys_openat_rd(path)
66 if fd<0 { return 0-1 }
67 var total: i64=0
68 while total<cap {
69 let r: i64 = sys_read(fd, (buf as i64 + total) as *u8, cap-total)
70 if r<=0 { break }
71 total=total+r
72 }
73 sys_close(fd)
74 return total
75}
76// load the trust store from the nxc2-relative certdata (matches nx_porkbun_ping_probe)
77func pbd_store() -> *TrustStore {
78 let lr: i64 = nx_trust_store_load_from_certdata("data/mozilla_certdata.txt\x00" as *u8, 300, PBD_MAGIC_4194304)
79 if lr<=0 { return 0 as *TrustStore }
80 return lr as *TrustStore
81}
82// load creds: line1=apikey line2=secretapikey; strips CR. returns 0 ok / -1 fail. NEVER prints values.
83func pbd_creds(path: *u8, ak_off: *i64, ak_len: *i64, sk_off: *i64, sk_len: *i64, raw: *u8) -> i64 {
84 let rawn: i64 = pbd_read_file(path, raw, PBD_MAGIC_2048)
85 if rawn<=0 { return 0-1 }
86 var nl: i64=0
87 while nl<rawn { if raw[nl]==0x0a { break } nl=nl+1 }
88 var akn: i64=nl
89 if akn>0 { if raw[akn-1]==0x0d { akn=akn-1 } }
90 let s2: i64=nl+1
91 var nl2: i64=s2
92 while nl2<rawn { if raw[nl2]==0x0a { break } nl2=nl2+1 }
93 var skn: i64=nl2-s2
94 if skn>0 { if raw[s2+skn-1]==0x0d { skn=skn-1 } }
95 if akn<=0 { return 0-1 }
96 if skn<=0 { return 0-1 }
97 ak_off[0]=0
98 ak_len[0]=akn
99 sk_off[0]=s2
100 sk_len[0]=skn
101 return 0
102}
103// print HTTP status + body of a parsed response (public-data modes only)
104func pbd_show(resp: *u8, total: i64, maxbody: i64) -> i64 {
105 let r: *i64 = nx_http_resp_alloc()
106 if nx_http_response_parse(resp, total, r)!=0 {
107 pbd_w("(unparseable response; total=" as *u8); pbd_n(total)
108 pbd_w(" first-bytes:)\n" as *u8)
109 var dn: i64 = total
110 if dn>160 { dn=160 }
111 if dn>0 { sys_write(1, resp, dn) }
112 pbd_w("\n" as *u8)
113 return 0-1
114 }
115 pbd_w("HTTP " as *u8); pbd_n(r[1])
116 var bn: i64 = total - r[6]
117 pbd_w(" body " as *u8); pbd_n(bn); pbd_w(" bytes\n" as *u8)
118 if bn>maxbody { bn=maxbody }
119 sys_write(1, (resp as i64 + r[6]) as *u8, bn)
120 pbd_w("\n" as *u8)
121 return r[1]
122}
123
124// ---- endpoints (paths as data; VERIFIED against banked docs for the spend path) ----
125// POST /pricing/get -- keyless public price list
126func pbd_pricing(store: *TrustStore, now: i64, resp: *u8, rn: *i64) -> i64 {
127 let url: *u8 = "https://api.porkbun.com/api/json/v3/pricing/get\x00" as *u8
128 // the banked spec's own curl example posts an empty ARRAY body for this keyless endpoint
129 let body: *u8 = "[]" as *u8
130 return nx_acme_req(url, 47, 1, "application/json" as *u8, 16, body, 2, store, now, resp, PBD_RESP_CAP, rn)
131}
132// POST /ping -- authed read-only echo (proves creds; response = yourIp)
133func pbd_ping(raw: *u8, ao: i64, an: i64, so: i64, sn2: i64, store: *TrustStore, now: i64, resp: *u8, rn: *i64) -> i64 {
134 let url: *u8 = "https://api.porkbun.com/api/json/v3/ping\x00" as *u8
135 let body: *u8 = sys_mmap(PBD_MAGIC_2048)
136 var bo: i64 = pk_creds(body, 0, (raw as i64 + ao) as *u8, an, (raw as i64 + so) as *u8, sn2)
137 bo = pk_put(body, bo, "}" as *u8)
138 return nx_acme_req(url, 40, 1, "application/json" as *u8, 16, body, bo, store, now, resp, PBD_RESP_CAP, rn)
139}
140// POST /domain/checkDomain/<domain> -- availability + first-year pricing
141func pbd_check(raw: *u8, ao: i64, an: i64, so: i64, sn2: i64, dom: *u8, store: *TrustStore, now: i64, resp: *u8, rn: *i64) -> i64 {
142 let url: *u8 = sys_mmap(512)
143 var uo: i64 = pk_put(url, 0, "https://api.porkbun.com/api/json/v3/domain/checkDomain/" as *u8)
144 uo = pk_put(url, uo, dom)
145 url[uo]=0 as u8
146 let body: *u8 = sys_mmap(PBD_MAGIC_2048)
147 var bo: i64 = pk_creds(body, 0, (raw as i64 + ao) as *u8, an, (raw as i64 + so) as *u8, sn2)
148 bo = pk_put(body, bo, "}" as *u8)
149 return nx_acme_req(url, uo, 1, "application/json" as *u8, 16, body, bo, store, now, resp, PBD_RESP_CAP, rn)
150}
151// POST /ssl/retrieve/<domain> -- cert bundle (PRIVATE KEY inside: saved 0600, never printed)
152func pbd_ssl(raw: *u8, ao: i64, an: i64, so: i64, sn2: i64, dom: *u8, store: *TrustStore, now: i64, resp: *u8, rn: *i64) -> i64 {
153 let url: *u8 = sys_mmap(512)
154 var uo: i64 = pk_put(url, 0, "https://api.porkbun.com/api/json/v3/ssl/retrieve/" as *u8)
155 uo = pk_put(url, uo, dom)
156 url[uo]=0 as u8
157 let body: *u8 = sys_mmap(PBD_MAGIC_2048)
158 var bo: i64 = pk_creds(body, 0, (raw as i64 + ao) as *u8, an, (raw as i64 + so) as *u8, sn2)
159 bo = pk_put(body, bo, "}" as *u8)
160 return nx_acme_req(url, uo, 1, "application/json" as *u8, 16, body, bo, store, now, resp, PBD_RESP_CAP, rn)
161}
162// ---- the SPEND WALL: every check must pass before any network I/O. codes:
163// -10 endpoint unverified in banked docs; -11 creds missing; -12 confirm token absent;
164// -13 bad quote/cap; -14 quote exceeds cap; 0 = wall passed.
165func pbd_register_wall(docs_ok: i64, creds_ok: i64, confirm_ok: i64, quote_cents: i64, cap_cents: i64) -> i64 {
166 if docs_ok!=1 { return 0-10 }
167 if creds_ok!=1 { return 0-11 }
168 if confirm_ok!=1 { return 0-12 }
169 if quote_cents<=0 { return 0-13 }
170 if cap_cents<=0 { return 0-13 }
171 if quote_cents>cap_cents { return 0-14 }
172 return 0
173}
174// docs verification: the banked MACHINE-READABLE SPEC (porkbun.com/api/json/v3/spec, fetched live
175// over the sovereign transport) must name the create endpoint. Raw spec bytes JSON-escape slashes,
176// so the needle matches the escaped form domain\/create\/{domain}.
177func pbd_docs_has_create(buf: *u8, cap: i64) -> i64 {
178 let n: i64 = pbd_read_file(PBD_SPEC_PATH, buf, cap)
179 if n<=0 { return 0 }
180 return pbd_find(buf, n, "domain\\/create\\/{domain}" as *u8)
181}
182// append integer decimal digits into a body buffer
183func pbd_putn(dst: *u8, off: i64, v: i64) -> i64 {
184 let t: *u8=sys_mmap(28)
185 var m: i64=v
186 var k: i64=0
187 if m<=0 { dst[off]=48 as u8; return off+1 }
188 while m>0 { t[k]=(48+(m%10)) as u8; m=m/10; k=k+1 }
189 var o: i64=off
190 var i: i64=0
191 while i<k { dst[o]=t[k-1-i]; o=o+1; i=i+1 }
192 return o
193}
194
195func main(argc: i64, argv: *i64) -> i64 {
196 if argc<2 {
197 pbd_w("usage: nx_porkbun_domain docs|pricing|ping <credfile>|check <domain> <credfile>|ssl <domain> <credfile> <outfile>|register <domain> <credfile> <price_cents> <cap_cents> confirm-spend\n" as *u8)
198 return 1
199 }
200 let mode: *u8 = argv[1] as *u8
201 let store: *TrustStore = pbd_store()
202 if (store as i64)==0 { pbd_w("trust store load FAIL (run from nxc2; needs data/mozilla_certdata.txt)\n" as *u8); return 1 }
203 let now: i64 = sys_now_realtime_sec()
204 let resp: *u8 = sys_mmap(PBD_RESP_CAP)
205 let rn: *i64 = sys_mmap(16) as *i64
206
207 if pbd_streq(mode, "docs" as *u8)==1 {
208 var url: *u8 = "https://porkbun.com/api/json/v3/documentation\x00" as *u8
209 var outp: *u8 = PBD_DOCS_PATH
210 if argc>=3 { url = argv[2] as *u8 }
211 if argc>=4 { outp = argv[3] as *u8 }
212 let rc: i64 = nx_acme_req(url, pbd_slen(url), 0, 0 as *u8, 0, 0 as *u8, 0, store, now, resp, PBD_RESP_CAP, rn)
213 if rc!=NX_ACME_HTTP_OK { pbd_w("docs fetch FAIL rc=" as *u8); pbd_n(rc); pbd_w("\n" as *u8); return 2 }
214 let fd: i64 = sys_openat_wr(outp, 0x1a4)
215 if fd<0 { pbd_w("docs save FAIL\n" as *u8); return 2 }
216 sys_write(fd, resp, rn[0])
217 sys_close(fd)
218 pbd_w("docs banked: " as *u8); pbd_n(rn[0]); pbd_w(" bytes -> " as *u8); pbd_w(outp); pbd_w("\n" as *u8)
219 return 0
220 }
221 if pbd_streq(mode, "pricing" as *u8)==1 {
222 let rc: i64 = pbd_pricing(store, now, resp, rn)
223 if rc!=NX_ACME_HTTP_OK { pbd_w("pricing FAIL rc=" as *u8); pbd_n(rc); pbd_w("\n" as *u8); return 2 }
224 pbd_show(resp, rn[0], 400)
225 return 0
226 }
227 let raw: *u8 = sys_mmap(PBD_MAGIC_2048)
228 let ao: *i64 = sys_mmap(8) as *i64
229 let an: *i64 = sys_mmap(8) as *i64
230 let so: *i64 = sys_mmap(8) as *i64
231 let sn2: *i64 = sys_mmap(8) as *i64
232 if pbd_streq(mode, "ping" as *u8)==1 {
233 if argc<3 { pbd_w("ping needs <credfile>\n" as *u8); return 1 }
234 if pbd_creds(argv[2] as *u8, ao, an, so, sn2, raw)!=0 { pbd_w("creds load FAIL\n" as *u8); return 2 }
235 pbd_w("creds loaded (lens " as *u8); pbd_n(an[0]); pbd_w("/" as *u8); pbd_n(sn2[0]); pbd_w("); pinging...\n" as *u8)
236 let rc: i64 = pbd_ping(raw, ao[0], an[0], so[0], sn2[0], store, now, resp, rn)
237 if rc!=NX_ACME_HTTP_OK { pbd_w("ping FAIL rc=" as *u8); pbd_n(rc); pbd_w("\n" as *u8); return 2 }
238 pbd_show(resp, rn[0], 300)
239 return 0
240 }
241 if pbd_streq(mode, "check" as *u8)==1 {
242 if argc<4 { pbd_w("check needs <domain> <credfile>\n" as *u8); return 1 }
243 if pbd_creds(argv[3] as *u8, ao, an, so, sn2, raw)!=0 { pbd_w("creds load FAIL\n" as *u8); return 2 }
244 let rc: i64 = pbd_check(raw, ao[0], an[0], so[0], sn2[0], argv[2] as *u8, store, now, resp, rn)
245 if rc!=NX_ACME_HTTP_OK { pbd_w("check FAIL rc=" as *u8); pbd_n(rc); pbd_w("\n" as *u8); return 2 }
246 pbd_show(resp, rn[0], PBD_MAGIC_1200)
247 return 0
248 }
249 if pbd_streq(mode, "ssl" as *u8)==1 {
250 if argc<5 { pbd_w("ssl needs <domain> <credfile> <outfile>\n" as *u8); return 1 }
251 if pbd_creds(argv[3] as *u8, ao, an, so, sn2, raw)!=0 { pbd_w("creds load FAIL\n" as *u8); return 2 }
252 let rc: i64 = pbd_ssl(raw, ao[0], an[0], so[0], sn2[0], argv[2] as *u8, store, now, resp, rn)
253 if rc!=NX_ACME_HTTP_OK { pbd_w("ssl FAIL rc=" as *u8); pbd_n(rc); pbd_w("\n" as *u8); return 2 }
254 // bundle contains the PRIVATE KEY -> save 0600, print ONLY status + byte count
255 let r: *i64 = nx_http_resp_alloc()
256 if nx_http_response_parse(resp, rn[0], r)!=0 { pbd_w("ssl parse FAIL\n" as *u8); return 2 }
257 let fd: i64 = sys_openat_wr(argv[4] as *u8, 0x180)
258 if fd<0 { pbd_w("ssl save FAIL\n" as *u8); return 2 }
259 sys_write(fd, (resp as i64 + r[6]) as *u8, rn[0]-r[6])
260 sys_close(fd)
261 pbd_w("HTTP " as *u8); pbd_n(r[1])
262 pbd_w(" bundle saved (0600): " as *u8); pbd_n(rn[0]-r[6]); pbd_w(" bytes (private key inside; NEVER printed)\n" as *u8)
263 return 0
264 }
265 var is_reg: i64 = 0
266 var is_dry: i64 = 0
267 if pbd_streq(mode, "register" as *u8)==1 { is_reg=1 }
268 if pbd_streq(mode, "dryrun" as *u8)==1 { is_dry=1 }
269 if is_reg+is_dry==1 {
270 if argc<7 { pbd_w("register|dryrun needs <domain> <credfile> <price_cents> <cap_cents> confirm-spend\n" as *u8); return 1 }
271 // R1 endpoint verified against banked docs bytes
272 let docsbuf: *u8 = sys_mmap(PBD_MAGIC_1048576)
273 let docs_ok: i64 = pbd_docs_has_create(docsbuf, PBD_MAGIC_1048576)
274 // R2 creds
275 var creds_ok: i64 = 0
276 if pbd_creds(argv[3] as *u8, ao, an, so, sn2, raw)==0 { creds_ok=1 }
277 // R3 literal confirm token
278 var confirm_ok: i64 = 0
279 if pbd_streq(argv[6] as *u8, "confirm-spend" as *u8)==1 { confirm_ok=1 }
280 // R4 quote + cap (integer cents)
281 var quote: i64 = 0
282 var cap: i64 = 0
283 let qs: *u8 = argv[4] as *u8
284 var qi: i64=0
285 while qs[qi]!=(0 as u8) { if qs[qi]>=(48 as u8) { if qs[qi]<=(57 as u8) { quote=quote*10+((qs[qi] as i64)-48) } } qi=qi+1 }
286 let cs: *u8 = argv[5] as *u8
287 var ci: i64=0
288 while cs[ci]!=(0 as u8) { if cs[ci]>=(48 as u8) { if cs[ci]<=(57 as u8) { cap=cap*10+((cs[ci] as i64)-48) } } ci=ci+1 }
289 let wall: i64 = pbd_register_wall(docs_ok, creds_ok, confirm_ok, quote, cap)
290 if wall!=0 {
291 pbd_w("REGISTER REFUSED (wall code " as *u8); pbd_n(wall)
292 pbd_w("): -10=endpoint-unverified-in-banked-docs -11=creds -12=confirm -13=quote/cap -14=over-cap\n" as *u8)
293 return 3
294 }
295 // R5 the request, on the SPEC-VERIFIED contract: POST /domain/create/{domain}
296 // body = creds + cost(pennies, must equal the checkDomain quote) + agreeToTerms.
297 // dryrun mode adds dryRun:true = full server-side pre-flight, NO order, NO charge.
298 let url: *u8 = sys_mmap(512)
299 var uo: i64 = pk_put(url, 0, "https://api.porkbun.com/api/json/v3/domain/create/" as *u8)
300 uo = pk_put(url, uo, argv[2] as *u8)
301 url[uo]=0 as u8
302 let body: *u8 = sys_mmap(PBD_MAGIC_2048)
303 var bo: i64 = pk_creds(body, 0, (raw as i64 + ao[0]) as *u8, an[0], (raw as i64 + so[0]) as *u8, sn2[0])
304 bo = pk_put(body, bo, ",\"cost\":" as *u8)
305 bo = pbd_putn(body, bo, quote)
306 bo = pk_put(body, bo, ",\"agreeToTerms\":\"yes\"" as *u8)
307 if is_dry==1 { bo = pk_put(body, bo, ",\"dryRun\":true" as *u8) }
308 bo = pk_put(body, bo, "}" as *u8)
309 pbd_w("SPEND WALL PASSED (quote " as *u8); pbd_n(quote)
310 pbd_w(" <= cap " as *u8); pbd_n(cap)
311 if is_dry==1 { pbd_w(" cents; DRY RUN -- server validates, no charge). Sending...\n" as *u8) }
312 else { pbd_w(" cents; endpoint spec-verified; confirm-spend given; REAL REGISTRATION). Sending...\n" as *u8) }
313 let rc: i64 = nx_acme_req(url, uo, 1, "application/json" as *u8, 16, body, bo, store, now, resp, PBD_RESP_CAP, rn)
314 if rc!=NX_ACME_HTTP_OK { pbd_w("register transport FAIL rc=" as *u8); pbd_n(rc); pbd_w("\n" as *u8); return 2 }
315 pbd_show(resp, rn[0], PBD_MAGIC_1200)
316 return 0
317 }
318 pbd_w("unknown mode\n" as *u8)
319 return 1
320}