Skip to content

app-frame: fall back to same-origin module import when CSP blocks blob:#109

Closed
miljanm wants to merge 1 commit into
mobius-os:mainfrom
miljanm:fix/app-frame-csp-blob-fallback
Closed

app-frame: fall back to same-origin module import when CSP blocks blob:#109
miljanm wants to merge 1 commit into
mobius-os:mainfrom
miljanm:fix/app-frame-csp-blob-fallback

Conversation

@miljanm

@miljanm miljanm commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Problem

Since the app frame switched to brokered blob module loading, it executes each app's compiled module via URL.createObjectURL + import(blobUrl). That requires the page CSP script-src to include blob:. On a deployment whose edge proxy serves a Content-Security-Policy WITHOUT blob: in script-src — e.g. a shared edge whose policy lags the app-code update (the bundled Caddyfile grants blob:, but a separately-deployed edge may not) — the browser blocks every app module with an opaque error:

TypeError: Failed to fetch dynamically imported module: blob:null/<uuid>

That is a total, cryptic app outage for the whole instance, caused by a version skew between the app code (needs blob:) and the edge security policy (doesn't grant it).

Fix

Make module loading self-healing. importBrokeredModule still tries the secure brokered blob path first; if the blob import() throws, it falls back to importing the compiled module directly from its same-origin /api/apps/{id}/module?token=... URL, which script-src 'self' always permits (the mechanism the frame used before brokered-blob loading). The compiled bundle is self-contained, so no relative/bare sub-imports depend on the blob URL, and a genuine syntax/eval error in the bundle fails identically on the direct path and still reaches the error UI — so the fallback is safe either way.

Result: an app-code vs. edge-CSP skew degrades gracefully (apps still load) instead of taking the instance dark, and self-heals once the edge policy is corrected (the blob path simply stops throwing).

Verification

Reproduced against a live deployment whose edge CSP lacked blob: in script-src: before, every app failed with the blob:null error; after, all apps load via the fallback. Verified two different apps load end-to-end through the real edge proxy.

Trade-off: the fallback passes the app token as a query param (dynamic import() cannot set headers) — the same compromise the pre-brokered loader made, and only on the degraded path.

A deployment whose edge proxy serves a script-src without blob: (an app-code vs edge-CSP version skew) blocked every app with an opaque 'Failed to fetch dynamically imported module: blob:null/...' error. The loader now falls back to a direct same-origin /module import (the mechanism used before brokered-blob loading), which script-src 'self' always permits, so app loading self-heals across that skew instead of a total outage.

Co-authored-by: Möbius Agent <mobius-agent@users.noreply.github.com>

@hamzamerzic hamzamerzic left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for chasing a real availability failure. I do not think this is safe to merge on the current loader contract yet.

The new catch is inside importBrokeredModule, so it catches every failure from both requestModuleBytes() and import(blobUrl), not only a CSP rejection. That means token-expiry, broker/network/HTTP failure, and a genuine top-level module evaluation error all fall through to a direct import(). In the evaluation-error case the module is executed twice, duplicating any top-level side effects. Current main deliberately added isBlobModuleLoadFailure() and retries only the narrow transport-shaped TypeError so app errors are never re-evaluated.

The direct path also bypasses the exact-parent broker contract (typed auth/HTTP errors, 8 MiB bound, source/version attribution and controlled service-worker/offline path), omits the module v= key, and embeds the bearer token in the imported URL where a stack/error report may retain it. The existing CI does not exercise this fallback; the current static assertion only forbids the older import(moduleUrl(...)) spelling and therefore misses the renamed helper.

Please rebase onto current main and keep any fallback outside importBrokeredModule, after the precise blob-failure classifier and bounded retry, with tests proving: token-expired still renegotiates; network/HTTP/app-evaluation failures never direct-import or execute twice; the broker size/source/version gates stay intact; and the exact CSP-skew path works without leaking the token into error telemetry. My preference is to correct/verify blob: on the separately deployed edge, because that is the owning policy layer and preserves offline behavior.

@hamzamerzic

Copy link
Copy Markdown
Collaborator

Closing after the second production reconciliation review. The same-origin fallback can bypass the exact-parent broker for unrelated failures and may execute top-level app code twice; that weakens the authority/version/offline invariant while masking the actual edge-CSP migration. Issue #114 remains the scoped place to design and verify that migration. PR #134 documents and tests the broker invariant but is itself still under review.

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.

2 participants