OWASP ZAP Scan — Every Release

A release is tagged only after the deployment serving the candidate build has been scanned with OWASP ZAP (Zed Attack Proxy), and the release notes page says what the scan found. The scan is a precondition in Release Process & Versioning, beside the sealed image set and the notes page; the release skill carries the same commands for the operator. Raw reports never enter a repository — they are a site map with response bodies — so the record that ships is the verdict line and the finding table below.

The two runs

The portal has two surfaces, and one run cannot cover both: everything a signed-in user loads — the code editor, the settings tabs, the layout areas — is invisible to an anonymous spider, and an active scan must never run with a real session, because it fires its payloads as that user at every write endpoint it finds.

run who mode what it reaches
Public anonymous active (zap-full-scan.py) — spiders, then attacks every input it found the public pages, sign-in, the assets they load
Authenticated a real browser session passive (zap-baseline.py -j) — spiders with the AJAX spider, inspects every response, sends no payload the signed-in portal and the bundles only its pages load
# The scanner is pinned by VERSION, and that version goes on the notes page beside the verdict;
# bumping it is a deliberate change (a new release of the scanner brings new rules). $OUT is a
# PLAIN directory (a Docker bind mount — an agent scratchpad is not mountable), one sub-folder
# per run, and each run's console output IS its log: the verdict is that log's last line, so it
# is captured — the scripts exit 0 on PASS, 2 on WARN, 1 on FAIL, 3 on a scanner error.
ZAP=ghcr.io/zaproxy/zaproxy:2.17.0
OUT=~/.cache/zap-scan-$(date -u +%F); mkdir -p "$OUT/public" "$OUT/auth"

# 1. Public, ACTIVE — anonymous, so safe against production.
docker run --rm -v "$OUT/public":/zap/wrk/:rw -t "$ZAP" \
  zap-full-scan.py -t https://memex.meshweaver.cloud \
  -r public-full.html -J public-full.json -w public-full.md \
  > "$OUT/public/public-full.log" 2>&1; echo "public scan exit=$?"

# 2. Authenticated, PASSIVE — $COOKIE is the full `Cookie` header of a real browser session
#    (the OIDC `.AspNetCore.Cookies` session; an API token does not authenticate the SPA).
#    -j turns on the AJAX spider, which is what reaches the assets a signed-in page loads.
docker run --rm -v "$OUT/auth":/zap/wrk/:rw -t "$ZAP" \
  zap-baseline.py -t https://memex.meshweaver.cloud -j \
  -r auth-report.html -J auth-report.json -w auth-report.md \
  -z "-config replacer.full_list(0).description=sess -config replacer.full_list(0).enabled=true \
      -config replacer.full_list(0).matchtype=REQ_HEADER -config replacer.full_list(0).matchstr=Cookie \
      -config replacer.full_list(0).regex=false -config replacer.full_list(0).replacement=$COOKIE" \
  > "$OUT/auth/auth-baseline.log" 2>&1; echo "authenticated scan exit=$?"

The cookie is a live session: it stays in the shell that runs the scan, never in a file under a repository, and the session is signed out when the run ends.

🚨 Neither run is automatable, and there is no CI lane — measured 2026-09-07: no workflow in Systemorph/MeshWeaver or MeshWeaver.Plugins invokes ZAP. The authenticated run needs a HUMAN: the session cookie can only come from a real interactive sign-in (the generic authenticated-scan skill opens a Playwright-driven Chrome and waits for the person to log in). So an agent can prepare a release, but it cannot produce this precondition — plan for an operator step. 🚨 And when the operator uses that skill's zap-auth-scan.sh, its invocation is not this one: it runs zaproxy:stable rather than the pinned version, and it omits -j, so it neither names a scanner version for the notes page nor runs the AJAX spider that reaches a signed-in SPA's assets. Capture the cookie with the skill if that is convenient, then run the command above with it.

The verdict

The last line of each run's log (the console output captured above) is the verdict:

FAIL-NEW: 0	FAIL-INPROG: 0	WARN-NEW: 9	WARN-INPROG: 0	INFO: 0	IGNORE: 0	PASS: 58

