code wiki / _hdl_build / nx_adnet_view.nx

nx_adnet_view.nx source

↩ module page · 85 lines · 4732 B

1// nx_adnet_view.nx -- LIB: MRC-compliant VIEWABLE-impression measurement for the sovereign ad slot. 2// THE DEFECT THIS CLOSES (debt 1785512185): the served-impression counter fires when the slot HTML is 3// INJECTED, before the browser has laid anything out. With a lazy-loaded creative that is frequently never 4// fetched at all, an advertiser is billed for pixels no human ever saw. The 2026 MRC display standard is 5// 50 percent of the creative's pixels in view for 1 CONTINUOUS second; a number weaker than that is not 6// sellable to a paying client, and billing on it is the credibility rock for the whole ad network. 7// 8// PRIVACY BY CONSTRUCTION, UNCHANGED: the beacon carries the AD id and NOTHING ELSE -- no cookie, no 9// visitor id, no dwell curve, no referrer, no timestamp from the client. The schema HAS NO VISITOR FIELD, 10// exactly like nx_adnet_selfserve. First-party only: same-origin sendBeacon and an inline script (the live 11// CSP is script-src 'self' 'unsafe-inline' and connect-src 'self'), zero third-party JS. 12// 13// FAIL-CLOSED: an id that is not a well-formed inventory id is DROPPED, never journalled -- a fabricated 14// or replayed id must never be able to manufacture a billable viewable impression. 15// license_tier: ORIGINAL 16import "nx_syscalls.nx" 17import "_hdl_build/nx_adnet_slot.nx" 18 19// MRC display thresholds -- the STANDARD, not taste. Weakening either makes the number unsellable, so 20// they are named constants the gate asserts against rather than digits buried in a script string. 21const AVIEW_RATIO_PCT: i64 = 50 22const AVIEW_DWELL_MS: i64 = 1000 23 24// decimal emit (no dynamic formatting in the emitted JS -- the thresholds come from the consts above, 25// so the script and the gate can never drift apart). 26func aview_putdec(out: *u8, off: i64, v: i64) -> i64 { 27 if v == 0 { out[off] = 48 as u8; return off + 1 } 28 let tmp: *u8 = sys_mmap(32) 29 var n: i64 = 0 30 var x: i64 = v 31 while x > 0 { tmp[n] = ((x - (x / 10) * 10) + 48) as u8; n = n + 1; x = x / 10 } 32 var o: i64 = off 33 var i: i64 = n - 1 34 while i >= 0 { out[o] = tmp[i]; o = o + 1; i = i - 1 } 35 return o 36} 37 38// Emit the first-party viewability observer for ONE ad id. Returns bytes written, 0 = refused. 39// The timer is CANCELLED when the creative leaves the threshold, which is what makes the second 40// CONTINUOUS rather than cumulative -- cumulative dwell is the usual way this measurement gets faked. 41// Fires at most once per page (o.disconnect + a sent latch), so a scroll-by cannot inflate the count. 42func aview_script(out: *u8, cap: i64, ad_id: *u8) -> i64 { 43 if cap < 1024 { return 0 } 44 if aslot_id_ok(ad_id) == 0 { return 0 } 45 var o: i64 = 0 46 o = ad_cat(out, o, "<script>(function(){var a=document.querySelector('.nx-ad-slot img');if(!a||!window.IntersectionObserver)return;var t=null,s=false;var o=new IntersectionObserver(function(en){var r=en[0];if(r.isIntersecting&&r.intersectionRatio*100>=" as *u8) 47 o = aview_putdec(out, o, AVIEW_RATIO_PCT) 48 o = ad_cat(out, o, "){if(t===null){t=setTimeout(function(){if(s)return;s=true;o.disconnect();try{navigator.sendBeacon('/ad/view/" as *u8) 49 o = ad_cat(out, o, ad_id) 50 o = ad_cat(out, o, "','')}catch(e){}}," as *u8) 51 o = aview_putdec(out, o, AVIEW_DWELL_MS) 52 o = ad_cat(out, o, ")}}else{if(t!==null){clearTimeout(t);t=null}}},{threshold:[0,0.5,1]});o.observe(a)})()</script>" as *u8) 53 out[o] = 0 as u8 54 return o 55} 56 57// Is this request path a viewable beacon? /ad/view/<id> 58func aview_is_path(path: *u8, pn: i64) -> i64 { 59 if pn < 10 { return 0 } 60 return aslot_starts(path, "/ad/view/" as *u8) 61} 62 63// Handle /ad/view/<id>: emit a 204 (no body -- a beacon has no reply to render) and set idout to the 64// VALIDATED id so the caller journals exactly one viewable impression. Returns response bytes, 0 = not 65// a beacon path. idout stays empty on a malformed id: the 204 is still emitted so a probe cannot use the 66// status code to enumerate which ids are live, but NOTHING is journalled. 67func aview_resp(path: *u8, pn: i64, out: *u8, cap: i64, idout: *u8) -> i64 { 68 idout[0] = 0 as u8 69 if aview_is_path(path, pn) == 0 { return 0 } 70 if cap < 256 { return 0 } 71 let id: *u8 = sys_mmap(128) 72 var i: i64 = 9 73 var j: i64 = 0 74 while i < pn { if j < 100 { id[j] = path[i]; j = j + 1 } i = i + 1 } 75 id[j] = 0 as u8 76 if aslot_id_ok(id) == 1 { 77 var k: i64 = 0 78 while id[k] != (0 as u8) { idout[k] = id[k]; k = k + 1 } 79 idout[k] = 0 as u8 80 } 81 var o: i64 = 0 82 o = ad_cat(out, o, "HTTP/1.1 204 No Content\r\nContent-Length: 0\r\nConnection: keep-alive\r\nKeep-Alive: timeout=65\r\nCache-Control: no-store\r\nX-Robots-Tag: noindex\r\n\r\n" as *u8) 83 out[o] = 0 as u8 84 return o 85}