code wiki / (root) / nx_grab_ext_emit.nx

nx_grab_ext_emit.nx source

↩ module page · 514 lines · 26600 B

1// nx_grab_ext_emit.nx -- /compare/mediaingest R10 (contract symbol ge_emit): EMIT the browser extension 2// from data, for BOTH engine families, so Nishi OS and the Nishi browser and every third-party browser get 3// the same capture surface without a native companion. 4// 5// OPERATOR STANDING ORDER 2026-08-30: "i dont want js powering any of this -- emit from the first byte up via 6// nishi lang." A WebExtension is JavaScript by the browser's own definition and no organ can change that. What 7// this organ changes is WHO WROTE IT: every byte the browser runs is emitted here from knowledge/grab_ext.conf, 8// so adding a media type or moving the edge host is a DATA edit that reproduces both twins, not a hand-edit of 9// two hand-written trees that then drift. That is the same reason nx_gen_ui_emit exists for the /gen page. 10// 11// WHY BOTH TWINS COME FROM ONE FILE. MV2 (Firefox/Waterfox) and MV3 (Chrome/Edge) differ in STRUCTURE: 12// background.scripts vs background.service_worker 13// browser_action vs action 14// permissions carries <all_urls> vs host_permissions carries it 15// They must NOT differ in POLICY. Emitting both from the same rows makes divergence impossible by construction 16// instead of by review discipline -- the estate's standing rule when two artifacts must agree. 17// 18// WHY NO NATIVE COMPANION (this is R11 ge_edge_push, and it is a DELIBERATE non-dependency). The incumbent 19// (Video DownloadHelper) shipped a native CoApp for years and its own README now says "VDH CoApp is not being 20// developed anymore -- VDH v10 doesn't require a companion." The field moved off native messaging; we do not 21// re-adopt it. The extension holds no binary and does no muxing: it SNIFFS in the tab and POSTs one URL to the 22// estate over the authenticated edge, and the NAS does the capture. That is why it works on Windows with no 23// WSL and no installed binary. 24// 25// ⛔ DECLARED IMPRECISION, NAMED BECAUSE AN UNDECLARED ONE IS A SILENT DEFECT: the emitted background body 26// holds its per-tab media list in a plain in-memory object. On MV2 that is correct -- the background page is 27// PERSISTENT. On MV3 the service worker is EPHEMERAL and is torn down when idle, so the list and the badge 28// count are lost on wake, and a user who leaves a tab open then clicks the toolbar button can see an empty 29// list on Chrome/Edge where Firefox/Waterfox shows the real one. The capture PUSH is unaffected (it carries 30// the url in the message), so this degrades discovery, never correctness of what is captured. 31// THE FIX IS chrome.storage.session MIRRORED ON EVERY ADD PLUS REHYDRATION ON WAKE, and it is deliberately 32// NOT done in this increment: it is a behaviour change to the shared body that both twins run, so it wants 33// its own teeth (wake-with-empty-memory rehydrates; MV2 unaffected) rather than being smuggled in beside the 34// structural split this increment exists to prove. Tracked as the next increment of R10. 35// 36// ⚠ nx_cc LEXER: a string literal may contain neither '#' nor '!'. The emitted JavaScript is therefore written 37// WITHOUT the '!' operator anywhere -- "=== false" and explicit comparisons instead of "!x" and "!==". This is 38// a real constraint on the emitted text, not a style choice, and a future edit that reaches for '!' will fail 39// to compile rather than fail silently. 40// 41// module: nishi-core.media.grab_ext_emit 42// contract: /compare/mediaingest R10 ge_emit (R11 ge_edge_push rides the same artifact) 43// license_tier: ORIGINAL 44 45import "nx_syscalls.nx" 46 47const GX_CONF_PATH: *u8 = "knowledge/grab_ext.conf" 48const GX_CONF_CAP: i64 = 65536 49const GX_OUT_CAP: i64 = 262144 50const GX_VAL_CAP: i64 = 4096 51const GX_PATH_CAP: i64 = 1024 52const GX_DIR_MODE: i64 = 0x1FD // 0o775 53const GX_FILE_MODE: i64 = 0x1A4 // 0o644 54 55const GX_CH_NL: i64 = 10 56const GX_CH_CR: i64 = 13 57const GX_CH_PIPE: i64 = 124 58const GX_CH_HASH: i64 = 35 59const GX_CH_SP: i64 = 32 60const GX_CH_D0: i64 = 48 61 62const GX_EXIT_OK: i64 = 0 63const GX_EXIT_NOCONF: i64 = 2 64const GX_EXIT_MISSKEY: i64 = 3 65const GX_EXIT_BADEDGE: i64 = 4 66const GX_EXIT_WRITE: i64 = 5 67const GX_EXIT_OVERFLOW: i64 = 6 68 69func gx_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } 70func gx_w(s: *u8) -> i64 { sys_write(1, s, gx_slen(s)); return 0 } 71func gx_wb(b: *u8, n: i64) -> i64 { sys_write(1, b, n); return 0 } 72func gx_wn(v: i64) -> i64 { 73 let t: *u8 = sys_mmap(32) 74 var m: i64 = v 75 var k: i64 = 0 76 if m < 0 { sys_write(1, "-" as *u8, 1); m = 0 - m } 77 if m == 0 { t[0] = GX_CH_D0 as u8; k = 1 } 78 while m > 0 { t[k] = (GX_CH_D0 + (m % 10)) as u8; m = m / 10; k = k + 1 } 79 var i: i64 = k - 1 80 while i >= 0 { sys_write(1, ((t as i64) + i) as *u8, 1); i = i - 1 } 81 return 0 82} 83 84func gx_app(dst: *u8, k0: i64, src: *u8) -> i64 { 85 var k: i64 = k0 86 var i: i64 = 0 87 let n: i64 = gx_slen(src) 88 while i < n { dst[k] = src[i]; k = k + 1; i = i + 1 } 89 return k 90} 91 92func gx_appn(dst: *u8, k0: i64, src: *u8, n: i64) -> i64 { 93 var k: i64 = k0 94 var i: i64 = 0 95 while i < n { dst[k] = src[i]; k = k + 1; i = i + 1 } 96 return k 97} 98 99// ---- conf reader ------------------------------------------------------------------------------- 100// rows are key|value, one per line; '#' starts a comment line; a key may repeat (media_ext, perm). 101// COMPOSES sys_read_file, which sizes its buffer from the file via lseek END and therefore CANNOT 102// short-read. A hand-rolled capped reader here would be the estate's measured cap defect: a conf that 103// grows past the cap is silently truncated mid-row, and the emitter would then drop media types or 104// permissions with no error at all -- the same shape as nx_cron_reconcile reading 16,383 of 24,356 105// bytes and reporting the truncated count as the population. 106 107// Return the extent of the idx-th value for `key` (0-based). len, with off_out[0] = offset. -1 if absent. 108func gx_nth(buf: *u8, n: i64, key: *u8, idx: i64, off_out: *i64) -> i64 { 109 off_out[0] = 0 - 1 110 let kl: i64 = gx_slen(key) 111 var i: i64 = 0 112 var hits: i64 = 0 113 var line_start: i64 = 0 114 while i <= n { 115 var eol: i64 = 0 116 if i == n { eol = 1 } else { if buf[i] == (GX_CH_NL as u8) { eol = 1 } } 117 if eol == 1 { 118 var ls: i64 = line_start 119 var le: i64 = i 120 if le > ls { if buf[le - 1] == (GX_CH_CR as u8) { le = le - 1 } } 121 var comment: i64 = 0 122 if le > ls { if buf[ls] == (GX_CH_HASH as u8) { comment = 1 } } 123 if comment == 0 { 124 if le - ls > kl { 125 var same: i64 = 1 126 var j: i64 = 0 127 while j < kl { if buf[ls + j] != key[j] { same = 0 } j = j + 1 } 128 if same == 1 { 129 if buf[ls + kl] == (GX_CH_PIPE as u8) { 130 if hits == idx { 131 off_out[0] = ls + kl + 1 132 return le - (ls + kl + 1) 133 } 134 hits = hits + 1 135 } 136 } 137 } 138 } 139 line_start = i + 1 140 } 141 i = i + 1 142 } 143 return 0 - 1 144} 145 146func gx_count(buf: *u8, n: i64, key: *u8) -> i64 { 147 let off: *i64 = sys_mmap(16) as *i64 148 var c: i64 = 0 149 var scanning: i64 = 1 150 while scanning == 1 { 151 if gx_nth(buf, n, key, c, off) < 0 { scanning = 0 } else { c = c + 1 } 152 } 153 return c 154} 155 156// REFUSE BY NAME. A silently-defaulted edge host would ship an extension that posts the operator's captures 157// to the wrong estate, so a key this organ needs and cannot find stops the emit and says which key it was. 158func gx_need(buf: *u8, n: i64, key: *u8, off_out: *i64) -> i64 { 159 let l: i64 = gx_nth(buf, n, key, 0, off_out) 160 if l < 0 { 161 gx_w("EMIT REFUSED missing-conf-key key=" as *u8); gx_w(key) 162 gx_w(" file=" as *u8); gx_w(GX_CONF_PATH) 163 gx_w(" -- not defaulted on purpose: a guessed edge host or endpoint would publish captures to the wrong estate\n" as *u8) 164 sys_exit(GX_EXIT_MISSKEY) 165 } 166 return l 167} 168 169func gx_starts(buf: *u8, off: i64, len: i64, lit: *u8) -> i64 { 170 let ll: i64 = gx_slen(lit) 171 if len < ll { return 0 } 172 var i: i64 = 0 173 var same: i64 = 1 174 while i < ll { if buf[off + i] != lit[i] { same = 0 } i = i + 1 } 175 return same 176} 177 178// ---- the media pattern, built ONCE and shared by every emitted artifact ------------------------ 179// One list in the conf becomes one regex alternation, used by the sniffer AND by the badge counter, so the 180// thing that decides "is this grabbable" cannot disagree with the thing that counts grabbables. 181func gx_media_alt(conf: *u8, cn: i64, out: *u8, cap: i64) -> i64 { 182 let off: *i64 = sys_mmap(16) as *i64 183 let total: i64 = gx_count(conf, cn, "media_ext" as *u8) 184 if total <= 0 { return 0 - 1 } 185 var k: i64 = 0 186 var i: i64 = 0 187 while i < total { 188 let l: i64 = gx_nth(conf, cn, "media_ext" as *u8, i, off) 189 if l > 0 { 190 if i > 0 { out[k] = GX_CH_PIPE as u8; k = k + 1 } 191 k = gx_appn(out, k, ((conf as i64) + off[0]) as *u8, l) 192 } 193 i = i + 1 194 } 195 if k >= cap { return 0 - 1 } 196 out[k] = 0 as u8 197 return k 198} 199 200// ---- manifests --------------------------------------------------------------------------------- 201func gx_manifest_common_tail(out: *u8, k0: i64) -> i64 { 202 var k: i64 = k0 203 k = gx_app(out, k, " \"content_scripts\": [\n" as *u8) 204 k = gx_app(out, k, " {\n" as *u8) 205 k = gx_app(out, k, " \"matches\": [\"<all_urls>\"],\n" as *u8) 206 k = gx_app(out, k, " \"js\": [\"content.js\"],\n" as *u8) 207 k = gx_app(out, k, " \"run_at\": \"document_idle\",\n" as *u8) 208 k = gx_app(out, k, " \"all_frames\": false\n" as *u8) 209 k = gx_app(out, k, " }\n" as *u8) 210 k = gx_app(out, k, " ]" as *u8) 211 return k 212} 213 214func gx_emit_mv2(conf: *u8, cn: i64, out: *u8, cap: i64) -> i64 { 215 let off: *i64 = sys_mmap(16) as *i64 216 var k: i64 = 0 217 k = gx_app(out, k, "{\n \"manifest_version\": 2,\n \"name\": \"" as *u8) 218 let nl: i64 = gx_need(conf, cn, "name" as *u8, off) 219 k = gx_appn(out, k, ((conf as i64) + off[0]) as *u8, nl) 220 k = gx_app(out, k, "\",\n \"version\": \"" as *u8) 221 let vl: i64 = gx_need(conf, cn, "version" as *u8, off) 222 k = gx_appn(out, k, ((conf as i64) + off[0]) as *u8, vl) 223 k = gx_app(out, k, "\",\n \"description\": \"" as *u8) 224 let dl: i64 = gx_need(conf, cn, "description" as *u8, off) 225 k = gx_appn(out, k, ((conf as i64) + off[0]) as *u8, dl) 226 k = gx_app(out, k, "\",\n \"permissions\": [" as *u8) 227 // MV2 keeps host permissions INSIDE permissions. The separator is driven by a WROTE COUNTER, not by 228 // the loop index: two consecutive loops feed one JSON array, so keying the comma off `i > 0` emits a 229 // LEADING comma the moment the first loop contributes nothing (a conf with host_perm and no perm), 230 // and that is invalid JSON the browser rejects at load time with no hint of which row caused it. 231 var wrote: i64 = 0 232 let pc: i64 = gx_count(conf, cn, "perm" as *u8) 233 var i: i64 = 0 234 while i < pc { 235 let l: i64 = gx_nth(conf, cn, "perm" as *u8, i, off) 236 if wrote > 0 { k = gx_app(out, k, ", " as *u8) } 237 k = gx_app(out, k, "\"" as *u8) 238 k = gx_appn(out, k, ((conf as i64) + off[0]) as *u8, l) 239 k = gx_app(out, k, "\"" as *u8) 240 wrote = wrote + 1 241 i = i + 1 242 } 243 let hc: i64 = gx_count(conf, cn, "host_perm" as *u8) 244 i = 0 245 while i < hc { 246 let l: i64 = gx_nth(conf, cn, "host_perm" as *u8, i, off) 247 if wrote > 0 { k = gx_app(out, k, ", " as *u8) } 248 k = gx_app(out, k, "\"" as *u8) 249 k = gx_appn(out, k, ((conf as i64) + off[0]) as *u8, l) 250 k = gx_app(out, k, "\"" as *u8) 251 wrote = wrote + 1 252 i = i + 1 253 } 254 k = gx_app(out, k, "],\n \"browser_action\": { \"default_title\": \"" as *u8) 255 let n2: i64 = gx_need(conf, cn, "name" as *u8, off) 256 k = gx_appn(out, k, ((conf as i64) + off[0]) as *u8, n2) 257 k = gx_app(out, k, "\" },\n \"background\": { \"scripts\": [\"background.js\"] },\n" as *u8) 258 k = gx_manifest_common_tail(out, k) 259 k = gx_app(out, k, ",\n \"browser_specific_settings\": { \"gecko\": { \"id\": \"" as *u8) 260 let gl: i64 = gx_need(conf, cn, "gecko_id" as *u8, off) 261 k = gx_appn(out, k, ((conf as i64) + off[0]) as *u8, gl) 262 k = gx_app(out, k, "\" } }\n}\n" as *u8) 263 if k >= cap { return 0 - 1 } 264 return k 265} 266 267func gx_emit_mv3(conf: *u8, cn: i64, out: *u8, cap: i64) -> i64 { 268 let off: *i64 = sys_mmap(16) as *i64 269 var k: i64 = 0 270 k = gx_app(out, k, "{\n \"manifest_version\": 3,\n \"name\": \"" as *u8) 271 let nl: i64 = gx_need(conf, cn, "name" as *u8, off) 272 k = gx_appn(out, k, ((conf as i64) + off[0]) as *u8, nl) 273 k = gx_app(out, k, "\",\n \"version\": \"" as *u8) 274 let vl: i64 = gx_need(conf, cn, "version" as *u8, off) 275 k = gx_appn(out, k, ((conf as i64) + off[0]) as *u8, vl) 276 k = gx_app(out, k, "\",\n \"description\": \"" as *u8) 277 let dl: i64 = gx_need(conf, cn, "description" as *u8, off) 278 k = gx_appn(out, k, ((conf as i64) + off[0]) as *u8, dl) 279 k = gx_app(out, k, "\",\n \"permissions\": [" as *u8) 280 // MV3 SPLIT: host permissions move out of permissions into host_permissions. The emitter does this, 281 // not the author -- that is the whole point of one conf feeding two twins. 282 let pc: i64 = gx_count(conf, cn, "perm" as *u8) 283 var i: i64 = 0 284 while i < pc { 285 let l: i64 = gx_nth(conf, cn, "perm" as *u8, i, off) 286 if i > 0 { k = gx_app(out, k, ", " as *u8) } 287 k = gx_app(out, k, "\"" as *u8) 288 k = gx_appn(out, k, ((conf as i64) + off[0]) as *u8, l) 289 k = gx_app(out, k, "\"" as *u8) 290 i = i + 1 291 } 292 k = gx_app(out, k, "],\n \"host_permissions\": [" as *u8) 293 let hc: i64 = gx_count(conf, cn, "host_perm" as *u8) 294 i = 0 295 while i < hc { 296 let l: i64 = gx_nth(conf, cn, "host_perm" as *u8, i, off) 297 if i > 0 { k = gx_app(out, k, ", " as *u8) } 298 k = gx_app(out, k, "\"" as *u8) 299 k = gx_appn(out, k, ((conf as i64) + off[0]) as *u8, l) 300 k = gx_app(out, k, "\"" as *u8) 301 i = i + 1 302 } 303 k = gx_app(out, k, "],\n \"action\": { \"default_title\": \"" as *u8) 304 let n2: i64 = gx_need(conf, cn, "name" as *u8, off) 305 k = gx_appn(out, k, ((conf as i64) + off[0]) as *u8, n2) 306 k = gx_app(out, k, "\" },\n \"background\": { \"service_worker\": \"background.js\" },\n" as *u8) 307 k = gx_manifest_common_tail(out, k) 308 k = gx_app(out, k, "\n}\n" as *u8) 309 if k >= cap { return 0 - 1 } 310 return k 311} 312 313// ---- background script ------------------------------------------------------------------------- 314// The sniffer + the push. No '!' anywhere (nx_cc lexer). Uses the MV2/MV3-portable `api` shim so ONE 315// emitted body serves both twins -- chrome.* and browser.* differ in name, not in the calls we make. 316func gx_emit_bg(conf: *u8, cn: i64, out: *u8, cap: i64) -> i64 { 317 let off: *i64 = sys_mmap(16) as *i64 318 let alt: *u8 = sys_mmap(GX_VAL_CAP) 319 if gx_media_alt(conf, cn, alt, GX_VAL_CAP) < 0 { return 0 - 1 } 320 var k: i64 = 0 321 k = gx_app(out, k, "// GENERATED by nx_grab_ext_emit from knowledge/grab_ext.conf -- do not hand-edit.\n" as *u8) 322 k = gx_app(out, k, "// Edit the conf and re-emit; both browser twins regenerate from the same rows.\n" as *u8) 323 k = gx_app(out, k, "const api = (typeof browser === 'object') ? browser : chrome;\n" as *u8) 324 k = gx_app(out, k, "const EDGE = '" as *u8) 325 let el: i64 = gx_need(conf, cn, "edge" as *u8, off) 326 k = gx_appn(out, k, ((conf as i64) + off[0]) as *u8, el) 327 k = gx_app(out, k, "';\n" as *u8) 328 k = gx_app(out, k, "const CAPTURE_PATH = '" as *u8) 329 let cl: i64 = gx_need(conf, cn, "capture_path" as *u8, off) 330 k = gx_appn(out, k, ((conf as i64) + off[0]) as *u8, cl) 331 k = gx_app(out, k, "';\n" as *u8) 332 k = gx_app(out, k, "const SESSION_HEADER = '" as *u8) 333 let sh: i64 = gx_need(conf, cn, "session_header" as *u8, off) 334 k = gx_appn(out, k, ((conf as i64) + off[0]) as *u8, sh) 335 k = gx_app(out, k, "';\n" as *u8) 336 k = gx_app(out, k, "const SESSION_KEY = '" as *u8) 337 let sk: i64 = gx_need(conf, cn, "session_key" as *u8, off) 338 k = gx_appn(out, k, ((conf as i64) + off[0]) as *u8, sk) 339 k = gx_app(out, k, "';\n" as *u8) 340 k = gx_app(out, k, "const MEDIA_RE = /[.](" as *u8) 341 k = gx_app(out, k, alt) 342 k = gx_app(out, k, ")([?]|$)/i;\n" as *u8) 343 k = gx_app(out, k, "const media = {};\n" as *u8) 344 k = gx_app(out, k, "function isMedia(u) { return MEDIA_RE.test(u) === true; }\n" as *u8) 345 k = gx_app(out, k, "api.webRequest.onBeforeRequest.addListener(function (d) {\n" as *u8) 346 k = gx_app(out, k, " if (isMedia(d.url) === true) {\n" as *u8) 347 k = gx_app(out, k, " if (media[d.tabId] === undefined) { media[d.tabId] = []; }\n" as *u8) 348 k = gx_app(out, k, " if (media[d.tabId].indexOf(d.url) < 0) { media[d.tabId].push(d.url); }\n" as *u8) 349 k = gx_app(out, k, " api.browserAction === undefined\n" as *u8) 350 k = gx_app(out, k, " ? api.action.setBadgeText({ tabId: d.tabId, text: String(media[d.tabId].length) })\n" as *u8) 351 k = gx_app(out, k, " : api.browserAction.setBadgeText({ tabId: d.tabId, text: String(media[d.tabId].length) });\n" as *u8) 352 k = gx_app(out, k, " }\n" as *u8) 353 k = gx_app(out, k, "}, { urls: ['<all_urls>'] });\n" as *u8) 354 k = gx_app(out, k, "api.tabs.onRemoved.addListener(function (t) { delete media[t]; });\n" as *u8) 355 k = gx_app(out, k, "// THE EDGE PUSH (R11 ge_edge_push): the estate does the capture, so there is NO native host,\n" as *u8) 356 k = gx_app(out, k, "// no bundled binary and nothing to install on Windows. One authenticated POST and we are done.\n" as *u8) 357 k = gx_app(out, k, "async function push(url, tabUrl) {\n" as *u8) 358 k = gx_app(out, k, " const st = await api.storage.local.get(SESSION_KEY).catch(function () { return {}; });\n" as *u8) 359 k = gx_app(out, k, " const tok = (st === undefined) ? '' : (st[SESSION_KEY] || '');\n" as *u8) 360 k = gx_app(out, k, " if (tok.length === 0) { return { error: 'no-session', msg: 'open nishifamily.com once to seat the session' }; }\n" as *u8) 361 k = gx_app(out, k, " const hdr = { 'Content-Type': 'application/x-www-form-urlencoded' };\n" as *u8) 362 k = gx_app(out, k, " hdr[SESSION_HEADER] = tok;\n" as *u8) 363 k = gx_app(out, k, " const body = 'url=' + encodeURIComponent(url) + '&referer=' + encodeURIComponent(tabUrl || '');\n" as *u8) 364 k = gx_app(out, k, " const r = await fetch(EDGE + CAPTURE_PATH, { method: 'POST', headers: hdr, body: body });\n" as *u8) 365 k = gx_app(out, k, " if (r.ok === false) { return { error: 'edge-' + r.status, msg: await r.text() }; }\n" as *u8) 366 k = gx_app(out, k, " return { ok: true };\n" as *u8) 367 k = gx_app(out, k, "}\n" as *u8) 368 k = gx_app(out, k, "api.runtime.onMessage.addListener(function (msg, sender, reply) {\n" as *u8) 369 k = gx_app(out, k, " if (msg.q === 'list') { reply({ items: media[msg.tabId] || [] }); return true; }\n" as *u8) 370 k = gx_app(out, k, " if (msg.q === 'savetoken') { const o = {}; o[SESSION_KEY] = msg.token; api.storage.local.set(o); reply({ ok: true }); return true; }\n" as *u8) 371 k = gx_app(out, k, " if (msg.q === 'push') { push(msg.url, msg.tabUrl).then(reply); return true; }\n" as *u8) 372 k = gx_app(out, k, " return false;\n" as *u8) 373 k = gx_app(out, k, "});\n" as *u8) 374 if k >= cap { return 0 - 1 } 375 return k 376} 377 378// ---- content script ---------------------------------------------------------------------------- 379func gx_emit_content(conf: *u8, cn: i64, out: *u8, cap: i64) -> i64 { 380 let off: *i64 = sys_mmap(16) as *i64 381 var k: i64 = 0 382 k = gx_app(out, k, "// GENERATED by nx_grab_ext_emit from knowledge/grab_ext.conf -- do not hand-edit.\n" as *u8) 383 k = gx_app(out, k, "const api = (typeof browser === 'object') ? browser : chrome;\n" as *u8) 384 k = gx_app(out, k, "// Seat the session: the estate's own page keeps an OPAQUE token in localStorage (no cookies, estate law C1).\n" as *u8) 385 k = gx_app(out, k, "// Harvest it ONLY on the estate's own origin, never on a third-party page.\n" as *u8) 386 k = gx_app(out, k, "const EDGE = '" as *u8) 387 let el: i64 = gx_need(conf, cn, "edge" as *u8, off) 388 k = gx_appn(out, k, ((conf as i64) + off[0]) as *u8, el) 389 k = gx_app(out, k, "';\n" as *u8) 390 k = gx_app(out, k, "const SESSION_KEY = '" as *u8) 391 let sk: i64 = gx_need(conf, cn, "session_key" as *u8, off) 392 k = gx_appn(out, k, ((conf as i64) + off[0]) as *u8, sk) 393 k = gx_app(out, k, "';\n" as *u8) 394 k = gx_app(out, k, "if (location.origin === EDGE) {\n" as *u8) 395 k = gx_app(out, k, " const t = localStorage.getItem(SESSION_KEY);\n" as *u8) 396 k = gx_app(out, k, " if (t === null) { /* not signed in yet */ } else { api.runtime.sendMessage({ q: 'savetoken', token: t }); }\n" as *u8) 397 k = gx_app(out, k, "}\n" as *u8) 398 if k >= cap { return 0 - 1 } 399 return k 400} 401 402// ---- output ------------------------------------------------------------------------------------ 403func gx_write_file(path: *u8, buf: *u8, n: i64) -> i64 { 404 let fd: i64 = sys_openat_wr(path, GX_FILE_MODE) 405 if fd < 0 { return 0 - 1 } 406 var off: i64 = 0 407 while off < n { 408 let w: i64 = sys_write(fd, ((buf as i64) + off) as *u8, n - off) 409 if w <= 0 { sys_close(fd); return 0 - 1 } 410 off = off + w 411 } 412 sys_close(fd) 413 return 0 414} 415 416func gx_join(dir: *u8, leaf: *u8, out: *u8) -> i64 { 417 var k: i64 = gx_app(out, 0, dir) 418 k = gx_app(out, k, "/" as *u8) 419 k = gx_app(out, k, leaf) 420 out[k] = 0 as u8 421 return k 422} 423 424func gx_emit_one(dir: *u8, leaf: *u8, body: *u8, n: i64) -> i64 { 425 let p: *u8 = sys_mmap(GX_PATH_CAP) 426 gx_join(dir, leaf, p) 427 if gx_write_file(p, body, n) < 0 { 428 gx_w("EMIT WRITE-FAIL path=" as *u8); gx_w(p); gx_w("\n" as *u8) 429 return 0 - 1 430 } 431 gx_w(" wrote " as *u8); gx_w(p); gx_w(" bytes=" as *u8); gx_wn(n); gx_w("\n" as *u8) 432 return 0 433} 434 435// ---- ge_emit: THE CONTRACT SYMBOL (/compare/mediaingest R10) ----------------------------------- 436// One call emits both twins under `root`. This is the real entry point, not a name added to satisfy a 437// watch: main() is argv handling and nothing else, so anything that wants to emit the extension -- 438// a beat, a ship loop, another organ -- composes THIS rather than forking a program. 439// ⚠The board's watch row measures whether this SYMBOL EXISTS. That is a receipt, never a proof: a stub 440// carrying this name would light the cell green with no capability behind it. The proof is 441// nx_grab_ext_emit_gate, whose teeth check the emitted MV2/MV3 structure in both directions. 442func ge_emit(root: *u8) -> i64 { 443 let szp: *i64 = sys_mmap(16) as *i64 444 szp[0] = 0 445 let conf: *u8 = sys_read_file(GX_CONF_PATH, szp) 446 var cn: i64 = 0 447 if (conf as i64) != 0 { cn = szp[0] } 448 if cn <= 0 { 449 gx_w("EMIT REFUSED no-conf path=" as *u8); gx_w(GX_CONF_PATH) 450 gx_w(" -- this organ emits FROM DATA and has nothing to emit from\n" as *u8) 451 sys_exit(GX_EXIT_NOCONF); return GX_EXIT_NOCONF 452 } 453 454 // The edge must be https. An http edge would carry the opaque session token in clear text, and the 455 // extension pushes that token on every capture -- so this is a refusal, not a warning. 456 let off: *i64 = sys_mmap(16) as *i64 457 let el: i64 = gx_need(conf, cn, "edge" as *u8, off) 458 if gx_starts(conf, off[0], el, "https://" as *u8) == 0 { 459 gx_w("EMIT REFUSED edge-not-https edge=" as *u8) 460 gx_wb(((conf as i64) + off[0]) as *u8, el) 461 gx_w(" -- the session token rides every push; an http edge would publish it in clear\n" as *u8) 462 sys_exit(GX_EXIT_BADEDGE); return GX_EXIT_BADEDGE 463 } 464 465 let mv2dir: *u8 = sys_mmap(GX_PATH_CAP) 466 let mv3dir: *u8 = sys_mmap(GX_PATH_CAP) 467 gx_join(root, "mv2" as *u8, mv2dir) 468 gx_join(root, "mv3" as *u8, mv3dir) 469 sys_mkdir(root, GX_DIR_MODE) 470 sys_mkdir(mv2dir, GX_DIR_MODE) 471 sys_mkdir(mv3dir, GX_DIR_MODE) 472 473 let out: *u8 = sys_mmap(GX_OUT_CAP) 474 gx_w("EMIT root=" as *u8); gx_w(root); gx_w("\n" as *u8) 475 476 let n2: i64 = gx_emit_mv2(conf, cn, out, GX_OUT_CAP) 477 if n2 < 0 { gx_w("EMIT OVERFLOW mv2-manifest\n" as *u8); sys_exit(GX_EXIT_OVERFLOW); return GX_EXIT_OVERFLOW } 478 if gx_emit_one(mv2dir, "manifest.json" as *u8, out, n2) < 0 { sys_exit(GX_EXIT_WRITE); return GX_EXIT_WRITE } 479 480 let n3: i64 = gx_emit_mv3(conf, cn, out, GX_OUT_CAP) 481 if n3 < 0 { gx_w("EMIT OVERFLOW mv3-manifest\n" as *u8); sys_exit(GX_EXIT_OVERFLOW); return GX_EXIT_OVERFLOW } 482 if gx_emit_one(mv3dir, "manifest.json" as *u8, out, n3) < 0 { sys_exit(GX_EXIT_WRITE); return GX_EXIT_WRITE } 483 484 // ONE body, BOTH twins: the background and content scripts are byte-identical across MV2 and MV3 by 485 // construction, because they are emitted once and written twice. Only the manifests differ. 486 let nb: i64 = gx_emit_bg(conf, cn, out, GX_OUT_CAP) 487 if nb < 0 { gx_w("EMIT OVERFLOW background\n" as *u8); sys_exit(GX_EXIT_OVERFLOW); return GX_EXIT_OVERFLOW } 488 if gx_emit_one(mv2dir, "background.js" as *u8, out, nb) < 0 { sys_exit(GX_EXIT_WRITE); return GX_EXIT_WRITE } 489 if gx_emit_one(mv3dir, "background.js" as *u8, out, nb) < 0 { sys_exit(GX_EXIT_WRITE); return GX_EXIT_WRITE } 490 491 let nc: i64 = gx_emit_content(conf, cn, out, GX_OUT_CAP) 492 if nc < 0 { gx_w("EMIT OVERFLOW content\n" as *u8); sys_exit(GX_EXIT_OVERFLOW); return GX_EXIT_OVERFLOW } 493 if gx_emit_one(mv2dir, "content.js" as *u8, out, nc) < 0 { sys_exit(GX_EXIT_WRITE); return GX_EXIT_WRITE } 494 if gx_emit_one(mv3dir, "content.js" as *u8, out, nc) < 0 { sys_exit(GX_EXIT_WRITE); return GX_EXIT_WRITE } 495 496 let alt: *u8 = sys_mmap(GX_VAL_CAP) 497 let al: i64 = gx_media_alt(conf, cn, alt, GX_VAL_CAP) 498 gx_w("EMIT ge_emit twins=2 media_types=" as *u8); gx_wn(gx_count(conf, cn, "media_ext" as *u8)) 499 gx_w(" perms=" as *u8); gx_wn(gx_count(conf, cn, "perm" as *u8)) 500 gx_w(" host_perms=" as *u8); gx_wn(gx_count(conf, cn, "host_perm" as *u8)) 501 gx_w(" alt=" as *u8); gx_wb(alt, al) 502 gx_w(" mv2_manifest=" as *u8); gx_wn(n2) 503 gx_w(" mv3_manifest=" as *u8); gx_wn(n3) 504 gx_w(" background=" as *u8); gx_wn(nb) 505 gx_w(" content=" as *u8); gx_wn(nc) 506 gx_w("\n" as *u8) 507 return GX_EXIT_OK 508} 509 510func main(argc: i64, argv: *i64) -> i64 { 511 var root: *u8 = "knowledge/grab_ext" as *u8 512 if argc >= 2 { root = argv[1] as *u8 } 513 return ge_emit(root) 514}