What the scanner cannot see

Verifying the editor still works — the positive control for the Monaco remedy

🚨 Removing a vulnerable bundle and breaking the editor is a worse outcome than the finding. Monaco is the portal's code and markdown edit surface, and every measurement in the section above stops at the asset boundary: a file answering 200 is not an editor that runs, and no test in either repository executes this shell's editor JavaScript. So the remedy needs a positive control, and it has to run against the bytes the portal actually serves rather than a local build.

The enabling fact is a side effect of the fix. BlazorMonaco's AMD loader fetched the editor lazily, when an editor was created, which needs a signed-in page — that is why this rule used to be authenticated-only. The bundle the portal builds itself is loaded eagerly by the app shell on every page, /login included, so the whole editor is reachable anonymously. The positive control therefore needs no session, no test account and no OIDC cookie, and can be run against production at any time.

Drive a headless browser at the anonymous shell, wait for window.monacoReady (the promise App.razor publishes and MonacoEditorView awaits), then exercise the editor through the same APIs the Blazor interop uses. The checks that matter, and what each one would catch:

check how what a failure means
the bundle loads await window.monacoReady; window.monaco is defined a 404 or a parse error — the dead-editor case the asset check cannot see
the interop's contract holds monaco.editor / monaco.languages / monaco.Uri present BlazorMonaco's jsInterop.js drives the editor through these only
values round-trip editor.create(...), getValue(), setValue() the Blazor interop's read/write path
syntax highlighting monaco.editor.tokenize(src, 'csharp') returns real token types, and the view carries several distinct mtk* classes grammars are lazily registered — tokenize immediately after load returns one null-state token, so poll until it changes or the check is vacuous
squiggles setModelMarkers then getModelMarkers, and a rendered .squiggly-error the exact API the LSP diagnostics path paints through
workers answer a deliberately invalid JSON model produces a marker a worker URL the bundle got wrong 404s silently
DOMPurify sanitises a hover whose markdown carries javascript: hrefs and onerror/onload attributes the rule-10003 exposure itself

🚨 The sanitisation check needs its own positive control. "No onerror in the output" is also what a renderer that rendered nothing produces. Include a benign **bold** in the same payload and assert it survives as <strong>: that distinguishes sanitised from broken.

// npm i playwright && npx playwright install chromium — run from a scratch directory, never
// committed (one-off diagnostics rot; this table and recipe are the durable form).
import { chromium } from 'playwright';
const browser = await chromium.launch();
const page = await browser.newPage();
const errors = []; const failed = [];
page.on('pageerror', e => errors.push(e.message));
page.on('console', m => m.type() === 'error' && errors.push(m.text()));
page.on('response', r => r.status() >= 400 && failed.push(`${r.url()} ${r.status()}`));

await page.goto('https://memex.meshweaver.cloud/login', { waitUntil: 'load' });
await page.evaluate(() => window.monacoReady);

