Skip to content

/static serves build assets only; all mesh content moves to /api/content (#587) - #911

Merged
rbuergi merged 4 commits into
mainfrom
fix/587-static-build-assets-only
Aug 7, 2026
Merged

rbuergi merged 4 commits into
mainfrom
fix/587-static-build-assets-only

Conversation

@rbuergi

@rbuergi rbuergi commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #587. Supersedes the gate in #907, which was rejected: protected content must be unreachable under /static, not reachable-then-denied. /static now has no access control at all — because nothing that needs protecting is served there.

Two surfaces with opposite contracts:

  • /static/{mount}/{file} — build assets only. Backed by a new StaticAssetMount(Segment, Assembly, ResourcePrefix) read straight from the assembly manifest. No IContentService, no IPathResolver, no hub post, no identity resolution, no permission check anywhere. public, max-age=30d, immutable. UserContextMiddleware re-excludes /static/ — it must not have a caller to check.
  • /api/content/{node}[/{collection}]/{file} — all mesh content. Resolves the owning node with IPathResolver, then posts GetDataRequest (which carries [RequiresPermission(Read)]) to that node's hub with the caller's AccessContext. private, no-store.

The collectionCache short-circuit is deleted — it was a standing bypass: the config lookup was the only permission-bearing hop, and a cache hit skipped it entirely.

What stays on /static, and why that is safe by construction

Mount Justification
NodeTypeIcons 42 SVGs EmbeddedResource-compiled into MeshWeaver.Graph. No user data, identical in every deployment, needed before any identity exists (login page, nav, anonymous cards). ~457 existing references + DB rows (migration V41) keep working unchanged.
DocContent Images embedded in MeshWeaver.Documentation, shipped on nuget.org. Same class; ~50 references unchanged.

Both are build output, not mesh content. Serving them from the assembly manifest rather than a collection is what lets /static be access-control-free by construction instead of by policy. Both remain registered as in-mesh collections for listings/reads, but neither declares IsStatic, so neither is fetchable on the content route either.

Unmounted

  • Mesh-level storage — IsStatic removed in Memex.Portal.Monolith and aspire/Memex.Portal.Distributed. This is the collection holding every partition's content at content/{nodePath}/… — the Hardening: /static/ endpoint serves content collections without authentication or access control #587 headline hole. Now unreachable over HTTP.
  • Memex.LocalMesh's own /static — was a reduced copy that resolved any collection with no identity resolution at all. Build-asset mounts only now.
  • IsStatic (default-closed) is now actually read, as the content route's mount check.

URL generators redirected

MeshNodeImageHelper · MeshNodeThumbnailControl · MeshNodeLayoutAreas.RenderImageAsync · MarkdownFileParser (resolve + serialize strip) · ContentLayoutArea.RenderPdf · DocumentLayoutAreas · FileBrowser (both hand-duplicated download links) · ContentCollectionsExtensions.AdaptResourceUrl · MarkdownExtensions.ToStaticHref → ToContentHref — the biggest miss, and #907 never touched it: it rewrites every image in every markdown document · BrandingResolver/ExportTemplateResolver (readers, now via ContentPathReference, still accepting the legacy prefix persisted on nodes) · 5 sample layout areas · WithThumbnailsPath in Northwind/Todo · 200 authored literals across 190 sample/doc/test-data files · 3 marketing imageLinks · 6 stale doc pages · content/ai/Skill/space.md + Agent/ToolsReference.md (agent guidance that told models to write /static logos) · plugin-catalog fixture · deploy runbook.

Post-sweep, /static survives only as: route/prefix declarations, NodeTypeIcons/DocContent, prose describing the contract, and two deliberate legacy-read constants.

Verification

  • StaticContentUnmountedTest (15, real HTTP over TestServer against a real mesh): the backing store is 404 for anonymous AND for a fully-entitled admin — unreachable, not denied; icons serve identically to both with public/immutable; the same file is served to an authorized caller via /api/content; an unpublished collection 404s even for an admin; a gated response is never public.
  • ContentUrlGenerationTest (25) pins every generator + the traversal rule.
  • ContentReferenceIntegrityTest.MeshContentUrls_AreNeverAddressedThroughTheStaticRoute — the durable repo-scan guard behind "static should not be referenced by anyone". It found 9 real offenders on first run (3 marketing links + 6 stale doc pages), all fixed.

Fail-without: with three probes applied (re-mount collections on /static, drop the IsStatic check, drop the traversal guard) → 5 of 15 fail, including BackingStoreUrl_IsNotReachableUnderStatic_ForAnonymous → "Expected NotFound … but found OK" — i.e. the vulnerability reproduced. Probes reverted, 243/243 on the identical tree.

Suites: Hosting.Blazor 243/0 · Content 246 (1 pre-existing skip) · ContentCollections 28 · Markdown 137 · Markdown.Export 19 · Documentation 19 · Autocomplete 150 · Memex.Portal.Shared 238 · ContentCollections.Indexing.Graph 31. Independently re-verified 40/40 across the three new classes after merging current main. -c Release -warnaserror clean on all 16 touched projects.

🚨 Review before merge — broad blast radius (243 files)

  1. Every content URL in the product changed. Server-side coverage is green but no browser E2E was run — worth a /playwright pass over images, PDFs, the file browser and doc diagrams.
  2. Persisted legacy values in live DBs. Node Icon / CorporateIdentity.LogoPath rows holding /static/storage/content/… still resolve through the readers, but a stored value rendered directly into an <img src> will 404 until re-saved. No migration written — flagged rather than guessed.
  3. Externally-authored plugin repos carrying /static/... URLs (e.g. an AgenticEngineering poster) now 404; those need a content-side edit.
  4. /api/content/{node}/{file}: if the file path's first segment matches a collection name on that node, the explicit-collection branch wins. Documented; no collision in the current tree.
  5. AdaptResourceUrl with a null address now returns the relative URL unchanged — zero callers, contract-only.

Closes #587

🤖 Generated with Claude Code

Fixes #587. Supersedes the gate in #907, which was rejected: protected
content must be UNREACHABLE under /static, not reachable-then-denied.
/static now has no access control at all, because nothing needing
protection is served there.

Two surfaces, opposite contracts:
- /static/{mount}/{file} — build assets only, backed by a new
  StaticAssetMount read straight from the assembly manifest. No
  IContentService, no IPathResolver, no hub post, no identity, no
  permission check anywhere. public/immutable. UserContextMiddleware
  re-excludes /static/ — it must not even HAVE a caller to check.
- /api/content/{node}[/{collection}]/{file} — all mesh content. Resolves
  the owner via IPathResolver, then posts GetDataRequest (which carries
  RequiresPermission(Read)) to that node's hub with the caller's
  AccessContext. private/no-store.

The collectionCache short-circuit is DELETED: the config lookup was the
only permission-bearing hop, and a cache hit skipped it entirely — a
standing bypass.

Staying on /static: NodeTypeIcons (42 SVGs EmbeddedResource-compiled into
MeshWeaver.Graph) and DocContent (images shipped in the nuget package).
Both are build output, not mesh content, and serving them from the manifest
rather than a collection is what makes /static access-control-free BY
CONSTRUCTION rather than by policy. Neither declares IsStatic, so neither
is fetchable on the content route either.

Unmounted: the mesh-level storage collection — every partition's content
at content/{nodePath}, the #587 headline hole — and Memex.LocalMesh's own
/static, which resolved any collection with no identity resolution at all.
IsStatic (default-closed) is now actually READ, as the content route's
mount check.

Generators redirected across the repo, including MarkdownExtensions
.ToStaticHref to ToContentHref, which rewrites every image in every
markdown document and which #907 never touched; plus 200 authored literals
across 190 files, marketing links, stale doc pages, and the agent guidance
that told models to write /static logos. Readers still accept the legacy
prefix persisted on nodes.

Tests: StaticContentUnmountedTest (15, real HTTP over TestServer) proves
the backing store is 404 for anonymous AND for a fully-entitled admin —
unreachable, not denied — while the same file IS served to an authorized
caller via /api/content; ContentUrlGenerationTest (25) pins every
generator; ContentReferenceIntegrityTest is the durable repo-scan guard
behind "static should not be referenced by anyone" and found 9 real
offenders on its first run.

Fail-without: with probes re-mounting the collections, 5 of 15 fail
including BackingStoreUrl_IsNotReachableUnderStatic_ForAnonymous —
"Expected NotFound but found OK", the vulnerability reproduced. Probes
reverted: 243/243. Suites green across Blazor 243, Content 246, Markdown
137, Portal.Shared 238 and six more; independently re-verified 40/40 on the
three new classes after merging main. Release -warnaserror clean on all 16
touched projects.

Closes #587

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 7, 2026 11:44

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens content serving by making /static serve build assets only (no identity, no ACL), and moving all mesh content file serving to the access-controlled /api/content/... route. It updates URL generators, docs, samples, and adds/adjusts tests to ensure protected content is unreachable under /static.

Changes:

  • Introduces build-asset mounts (e.g., NodeTypeIcons, DocContent) for /static, and ensures mesh content is published only via /api/content.
  • Redirects content URL generation across markdown, layout areas, file browser, branding/export readers, and sample data from /static/... to /api/content/....
  • Adds/updates integration + guard tests to enforce “unreachable under /static” and “no /static references” invariants.

Reviewed changes

Copilot reviewed 242 out of 243 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
test/MeshWeaver.PluginCatalog.Test/NodeRepoPluginListingTest.cs Updates plugin fixture poster URL to /api/content/... and asserts new value.
test/MeshWeaver.Hosting.Monolith.Test/TestData/MeshWeaver/Documentation/DataMesh/UnifiedPath.md Updates test doc front-matter Icon URL to /api/content/....
test/MeshWeaver.Hosting.Blazor.Test/UserContextMiddlewareExclusionTest.cs Updates middleware exclusion expectations: /static excluded again; /api/content must resolve user.
test/MeshWeaver.Hosting.Blazor.Test/MeshWeaver.Hosting.Blazor.Test.csproj Adds monolith test base reference for real HTTP endpoint tests.
test/MeshWeaver.Content.Test/MarkdownFileParserTest.cs Updates expectations for resolved icon URLs and serialization behavior under /api/content.
test/MeshWeaver.Autocomplete.Test/AutocompleteIconTests.cs Updates icon expectations to /api/content/... in autocomplete results.
src/MeshWeaver.Markdown/MarkdownExtensions.cs Rewrites markdown image hrefs to use api/content/... (relative to <base href>).
src/MeshWeaver.Markdown.Export/Branding/ExportTemplateResolver.cs Switches template path parsing to ContentPathReference.
src/MeshWeaver.Markdown.Export/Branding/CorporateIdentity.cs Updates docs to reference /api/content/... path style.
src/MeshWeaver.Markdown.Export/Branding/BrandingResolver.cs Uses ContentPathReference to accept content: + legacy + /api/content shapes.
src/MeshWeaver.Layout/DocumentSourceControl.cs Updates docs/examples to indicate source file is served via /api/content/....
src/MeshWeaver.Hosting/Persistence/Parsers/MarkdownFileParser.cs Resolves relative icon paths via content-route helpers and avoids re-serializing resolved URLs.
src/MeshWeaver.Graph/SettingsLayoutArea.cs Updates UI placeholders/docs to stop suggesting /static content paths.
src/MeshWeaver.Graph/NodeTypeLayoutAreas.cs Updates UI placeholders/docs to stop suggesting /static content paths.
src/MeshWeaver.Graph/NodeIconPickerDialog.cs Updates docs to reflect /api/content rewrite behavior.
src/MeshWeaver.Graph/MeshNodeThumbnailControl.cs Uses content-route helper for thumbnails (no /static for mesh content).
src/MeshWeaver.Graph/MeshNodeLayoutAreas.cs Updates image rendering to use /api/content route.
src/MeshWeaver.Graph/MeshNodeImageHelper.cs Uses content-route helper for content: icon references.
src/MeshWeaver.Graph/CreateLayoutArea.cs Updates docs to reflect /api/content icon URL support.
src/MeshWeaver.Graph/Configuration/GraphConfigurationExtensions.cs Registers NodeTypeIcons as a build-asset mount for /static.
src/MeshWeaver.GitSync/ContentAssetMapper.cs Updates docs to reflect new content URL scheme.
src/MeshWeaver.Documentation/DocumentationExtensions.cs Registers DocContent as a build-asset mount; publishes page assets via content route.
src/MeshWeaver.Documentation/Data/index.md Updates documentation space icon URL to /api/content/....
src/MeshWeaver.Documentation/Data/DataMesh/UnifiedPath.md Updates docs to describe /api/content/... as the HTTP surface for files.
src/MeshWeaver.Documentation/Data/DataMesh/CreatingNodeTypes.md Updates example icon URL to /api/content/....
src/MeshWeaver.Documentation/Data/Architecture/StaticRepoImport.md Updates docs to reference /api/content/... serving of imported assets.
src/MeshWeaver.Documentation/Data/Architecture/OnDeviceVoice.md Updates catalog URL to /api/content/... for model downloads.
src/MeshWeaver.Documentation/Data/Architecture/OnboardingNewEnvironment.md Updates rollout verification probes from /static to /api/content.
src/MeshWeaver.Documentation/Data/Architecture/AuthoringDocumentation.md Updates authoring guidance to avoid /static for content.
src/MeshWeaver.ContentCollections/ContentService.cs Ensures IsStatic publication is taken from the mapping config, not inherited from backing store.
src/MeshWeaver.ContentCollections/ContentPathReference.cs Adds helper to normalize content: + legacy /static/... + /api/content/... references.
src/MeshWeaver.ContentCollections/ContentLayoutArea.cs Renders PDFs via /api/content URL helper instead of /static.
src/MeshWeaver.ContentCollections/ContentCollectionConfig.cs Clarifies IsStatic now controls publication on the access-controlled content route (not /static).
src/MeshWeaver.ContentCollections.Indexing.Graph/DocumentLayoutAreas.cs Builds source-file URLs using ContentFileRoutePrefix (/api/content).
src/MeshWeaver.Blazor/Infrastructure/UserContextMiddleware.cs Re-excludes /static/ from identity resolution; clarifies /api/content is the gated route.
src/MeshWeaver.Blazor/FileExplorer/FileBrowser.razor.cs Redirects download link generation from /static/... to /api/content/....
src/MeshWeaver.Blazor/Components/NamedAreaView.razor.cs Updates docs to reflect /api/content/... site-relative meta URL resolution.
samples/Todo/MeshWeaver.Todo/TodoApplicationExtensions.cs Publishes embedded resources on /api/content and updates thumbnails path.
samples/Northwind/MeshWeaver.Northwind.Application/NorthwindViewModels.cs Updates thumbnails path from /static to /api/content.
samples/Northwind/MeshWeaver.Northwind.Application/NorthwindApplicationExtensions.cs Publishes embedded resources on /api/content.
samples/Insurance/MeshWeaver.Insurance.Domain/InsuranceApplicationExtensions.cs Publishes embedded resources and mapped submission files on /api/content.
samples/Graph/Data/Systemorph/Marketing/*.json Updates many sample icon/imageLink URLs from /static to /api/content.
samples/Graph/Data/Systemorph.json Updates sample space icon/logo URLs to /api/content.
samples/Graph/Data/Northwind/ReportsCatalog/Source/ReportsCatalogLayoutAreas.cs Updates thumbnail image URLs to /api/content.
samples/Graph/Data/Northwind/Article/Source/ArticleLayoutAreas.cs Updates thumbnail image URLs to /api/content.
samples/Graph/Data/Northwind/*.md / *.json Updates space/catalog icons from /static to /api/content.
samples/Graph/Data/MeshWeaver*.md / *.json Updates sample space icons/logos to /api/content.
samples/Graph/Data/FutuRe/** Sweeps sample content to update icon URLs from /static to /api/content.
samples/Graph/Data/Cornerstone/** Updates sample icon URLs from /static to /api/content.
samples/Graph/Data/Brand/Systemorph.json Updates corporate identity logo/icon paths to /api/content.
samples/Graph/Data/ACME/** Updates sample icon URLs and docs to /api/content.
memex/Memex.Portal.Shared/MemexConfiguration.cs Publishes per-node content + attachments + static assets collections on /api/content.
memex/Memex.Portal.Shared/Authentication/OnboardingMiddleware.cs Excludes /api/content from onboarding redirects to avoid broken asset fetches.
memex/Memex.Portal.Shared/Api/MeshApiEndpoints.cs Updates docs/comments to reference /api/content instead of /static.
memex/Memex.Portal.Monolith/Program.cs Ensures mesh-level storage backing store is not published (IsStatic stays false).
memex/Memex.LocalMesh/Program.cs Changes /static endpoint to serve only build-asset mounts (no content collections).
memex/aspire/Memex.Portal.Distributed/Program.cs Ensures distributed storage backing store is not published (IsStatic stays false).
deploy/aks/DEPLOY-RUNBOOK.md Updates rollout verification probe URLs from /static to /api/content.
content/ai/Skill/space.md Updates agent guidance to prefer content: → /api/content for logos; forbids /static for content.
content/ai/Agent/ToolsReference.md Updates guidance to forbid file-path icons (now including /api/content/...).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +47 to +52
// /api/content/{collection}/content/{path} — fold the default-collection segment out so the
// result is the same {collection}/{path} the other two shapes produce.
var rest = path[routePrefix.Length..];
var marker = $"/{ContentCollectionsExtensions.DefaultCollectionName}/";
var at = rest.IndexOf(marker, StringComparison.OrdinalIgnoreCase);
return at < 0 ? rest : rest[..at] + "/" + rest[(at + marker.Length)..];
Comment on lines 1402 to 1407
var address = host.Hub.Address.ToString();
var staticUrl = $"/static/{address}/{ContentCollectionsExtensions.DefaultCollectionName}/{contentPath}";
var staticUrl = ContentCollectionsExtensions.GetContentFileUrl(
address, ContentCollectionsExtensions.DefaultCollectionName, contentPath);

return Observable.Return<UiControl?>(
Controls.Html($"<img src='{staticUrl}' alt='{Path.GetFileName(contentPath)}' style='max-width: 100%;' />"));
Comment on lines 29 to 33
/// <summary>
/// Resolves a node's icon for rendering, handling content: references relative to the node path.
/// E.g., "content:icon.svg" on node "Org/Project" → "/static/storage/content/Org/Project/icon.svg".
/// E.g., "content:icon.svg" on node "Org/Project" → "/api/content/Org/Project/content/icon.svg".
/// When the node carries no icon of its own, falls back to a default icon for its
/// <see cref="MeshNode.NodeType"/> (<see cref="DefaultIconForNodeType"/>) so every node reads as
@github-actions

github-actions Bot commented Aug 7, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 3)

   11 files     11 suites   5m 23s ⏱️
1 879 tests 1 771 ✅ 108 💤 0 ❌
1 973 runs  1 865 ✅ 108 💤 0 ❌

Results for commit aeef09d.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 7, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 4)

1 076 tests   1 071 ✅  6m 24s ⏱️
   11 suites      5 💤
   11 files        0 ❌

Results for commit aeef09d.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 7, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 0)

672 tests   672 ✅  5m 4s ⏱️
 10 suites    0 💤
 10 files      0 ❌

Results for commit aeef09d.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 7, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 1)

1 225 tests   1 225 ✅  7m 4s ⏱️
   11 suites      0 💤
   11 files        0 ❌

Results for commit aeef09d.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 7, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 5)

1 671 tests   1 480 ✅  5m 46s ⏱️
   11 suites    191 💤
   11 files        0 ❌

Results for commit aeef09d.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 7, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 2)

1 726 tests   1 724 ✅  6m 52s ⏱️
   11 suites      2 💤
   11 files        0 ❌

Results for commit aeef09d.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 7, 2026 •

Copy link
Copy Markdown
Contributor

Test Results

   65 files     65 suites   36m 36s ⏱️
8 249 tests 7 943 ✅ 306 💤 0 ❌
8 343 runs  8 037 ✅ 306 💤 0 ❌

Results for commit aeef09d.

♻️ This comment has been updated with latest results.

@rbuergi

rbuergi commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

CI red on a single test, and it is not this change: SpaceMenuAndAccessTest.Space_ChildrenAreQueryable on shard 5 — the recursive-delete deadlock tracked in #899 and fixed by PR #910. Every other shard is green, and the three new classes here (StaticContentUnmountedTest, ContentUrlGenerationTest, ContentReferenceIntegrityTest) pass 40/40 locally against current main.

#899 now blocks essentially every PR in the repo — it has redded #905, #907, #910's own run and this one. This PR should merge once #910 lands, not before; re-running shard 5 here would only re-sample the same deadlock.

🤖 Generated with Claude Code

@rbuergi
rbuergi merged commit e8e720e into main Aug 7, 2026
16 checks passed
@rbuergi
rbuergi deleted the fix/587-static-build-assets-only branch August 19, 2026 10:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Hardening: /static/ endpoint serves content collections without authentication or access control

2 participants