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}