console.log(await page.evaluate(async () => {
  const m = window.monaco, out = {};
  const src = 'public record Foo(int Bar)\n{\n    // c\n    public string B => "s";\n}\n';
  const host = document.createElement('div');
  host.style.cssText = 'width:1000px;height:400px;position:absolute;top:0;left:0';
  document.body.appendChild(host);
  const ed = m.editor.create(host, { value: src, language: 'csharp' });
  out.roundTrip = ed.getValue() === src;

  // the csharp grammar registers lazily — poll, or this assertion is vacuous
  const t0 = Date.now(); let types = [];
  while (Date.now() - t0 < 20000 &&
         (types = [...new Set(m.editor.tokenize(src, 'csharp').flat().map(t => t.type))]).length < 2)
    await new Promise(r => setTimeout(r, 200));
  out.tokens = types;
  await new Promise(r => setTimeout(r, 600));
  out.colourClasses = new Set([...host.querySelectorAll('.view-line span[class^="mtk"]')]
    .map(s => s.className)).size;

  m.editor.setModelMarkers(ed.getModel(), 'probe', [{
    startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 7,
    message: 'probe', severity: m.MarkerSeverity.Error }]);
  out.markers = m.editor.getModelMarkers({ owner: 'probe' }).length;
  out.rendered = host.querySelectorAll('.squiggly-error').length;

  // workers answer: only the JSON worker can mark an invalid JSON model — poll for its marker
  const jm = m.editor.createModel('{ "a": 1, }', 'json', m.Uri.parse('inmemory://probe/bad.json'));
  const j0 = Date.now(); let jmk = [];
  while (Date.now() - j0 < 20000 && (jmk = m.editor.getModelMarkers({ resource: jm.uri })).length === 0)
    await new Promise(r => setTimeout(r, 100));
  out.jsonWorker = { ms: Date.now() - j0, messages: jmk.map(x => x.message) };

  m.languages.registerHoverProvider('csharp', { provideHover: () => ({ contents: [{
    value: '[x](javascript:alert(1))\n\n<img src=x onerror="alert(2)">\n\n'
         + '<svg onload="alert(3)"></svg>\n\n**bold survives**',
    supportHtml: true, isTrusted: false }] }) });
  return out;
}));

// open the hover with a real mouse move, then read what was rendered
const at = await page.evaluate(() => { const r = document.querySelector('.view-line')
  .getBoundingClientRect(); return { x: r.x + 40, y: r.y + r.height / 2 }; });
await page.mouse.move(at.x, at.y); await page.waitForTimeout(1800);
console.log(await page.evaluate(() => {
  const html = document.querySelector('.monaco-hover')?.innerHTML ?? '';
  return { onerror: /onerror/i.test(html), onload: /onload/i.test(html),
           jsHref: /href\s*=\s*["']?javascript:/i.test(html),
           boldSurvived: /<strong>bold survives<\/strong>/.test(html) };
}));
console.log({ errors, failed });
await browser.close();

2026-09-11 — run against both portals

curl on the served bytes, then the headless run above against https://memex.meshweaver.cloud/login:

memex.meshweaver.cloud memex.systemorph.com
/api/version 3.0.0+6231c4da 3.0.0+45306a33
…/MeshWeaver.Blazor/lib/monaco-editor/monaco.js 200 · 4,483,269 B · sha256:8e991296…b039e846 200 · 4,483,269 B · same sha256
its DOMPurify banners exactly one: 3.4.14 exactly one: 3.4.14
versions.json monaco-editor 0.56.0 · dompurify 3.4.14 · dompurify-vendored-by-monaco 3.4.8 identical
…/BlazorMonaco/…/min/vs/editor.api-CalNCsUg.js 404 404
…/BlazorMonaco/…/min/vs/loader.js 404 404
…/BlazorMonaco/jsInterop.js 200 · 41,627 B 200 · 41,627 B

🚨 This table is what the portals SERVED on 2026-09-11, and 3.4.14 is no longer what is committed. MeshWeaver.Plugins#1640 moved the pin to 3.4.15 the same day — see DOMPurify 3.4.15 below for the new bytes — so both portals answer 8e991296… until the next roll and 51408c8f… after it. Neither reading changes rule 10003: both versions are above the retire.js floor.

🚨 memex.systemorph.com has now rolled. On 2026-09-08 it was still on ci.8059 and still served the flagged 3.6 MB chunk; it no longer does. The pre-fix control described in the previous section is therefore expired — the fleet no longer has a portal on the old side of the filter, and reconstructing it now means pulling an old image.

The headless run, against the anonymous shell:

So the editor the portal serves is intact end to end, on the same bytes the scanner reads.

Running the same check BEFORE the roll, with no portal at all

The recipe above drives a portal, so it can only answer after a deploy. On a bundle bump the only thing that changed is the bundle, so the same checks run against the freshly built bytes on a static file server: stage the built folder under _content/MeshWeaver.Blazor/lib/monaco-editor/ and BlazorMonaco's jsInterop.js under _content/BlazorMonaco/, put App.razor's bootstrap block copied verbatim into a bare page with <base href="/">, serve it, and point the script at http://127.0.0.1:<port>/. Copying the bootstrap rather than paraphrasing it is the point — a hand-written one tests your bootstrap, not the portal's. Every row of the table is reachable this way, workers and hover sanitisation included; the step-by-step is in tools/monaco-editor/README.md (MeshWeaver.Plugins). What it does not cover is delivery — that the portal serves these bytes — which is the curl half above and only answerable after the roll.

2026-09-11 — DOMPurify 3.4.15, verified on the bundle before it shipped

MeshWeaver.Plugins#1640 moved the pinned sanitiser 3.4.14 → 3.4.15 (published 2026-09-06; https://registry.npmjs.org/dompurify/latest answered HTTP 200 / 6,547 B with version: 3.4.15, re-measured 2026-09-11). 🚨 This was freshness, not exposure, and the distinction is worth keeping straight: 3.4.14 was already above the retire.js floor of 3.4.13, so nothing was flagged and no scan moved. OSV answers zero vulnerabilities for both 3.4.14 and 3.4.15 (api.osv.dev/v1/query, HTTP 200), the newest GitHub advisory touching the package is GHSA-55q2-fjhq-7xh7 (<= 3.4.12, patched 3.4.13), and 3.4.15's own release notes are hardening — clobbering when XML content is involved, edge cases, dependency bumps — with no advisory attached. The bump is the standing use the latest within the minor line directive, nothing more.

before after
monaco.js 4,483,269 B · sha256:8e991296…b039e846 4,483,398 B · sha256:51408c8f804b7723b55b9ffb66ec7df202ca7cdfcfc86ffc02a248ef5691dfb4
banners in it exactly one: @license DOMPurify 3.4.14 exactly one: @license DOMPurify 3.4.15
versions.json monaco-editor 0.56.0 · dompurify 3.4.14 · vendored 3.4.8 monaco-editor 0.56.0 · dompurify **3.4.15** · vendored 3.4.8
the five workers byte-identical

The workers not moving is the check that the swap stayed confined: only esm/vs/base/browser/domSanitize.js imports the vendored sanitiser and no worker entry point reaches it, so a DOMPurify-only bump that moved one would mean something else changed too. Two consecutive builds produced the same sha256, so the output is reproducible and a differing hash is a real difference.

The full editor check was run against those bytes on the local harness described above, and matched the portal run line for line: window.monacoReady resolved, 91 languages registered, editor created, getValue/setValue round-tripped, the csharp grammar registered after 203 ms and tokenized to real types with 5 distinct mtk* colour classes rendered, one Error marker round- tripped through setModelMarkers/getModelMarkers painting one .squiggly-error, the JSON worker answered an invalid model in 103 ms (Trailing comma), and zero console errors and zero failed or 4xx requests. The hover rendered as

<div class="rendered-markdown"><p>x</p><img>

<p></p><p><strong>bold survives</strong></p></div>

[x](javascript:alert(1)) reduced to its text, <img src=x onerror=…> stripped to a bare <img>, <svg onload=…> dropped entirely, nothing executed, and **bold survives** through as <strong>. That last clause is the whole point of the check: without it, an empty output would be indistinguishable from a renderer that failed.

Licences now travel with the bytes. esbuild keeps DOMPurify's /*! @license … */ banner inline — that banner is what retire.js reads, and it carries a permalink pinned to the exact tag — but a banner is not a licence text. build.mjs now also emits LICENSE-monaco-editor-MIT.txt, LICENSE-dompurify-Apache-2.0.txt and LICENSE-dompurify-MPL-2.0.txt beside the output (Monaco is MIT; DOMPurify is dual licensed Apache-2.0 OR MPL-2.0 and ships both). They have to be emitted rather than committed by hand: the output directory is wiped on every build.

🚨 npm audit reports a vulnerable dompurify in this workspace and it is not the output. Read the path — node_modules/monaco-editor/node_modules/dompurify is Monaco's own transitive 3.4.8, the copy this build exists to replace. npm audit fix --force "fixes" it by downgrading monaco-editor to 0.53.0. The output's only DOMPurify is the top-level pin, and build.mjs fails on any banner but that one.

Findings by release

3.0.0 — scanned 2026-09-06 against memex.meshweaver.cloud, ZAP 2.17.0

The full report of this scan — coverage, attack classes exercised, the delta against 23 August, live header verification, CORS posture and limitations — is OWASP ZAP Scan — 3.0.0 (6 September 2026). The table below carries the CURRENT disposition of each rule, which may be newer than the report's.

run verdict endpoints
public, active FAIL-NEW: 0 · WARN-NEW: 5 · PASS: 136 296
authenticated, passive FAIL-NEW: 0 · WARN-NEW: 9 · PASS: 58 179
rule level run instances disposition
Vulnerable JS Library [10003] — DOMPurify 3.2.7 inside BlazorMonaco's Monaco bundle Medium authenticated 1 Fixed and DELIVERED, re-scan still owed: MeshWeaver#3378 — the portal builds its own Monaco with DOMPurify 3.4.14 (MeshWeaver.Plugins#1393, tools/monaco-editor), guarded by MonacoBundleGuard. Delivery measured 2026-09-07 on the served bytes, not on the merge: GET /_content/MeshWeaver.Blazor/lib/monaco-editor/monaco.js answers 200 / 4,483,269 bytes / sha256:8e991296e5e49dca83a02afa00a0eca20128a5c530b9996ce00830e6b039e846 on both memex.meshweaver.cloud and memex.systemorph.com — byte-identical to the committed bundle on MeshWeaver.Plugins main — carrying /*! @license DOMPurify 3.4.14 (versions.json: monaco-editor 0.56.0, dompurify 3.4.14). Corroborated by an anonymous re-scan on 2026-09-07 that provably reached the bundle (10003 PASS over 1330 URLs, 10096 on monaco.js); the issue still closes on the AUTHENTICATED re-scan. Residue CLOSED by MeshWeaver#3617 (MeshWeaver.Plugins#1482): the retired min/vs tree is no longer published, measured 2026-09-08 on the shipped image (366 files under _content/BlazorMonaco → 3; 726 retired endpoints → 0) and on the wire (the flagged editor.api-CalNCsUg.js answers 404 on a portal running ≥ ci.8079) — see What the scanner cannot see. That removes the URL the alert instanced, and with it the only DOMPurify 3.2.7 the origin served; it does NOT by itself settle rule 10003, which is a verdict over every library the signed-in portal loads. 🚨 A package bump is still not an alternative remedy, re-measured against the live registry 2026-09-11: the NuGet flat-container index for blazormonaco answers HTTP 200 with 3.5.0 still the newest release, and blazormonaco.3.5.0.nupkg (HTTP 200, 4,514,906 B) still carries min/vs/loader.js declaring Monaco 0.42.0-dev-20230906 and an editor.api banner-stamped DOMPurify 3.2.7. Upstream monaco-editor is 0.56.0 — the version the portal already builds — and it vendors DOMPurify 3.4.8, below the retire.js floor, so even a hypothetical BlazorMonaco carrying current Monaco would not by itself clear the rule. The pin is now held by BlazorMonacoPinGuard (core, test/MeshWeaver.Documentation.Test/). 2026-09-11: both portals answer 404 on the flagged URL (memex.systemorph.com has rolled), and the editor is verified working end to end — see Verifying the editor still works.
Backup File Disclosure [10095] Medium public 21 False positive, measured: every instance is /static/NodeTypeIcons/Copy (n) of <icon>.svg, and that route synthesises an icon for ANY name — a nonsense name answers 200 with a 547-byte SVG of its own, while bot.svg.bak is 404 — so no file is disclosed; the rule keys on "a variant of the URL also answers 200". Carried: the fallback icon is the feature.
Proxy Disclosure [40025] Medium public systemic False positive, measured: TRACE and OPTIONS answer 405 (allow: GET, POST) with no Server/Via header; the "Unknown proxy" is ZAP's inference from the refusal. Carried.
CSP: Failure to Define Directive with No Fallback [10055] Medium both 15 / 10 Carried by design — see the row below; form-action 'self' https: is declared on every response measured (/, /login), so the missing directive the rule names is to be re-read on the next scan.
CSP: script-src unsafe-inline · script-src unsafe-eval · style-src unsafe-inline · Wildcard Directive [10055] Medium both 3 each / 2 each Carried by design: the policy is set and explained in MemexPortalComposition.cs (MeshWeaver.Plugins; enforced since #1988 after a Report-Only run over the live pages with zero violations) — 'unsafe-inline'/'unsafe-eval', blob:/data: and https:/wss: are what the Blazor Server circuit, the editor and embedded https content need; per-response nonces and dropping 'unsafe-inline' are a separate hardening pass. Follow-up: the bundled Monaco (MeshWeaver.Plugins#1393) carries no eval/new Function, so 'unsafe-eval' — kept for the editor — can be re-measured.
Cross-Origin-Resource-Policy header missing [90004] · Cross-Origin-Embedder-Policy header missing Low both systemic / 7 Accepted: the portal embeds cross-origin resources by design (sign-in assets from the Microsoft CDNs, fonts, user-embedded media); COEP: require-corp would break them, and CORP: same-site on the portal's own assets is the intended scope.
Dangerous JS Functions [10110] — eval( Low both 1 Fixed by MeshWeaver.Plugins#1393: the eval( is in BlazorMonaco's AMD loader.js, which the page no longer loads; the bundled Monaco has no eval and no new Function. Confirmed on the rolled portal — PASS: Dangerous JS Functions [10110] in the 2026-09-07 anonymous baseline, the same run that reached monaco.js. The eval(-bearing file itself stayed published until MeshWeaver#3617 (MeshWeaver.Plugins#1482) dropped the tree from the build output: GET …/min/vs/loader.js answered 200 · 39,848 B on ci.8059 and 404 on ci.8079, measured 2026-09-08.
Timestamp Disclosure — Unix [10096] Low authenticated 3 False positive: 1732584193, 1518500249, 1859775393 are 0x67452301, 0x5A827999, 0x6ED9EBA1 — SHA-1 round constants in Monaco's hashing code, not timestamps. The same constants sit in the new bundle and will be flagged again.
Re-examine Cache-control Directives [10015] · Non-Storable Content [10049] · Suspicious Comments [10027] · Modern Web Application [10109] Informational authenticated 5 / 11 / 15 / 5 informational — no action

An earlier scan of the same portal on 2026-08-23 had already reported rule 10003 on the Monaco bundle; the advisory list had grown by 2026-09-06, which is what turned it into MeshWeaver#3378.

2026-09-07 re-scan of rule 10003 — anonymous, passive, and NOT the acceptance run

After the fix rolled, an anonymous zap-baseline.py -j -m 5 at the same pinned ZAP 2.17.0 against memex.meshweaver.cloud read FAIL-NEW: 0 · WARN-NEW: 9 · PASS: 58 over 1330 URLs, with PASS: Vulnerable JS Library (Powered by Retire.js) [10003].

🚨 That PASS is not vacuous the way an anonymous PASS on this rule used to be, and the reason is worth keeping. The bullet above says a public run cannot see the signed-in surface — true, and until 2026-09-06 it was why only the authenticated run could report 10003: BlazorMonaco's AMD loader fetched editor.api lazily, when an editor was created, which needs a signed-in page. The new shell loads monaco.js eagerly on every page, /login included, so the editor bundle is now on the anonymous surface. The proof that the run actually retrieved and scanned it is in the same report: rule 10096 fires three times on /_content/MeshWeaver.Blazor/lib/monaco-editor/monaco.js with evidence 1732584193, 1518500249, 1859775393 — the SHA-1 round constants this page predicted would follow the new bundle. Retire.js read that file and passed it.

It is still not the acceptance measurement. The release precondition is the AUTHENTICATED baseline (it is the run that reaches libraries only a signed-in page loads), and it needs a human sign-in. What the anonymous run settles is this one rule against this one bundle; what it cannot settle is any library the signed-in portal loads and the anonymous shell does not.

2026-09-08 — the retired BlazorMonaco tree measured gone, on the artifact and on the wire

MeshWeaver.Plugins#1482 merged 2026-09-07T23:14:40Z. That morning the fleet was mid-roll and running one portal image on each side of that merge, which is what made the before column still measurable: memex-cloud had taken meshweaver.azurecr.io/memex-portal-ai:3.0.0-ci.8079 (built 2026-09-08T05:55:35Z, well after the merge), while memex was still on …:3.0.0-ci.8059, built 2026-09-07T23:06:50Z — eight minutes short of the merge, so it cannot carry the filter.

🚨 Neither image is adjacent to the merge, and the tag numbers do not say so: ci.8058 finished one second after it from a build that started before, and ci.8064 (2026-09-08T00:04:16Z) is the earliest whose build could have carried it. Tag order is not build order — read createdTime off the registry (az acr repository show-tags --orderby time_desc --detail) rather than sorting the numbers.

ci.8059 → memex.systemorph.com ci.8079 → memex.meshweaver.cloud
files under /app/wwwroot/_content/BlazorMonaco/ in the image 366 3jsInterop.js, .br, .gz
endpoints in the shipped Memex.Portal.Distributed.staticwebassets.endpoints.json 1,419 693
…naming BlazorMonaco 732 6
…under lib/monaco-editor/ 726 0
GET /_content/BlazorMonaco/jsInterop.js 200 · 41,627 B 200 · 41,627 B
GET …/min/vs/loader.js 200 · 39,848 B 404
GET …/min/vs/editor.api-CalNCsUg.js 200 · 3,669,759 B 404
GET …/min/vs/editor/editor.main.css 200 · 308,989 B 404
anonymous app shell / 96,125 B 43,826 B
its <script type="importmap"> 81,371 B 29,027 B
import map imports keys — total / BlazorMonaco / retired 181 / 121 / 120 61 / 1 / 0
import map integrity keys — total / BlazorMonaco / retired 363 / 242 / 240 123 / 2 / 0

The package ships 122 static web asset files, of which exactly one sits outside the retired tree (jsInterop.js) and exactly one is not JavaScript (min/vs/editor/editor.main.css). So the four GET rows are the whole denominator rather than a sample: they probe the kept file, the two the scanner named, and the only asset the JavaScript-only import map could never have accounted for.

Nothing lost. On ci.8079 every URL the shell's Monaco bootstrap can resolve answers 200: monaco.js (4,483,269 B, carrying @license DOMPurify 3.4.14), monaco.css, versions.json and all five workers getWorkerUrl names (editor, json, css, html, ts) — all under _content/MeshWeaver.Blazor/, none under BlazorMonaco. jsInterop.js is byte-identical across the two sets. 🚨 That is an asset-level check, not a rendered editor. No test in either repository executes this shell's editor JavaScript — measured, not assumed: MonacoBundleGuard and MonacoEditorContainerSizingGuard both read files and match regexes, the remaining Monaco-touching suites render the component tree without a browser, and the fleet's only Playwright project (clients/portal-next/e2e) drives the separate Next.js client, which serves no _content/BlazorMonaco at all. A dead editor caused by a missing static asset would be caught by none of them. That remains true of the automated suites — but the manual control is no longer a one-off buried in a pull request: Verifying the editor still works above carries the recipe and a 2026-09-11 run of it against both portals, and it needs no session because the fix made the editor load on the anonymous shell.

Three method notes, each of which was needed to reach that verdict:

One residual, named here so it is not rediscovered as a finding: Systemorph/Memex still holds Memex.Portal.Shared/App.razor loading …/min/vs/loader.js (last touched 2026-08-13). That tree is a frozen copy that ships to nobody — the running portal is built by MeshWeaver.Plugins from src/Memex.Portal.Distributed, and that repository's Portal copies are frozen gate holds the copy in place deliberately (docs/portal-source-copies.md). It is not an exposure; it would become one only if that copy were ever revived as a build.

See also

Reconnecting…
The server was updated. Reloading the page to pick up the latest version.