fix(playground): inject in-memory localStorage shim — fixes SecurityError in sandboxed iframe - #36

Merged
joaodibba merged 1 commit into
TypeComposer:mainfrom
lucas-spin:fix/playground-localstorage-sandbox-shim
Jun 12, 2026
Merged

fix(playground): inject in-memory localStorage shim — fixes SecurityError in sandboxed iframe#36
joaodibba merged 1 commit into
TypeComposer:mainfrom
lucas-spin:fix/playground-localstorage-sandbox-shim

Conversation

@lucas-spin

Copy link
Copy Markdown
Contributor

Bug reported by João (post-PR #35)

[Playground] Initialization failed: SecurityError: Failed to read the
'localStorage' property from 'Window': The document is sandboxed and
lacks the 'allow-same-origin' flag.
Runtime Error: SecurityError: Failed to read the 'localStorage' property
from 'Window': The document is sandboxed and lacks the 'allow-same-origin' flag.

Root cause

PR #34 added sandbox="allow-scripts" to the preview <iframe> (correct security posture). The browser enforces that a sandboxed document cannot access window.localStorage unless allow-same-origin is also present in the sandbox attribute. TypeComposer's runtime (loaded from jsDelivr, post-PR #35) probes localStorage during initialization, which throws a SecurityError inside the iframe.

Why allow-same-origin is NOT the fix

The iframe is loaded via a blob: URL (see injectCode()). Blob URLs always carry an opaque origin — they are never same-origin with the parent page. Adding allow-same-origin to a blob-loaded iframe:

  • Would not grant access to the parent page's localStorage (opaque origin = no same-origin relationship).
  • Would, however, weaken the sandbox signal and confuse future readers into thinking the iframe is relaxed.
  • Adds no benefit since there is no same-origin relationship to exploit.

Fix — in-memory localStorage shim

Inject a tiny IIFE shim as the first <script> tag in <head> (before the importmap), so window.localStorage and window.sessionStorage are always available when TypeComposer imports.

Strategy:

  1. try { void window.localStorage } — probe real storage.
  2. If it throws (SecurityError) → install a Map-backed localStorage-shaped object via Object.defineProperty (or direct assignment as fallback).
  3. The shim is activation-only — a no-op in environments where real localStorage works.
  4. State is ephemeral (iframe lifetime only). This is correct for a preview pane.
<script>(function(){try{voidwindow.localStorage;// probe — succeeds → skip shim}catch(_e){var_store=Object.create(null);// ... Map-backed getItem / setItem / removeItem / clear / key / lengthObject.defineProperty(window,'localStorage',{value: _shimStorage,writable: false,configurable: true});Object.defineProperty(window,'sessionStorage',{value: _shimStorage,writable: false,configurable: true});}})();</script>

Security analysis

ApproachIsolation preservedFixes errorNotes
Add allow-same-origin⚠️ Weakens sandboxWrong: opaque origin means no real-storage access AND weakens sandbox
In-memory shim (this PR)✅ No change to sandboxSafest: no attribute change, opaque origin preserved, storage scoped to iframe

The sandbox="allow-scripts" attribute is unchanged. iframe isolation is identical to PR #34.


Files changed

FileChange
src/views/playground/PlaygroundView.tsShim <script> injected before importmap in createIframeHTML(); sandbox comment and JSDoc updated

Build result

✓ 3448 modules transformed.
dist/assets/esbuild-BHljloGq.wasm 12,332.68 kB
dist/assets/index-C2_sis7Q.css 48.96 kB
dist/assets/index-BzPRh-vd.js 1,756.71 kB
✓ built in 14.92s

Verified in dist:

  • In-memory localStorage shim comment present in bundle
  • SecurityError shim guard string present
  • esm.sh not in bundle
  • ✅ jsDelivr URL for typecomposer present
  • allow-same-origin appears only inside a comment string (not as attribute)

Manual browser verification still needed

  1. Open deployed docs → Playground tab
  2. DevTools Console: no SecurityError on load
  3. DevTools Network: no esm.sh requests
  4. Playground renders the demo AppPage (purple gradient + click button works)

cc @zico15@joaodibba

…ror in sandbox
sandbox="allow-scripts" (added in PR TypeComposer#34) blocks real localStorage access
inside the iframe, causing TypeComposer's runtime to throw:
SecurityError: Failed to read the 'localStorage' property from 'Window':
The document is sandboxed and lacks the 'allow-same-origin' flag.
Adding allow-same-origin is not the right fix:
- The iframe loads a blob: URL, which always has an opaque origin, so
allow-same-origin cannot grant access to the parent page's storage anyway.
- It would also weaken the sandbox comment/intent and confuse future readers.
The correct fix is to shim window.localStorage and window.sessionStorage with
a lightweight in-memory Map-backed implementation that is installed before any
module code runs (as the first <script> tag in <head>, before the importmap).
The shim only activates when a try/catch probe detects that real localStorage
throws a SecurityError — no-op if the browser ever allows it.
The shim is deliberately non-persistent (lifetime = iframe document). Any
TypeComposer code that probes storage for feature detection or theme prefs
will work correctly; actual persistence is not meaningful inside a preview
iframe anyway.
Files changed:
src/views/playground/PlaygroundView.ts — shim <script> injected before
importmap; sandbox attribute comment and JSDoc updated.
Reported by: João (post-merge of PR TypeComposer#35)
@joaodibba
joaodibba merged commit 453f156 into TypeComposer:mainJun 12, 2026
1 check failed
Sign up for freeto 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

@lucas-spin@joaodibba
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(playground): inject in-memory localStorage shim — fixes SecurityError in sandboxed iframe - #36

Merged
joaodibba merged 1 commit into
TypeComposer:mainfrom
lucas-spin:fix/playground-localstorage-sandbox-shim
Jun 12, 2026
Merged

fix(playground): inject in-memory localStorage shim — fixes SecurityError in sandboxed iframe#36
joaodibba merged 1 commit into
TypeComposer:mainfrom
lucas-spin:fix/playground-localstorage-sandbox-shim

Conversation

@lucas-spin

Copy link
Copy Markdown
Contributor

Bug reported by João (post-PR #35)

[Playground] Initialization failed: SecurityError: Failed to read the
'localStorage' property from 'Window': The document is sandboxed and
lacks the 'allow-same-origin' flag.
Runtime Error: SecurityError: Failed to read the 'localStorage' property
from 'Window': The document is sandboxed and lacks the 'allow-same-origin' flag.

Root cause

PR #34 added sandbox="allow-scripts" to the preview <iframe> (correct security posture). The browser enforces that a sandboxed document cannot access window.localStorage unless allow-same-origin is also present in the sandbox attribute. TypeComposer's runtime (loaded from jsDelivr, post-PR #35) probes localStorage during initialization, which throws a SecurityError inside the iframe.

Why allow-same-origin is NOT the fix

The iframe is loaded via a blob: URL (see injectCode()). Blob URLs always carry an opaque origin — they are never same-origin with the parent page. Adding allow-same-origin to a blob-loaded iframe:

  • Would not grant access to the parent page's localStorage (opaque origin = no same-origin relationship).
  • Would, however, weaken the sandbox signal and confuse future readers into thinking the iframe is relaxed.
  • Adds no benefit since there is no same-origin relationship to exploit.

Fix — in-memory localStorage shim

Inject a tiny IIFE shim as the first <script> tag in <head> (before the importmap), so window.localStorage and window.sessionStorage are always available when TypeComposer imports.

Strategy:

  1. try { void window.localStorage } — probe real storage.
  2. If it throws (SecurityError) → install a Map-backed localStorage-shaped object via Object.defineProperty (or direct assignment as fallback).
  3. The shim is activation-only — a no-op in environments where real localStorage works.
  4. State is ephemeral (iframe lifetime only). This is correct for a preview pane.
<script>(function(){try{voidwindow.localStorage;// probe — succeeds → skip shim}catch(_e){var_store=Object.create(null);// ... Map-backed getItem / setItem / removeItem / clear / key / lengthObject.defineProperty(window,'localStorage',{value: _shimStorage,writable: false,configurable: true});Object.defineProperty(window,'sessionStorage',{value: _shimStorage,writable: false,configurable: true});}})();</script>

Security analysis

ApproachIsolation preservedFixes errorNotes
Add allow-same-origin⚠️ Weakens sandboxWrong: opaque origin means no real-storage access AND weakens sandbox
In-memory shim (this PR)✅ No change to sandboxSafest: no attribute change, opaque origin preserved, storage scoped to iframe

The sandbox="allow-scripts" attribute is unchanged. iframe isolation is identical to PR #34.


Files changed

FileChange
src/views/playground/PlaygroundView.tsShim <script> injected before importmap in createIframeHTML(); sandbox comment and JSDoc updated

Build result

✓ 3448 modules transformed.
dist/assets/esbuild-BHljloGq.wasm 12,332.68 kB
dist/assets/index-C2_sis7Q.css 48.96 kB
dist/assets/index-BzPRh-vd.js 1,756.71 kB
✓ built in 14.92s

Verified in dist:

  • In-memory localStorage shim comment present in bundle
  • SecurityError shim guard string present
  • esm.sh not in bundle
  • ✅ jsDelivr URL for typecomposer present
  • allow-same-origin appears only inside a comment string (not as attribute)

Manual browser verification still needed

  1. Open deployed docs → Playground tab
  2. DevTools Console: no SecurityError on load
  3. DevTools Network: no esm.sh requests
  4. Playground renders the demo AppPage (purple gradient + click button works)

cc @zico15@joaodibba

…ror in sandbox
sandbox="allow-scripts" (added in PR TypeComposer#34) blocks real localStorage access
inside the iframe, causing TypeComposer's runtime to throw:
SecurityError: Failed to read the 'localStorage' property from 'Window':
The document is sandboxed and lacks the 'allow-same-origin' flag.
Adding allow-same-origin is not the right fix:
- The iframe loads a blob: URL, which always has an opaque origin, so
allow-same-origin cannot grant access to the parent page's storage anyway.
- It would also weaken the sandbox comment/intent and confuse future readers.
The correct fix is to shim window.localStorage and window.sessionStorage with
a lightweight in-memory Map-backed implementation that is installed before any
module code runs (as the first <script> tag in <head>, before the importmap).
The shim only activates when a try/catch probe detects that real localStorage
throws a SecurityError — no-op if the browser ever allows it.
The shim is deliberately non-persistent (lifetime = iframe document). Any
TypeComposer code that probes storage for feature detection or theme prefs
will work correctly; actual persistence is not meaningful inside a preview
iframe anyway.
Files changed:
src/views/playground/PlaygroundView.ts — shim <script> injected before
importmap; sandbox attribute comment and JSDoc updated.
Reported by: João (post-merge of PR TypeComposer#35)
@joaodibba
joaodibba merged commit 453f156 into TypeComposer:mainJun 12, 2026
1 check failed
Sign up for freeto 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

@lucas-spin@joaodibba
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(playground): inject in-memory localStorage shim — fixes SecurityError in sandboxed iframe - #36

Merged
joaodibba merged 1 commit into
TypeComposer:mainfrom
lucas-spin:fix/playground-localstorage-sandbox-shim
Jun 12, 2026
Merged

fix(playground): inject in-memory localStorage shim — fixes SecurityError in sandboxed iframe#36
joaodibba merged 1 commit into
TypeComposer:mainfrom
lucas-spin:fix/playground-localstorage-sandbox-shim

Conversation

@lucas-spin

Copy link
Copy Markdown
Contributor

Bug reported by João (post-PR #35)

[Playground] Initialization failed: SecurityError: Failed to read the
'localStorage' property from 'Window': The document is sandboxed and
lacks the 'allow-same-origin' flag.
Runtime Error: SecurityError: Failed to read the 'localStorage' property
from 'Window': The document is sandboxed and lacks the 'allow-same-origin' flag.

Root cause

PR #34 added sandbox="allow-scripts" to the preview <iframe> (correct security posture). The browser enforces that a sandboxed document cannot access window.localStorage unless allow-same-origin is also present in the sandbox attribute. TypeComposer's runtime (loaded from jsDelivr, post-PR #35) probes localStorage during initialization, which throws a SecurityError inside the iframe.

Why allow-same-origin is NOT the fix

The iframe is loaded via a blob: URL (see injectCode()). Blob URLs always carry an opaque origin — they are never same-origin with the parent page. Adding allow-same-origin to a blob-loaded iframe:

  • Would not grant access to the parent page's localStorage (opaque origin = no same-origin relationship).
  • Would, however, weaken the sandbox signal and confuse future readers into thinking the iframe is relaxed.
  • Adds no benefit since there is no same-origin relationship to exploit.

Fix — in-memory localStorage shim

Inject a tiny IIFE shim as the first <script> tag in <head> (before the importmap), so window.localStorage and window.sessionStorage are always available when TypeComposer imports.

Strategy:

  1. try { void window.localStorage } — probe real storage.
  2. If it throws (SecurityError) → install a Map-backed localStorage-shaped object via Object.defineProperty (or direct assignment as fallback).
  3. The shim is activation-only — a no-op in environments where real localStorage works.
  4. State is ephemeral (iframe lifetime only). This is correct for a preview pane.
<script>(function(){try{voidwindow.localStorage;// probe — succeeds → skip shim}catch(_e){var_store=Object.create(null);// ... Map-backed getItem / setItem / removeItem / clear / key / lengthObject.defineProperty(window,'localStorage',{value: _shimStorage,writable: false,configurable: true});Object.defineProperty(window,'sessionStorage',{value: _shimStorage,writable: false,configurable: true});}})();</script>

Security analysis

ApproachIsolation preservedFixes errorNotes
Add allow-same-origin⚠️ Weakens sandboxWrong: opaque origin means no real-storage access AND weakens sandbox
In-memory shim (this PR)✅ No change to sandboxSafest: no attribute change, opaque origin preserved, storage scoped to iframe

The sandbox="allow-scripts" attribute is unchanged. iframe isolation is identical to PR #34.


Files changed

FileChange
src/views/playground/PlaygroundView.tsShim <script> injected before importmap in createIframeHTML(); sandbox comment and JSDoc updated

Build result

✓ 3448 modules transformed.
dist/assets/esbuild-BHljloGq.wasm 12,332.68 kB
dist/assets/index-C2_sis7Q.css 48.96 kB
dist/assets/index-BzPRh-vd.js 1,756.71 kB
✓ built in 14.92s

Verified in dist:

  • In-memory localStorage shim comment present in bundle
  • SecurityError shim guard string present
  • esm.sh not in bundle
  • ✅ jsDelivr URL for typecomposer present
  • allow-same-origin appears only inside a comment string (not as attribute)

Manual browser verification still needed

  1. Open deployed docs → Playground tab
  2. DevTools Console: no SecurityError on load
  3. DevTools Network: no esm.sh requests
  4. Playground renders the demo AppPage (purple gradient + click button works)

cc @zico15@joaodibba

…ror in sandbox
sandbox="allow-scripts" (added in PR TypeComposer#34) blocks real localStorage access
inside the iframe, causing TypeComposer's runtime to throw:
SecurityError: Failed to read the 'localStorage' property from 'Window':
The document is sandboxed and lacks the 'allow-same-origin' flag.
Adding allow-same-origin is not the right fix:
- The iframe loads a blob: URL, which always has an opaque origin, so
allow-same-origin cannot grant access to the parent page's storage anyway.
- It would also weaken the sandbox comment/intent and confuse future readers.
The correct fix is to shim window.localStorage and window.sessionStorage with
a lightweight in-memory Map-backed implementation that is installed before any
module code runs (as the first <script> tag in <head>, before the importmap).
The shim only activates when a try/catch probe detects that real localStorage
throws a SecurityError — no-op if the browser ever allows it.
The shim is deliberately non-persistent (lifetime = iframe document). Any
TypeComposer code that probes storage for feature detection or theme prefs
will work correctly; actual persistence is not meaningful inside a preview
iframe anyway.
Files changed:
src/views/playground/PlaygroundView.ts — shim <script> injected before
importmap; sandbox attribute comment and JSDoc updated.
Reported by: João (post-merge of PR TypeComposer#35)
@joaodibba
joaodibba merged commit 453f156 into TypeComposer:mainJun 12, 2026
1 check failed
Sign up for freeto 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

@lucas-spin@joaodibba
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(playground): inject in-memory localStorage shim — fixes SecurityError in sandboxed iframe - #36

Merged
joaodibba merged 1 commit into
TypeComposer:mainfrom
lucas-spin:fix/playground-localstorage-sandbox-shim
Jun 12, 2026
Merged

fix(playground): inject in-memory localStorage shim — fixes SecurityError in sandboxed iframe#36
joaodibba merged 1 commit into
TypeComposer:mainfrom
lucas-spin:fix/playground-localstorage-sandbox-shim

Conversation

@lucas-spin

Copy link
Copy Markdown
Contributor

Bug reported by João (post-PR #35)

[Playground] Initialization failed: SecurityError: Failed to read the
'localStorage' property from 'Window': The document is sandboxed and
lacks the 'allow-same-origin' flag.
Runtime Error: SecurityError: Failed to read the 'localStorage' property
from 'Window': The document is sandboxed and lacks the 'allow-same-origin' flag.

Root cause

PR #34 added sandbox="allow-scripts" to the preview <iframe> (correct security posture). The browser enforces that a sandboxed document cannot access window.localStorage unless allow-same-origin is also present in the sandbox attribute. TypeComposer's runtime (loaded from jsDelivr, post-PR #35) probes localStorage during initialization, which throws a SecurityError inside the iframe.

Why allow-same-origin is NOT the fix

The iframe is loaded via a blob: URL (see injectCode()). Blob URLs always carry an opaque origin — they are never same-origin with the parent page. Adding allow-same-origin to a blob-loaded iframe:

  • Would not grant access to the parent page's localStorage (opaque origin = no same-origin relationship).
  • Would, however, weaken the sandbox signal and confuse future readers into thinking the iframe is relaxed.
  • Adds no benefit since there is no same-origin relationship to exploit.

Fix — in-memory localStorage shim

Inject a tiny IIFE shim as the first <script> tag in <head> (before the importmap), so window.localStorage and window.sessionStorage are always available when TypeComposer imports.

Strategy:

  1. try { void window.localStorage } — probe real storage.
  2. If it throws (SecurityError) → install a Map-backed localStorage-shaped object via Object.defineProperty (or direct assignment as fallback).
  3. The shim is activation-only — a no-op in environments where real localStorage works.
  4. State is ephemeral (iframe lifetime only). This is correct for a preview pane.
<script>(function(){try{voidwindow.localStorage;// probe — succeeds → skip shim}catch(_e){var_store=Object.create(null);// ... Map-backed getItem / setItem / removeItem / clear / key / lengthObject.defineProperty(window,'localStorage',{value: _shimStorage,writable: false,configurable: true});Object.defineProperty(window,'sessionStorage',{value: _shimStorage,writable: false,configurable: true});}})();</script>

Security analysis

ApproachIsolation preservedFixes errorNotes
Add allow-same-origin⚠️ Weakens sandboxWrong: opaque origin means no real-storage access AND weakens sandbox
In-memory shim (this PR)✅ No change to sandboxSafest: no attribute change, opaque origin preserved, storage scoped to iframe

The sandbox="allow-scripts" attribute is unchanged. iframe isolation is identical to PR #34.


Files changed

FileChange
src/views/playground/PlaygroundView.tsShim <script> injected before importmap in createIframeHTML(); sandbox comment and JSDoc updated

Build result

✓ 3448 modules transformed.
dist/assets/esbuild-BHljloGq.wasm 12,332.68 kB
dist/assets/index-C2_sis7Q.css 48.96 kB
dist/assets/index-BzPRh-vd.js 1,756.71 kB
✓ built in 14.92s

Verified in dist:

  • In-memory localStorage shim comment present in bundle
  • SecurityError shim guard string present
  • esm.sh not in bundle
  • ✅ jsDelivr URL for typecomposer present
  • allow-same-origin appears only inside a comment string (not as attribute)

Manual browser verification still needed

  1. Open deployed docs → Playground tab
  2. DevTools Console: no SecurityError on load
  3. DevTools Network: no esm.sh requests
  4. Playground renders the demo AppPage (purple gradient + click button works)

cc @zico15@joaodibba

…ror in sandbox
sandbox="allow-scripts" (added in PR TypeComposer#34) blocks real localStorage access
inside the iframe, causing TypeComposer's runtime to throw:
SecurityError: Failed to read the 'localStorage' property from 'Window':
The document is sandboxed and lacks the 'allow-same-origin' flag.
Adding allow-same-origin is not the right fix:
- The iframe loads a blob: URL, which always has an opaque origin, so
allow-same-origin cannot grant access to the parent page's storage anyway.
- It would also weaken the sandbox comment/intent and confuse future readers.
The correct fix is to shim window.localStorage and window.sessionStorage with
a lightweight in-memory Map-backed implementation that is installed before any
module code runs (as the first <script> tag in <head>, before the importmap).
The shim only activates when a try/catch probe detects that real localStorage
throws a SecurityError — no-op if the browser ever allows it.
The shim is deliberately non-persistent (lifetime = iframe document). Any
TypeComposer code that probes storage for feature detection or theme prefs
will work correctly; actual persistence is not meaningful inside a preview
iframe anyway.
Files changed:
src/views/playground/PlaygroundView.ts — shim <script> injected before
importmap; sandbox attribute comment and JSDoc updated.
Reported by: João (post-merge of PR TypeComposer#35)
@joaodibba
joaodibba merged commit 453f156 into TypeComposer:mainJun 12, 2026
1 check failed
Sign up for freeto 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

@lucas-spin@joaodibba
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(playground): inject in-memory localStorage shim — fixes SecurityError in sandboxed iframe - #36

Merged
joaodibba merged 1 commit into
TypeComposer:mainfrom
lucas-spin:fix/playground-localstorage-sandbox-shim
Jun 12, 2026
Merged

fix(playground): inject in-memory localStorage shim — fixes SecurityError in sandboxed iframe#36
joaodibba merged 1 commit into
TypeComposer:mainfrom
lucas-spin:fix/playground-localstorage-sandbox-shim

Conversation

@lucas-spin

Copy link
Copy Markdown
Contributor

Bug reported by João (post-PR #35)

[Playground] Initialization failed: SecurityError: Failed to read the
'localStorage' property from 'Window': The document is sandboxed and
lacks the 'allow-same-origin' flag.
Runtime Error: SecurityError: Failed to read the 'localStorage' property
from 'Window': The document is sandboxed and lacks the 'allow-same-origin' flag.

Root cause

PR #34 added sandbox="allow-scripts" to the preview <iframe> (correct security posture). The browser enforces that a sandboxed document cannot access window.localStorage unless allow-same-origin is also present in the sandbox attribute. TypeComposer's runtime (loaded from jsDelivr, post-PR #35) probes localStorage during initialization, which throws a SecurityError inside the iframe.

Why allow-same-origin is NOT the fix

The iframe is loaded via a blob: URL (see injectCode()). Blob URLs always carry an opaque origin — they are never same-origin with the parent page. Adding allow-same-origin to a blob-loaded iframe:

  • Would not grant access to the parent page's localStorage (opaque origin = no same-origin relationship).
  • Would, however, weaken the sandbox signal and confuse future readers into thinking the iframe is relaxed.
  • Adds no benefit since there is no same-origin relationship to exploit.

Fix — in-memory localStorage shim

Inject a tiny IIFE shim as the first <script> tag in <head> (before the importmap), so window.localStorage and window.sessionStorage are always available when TypeComposer imports.

Strategy:

  1. try { void window.localStorage } — probe real storage.
  2. If it throws (SecurityError) → install a Map-backed localStorage-shaped object via Object.defineProperty (or direct assignment as fallback).
  3. The shim is activation-only — a no-op in environments where real localStorage works.
  4. State is ephemeral (iframe lifetime only). This is correct for a preview pane.
<script>(function(){try{voidwindow.localStorage;// probe — succeeds → skip shim}catch(_e){var_store=Object.create(null);// ... Map-backed getItem / setItem / removeItem / clear / key / lengthObject.defineProperty(window,'localStorage',{value: _shimStorage,writable: false,configurable: true});Object.defineProperty(window,'sessionStorage',{value: _shimStorage,writable: false,configurable: true});}})();</script>

Security analysis

ApproachIsolation preservedFixes errorNotes
Add allow-same-origin⚠️ Weakens sandboxWrong: opaque origin means no real-storage access AND weakens sandbox
In-memory shim (this PR)✅ No change to sandboxSafest: no attribute change, opaque origin preserved, storage scoped to iframe

The sandbox="allow-scripts" attribute is unchanged. iframe isolation is identical to PR #34.


Files changed

FileChange
src/views/playground/PlaygroundView.tsShim <script> injected before importmap in createIframeHTML(); sandbox comment and JSDoc updated

Build result

✓ 3448 modules transformed.
dist/assets/esbuild-BHljloGq.wasm 12,332.68 kB
dist/assets/index-C2_sis7Q.css 48.96 kB
dist/assets/index-BzPRh-vd.js 1,756.71 kB
✓ built in 14.92s

Verified in dist:

  • In-memory localStorage shim comment present in bundle
  • SecurityError shim guard string present
  • esm.sh not in bundle
  • ✅ jsDelivr URL for typecomposer present
  • allow-same-origin appears only inside a comment string (not as attribute)

Manual browser verification still needed

  1. Open deployed docs → Playground tab
  2. DevTools Console: no SecurityError on load
  3. DevTools Network: no esm.sh requests
  4. Playground renders the demo AppPage (purple gradient + click button works)

cc @zico15@joaodibba

…ror in sandbox
sandbox="allow-scripts" (added in PR TypeComposer#34) blocks real localStorage access
inside the iframe, causing TypeComposer's runtime to throw:
SecurityError: Failed to read the 'localStorage' property from 'Window':
The document is sandboxed and lacks the 'allow-same-origin' flag.
Adding allow-same-origin is not the right fix:
- The iframe loads a blob: URL, which always has an opaque origin, so
allow-same-origin cannot grant access to the parent page's storage anyway.
- It would also weaken the sandbox comment/intent and confuse future readers.
The correct fix is to shim window.localStorage and window.sessionStorage with
a lightweight in-memory Map-backed implementation that is installed before any
module code runs (as the first <script> tag in <head>, before the importmap).
The shim only activates when a try/catch probe detects that real localStorage
throws a SecurityError — no-op if the browser ever allows it.
The shim is deliberately non-persistent (lifetime = iframe document). Any
TypeComposer code that probes storage for feature detection or theme prefs
will work correctly; actual persistence is not meaningful inside a preview
iframe anyway.
Files changed:
src/views/playground/PlaygroundView.ts — shim <script> injected before
importmap; sandbox attribute comment and JSDoc updated.
Reported by: João (post-merge of PR TypeComposer#35)
@joaodibba
joaodibba merged commit 453f156 into TypeComposer:mainJun 12, 2026
1 check failed
Sign up for freeto 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

@lucas-spin@joaodibba
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(playground): inject in-memory localStorage shim — fixes SecurityError in sandboxed iframe - #36

Merged
joaodibba merged 1 commit into
TypeComposer:mainfrom
lucas-spin:fix/playground-localstorage-sandbox-shim
Jun 12, 2026
Merged

fix(playground): inject in-memory localStorage shim — fixes SecurityError in sandboxed iframe#36
joaodibba merged 1 commit into
TypeComposer:mainfrom
lucas-spin:fix/playground-localstorage-sandbox-shim

Conversation

@lucas-spin

Copy link
Copy Markdown
Contributor

Bug reported by João (post-PR #35)

[Playground] Initialization failed: SecurityError: Failed to read the
'localStorage' property from 'Window': The document is sandboxed and
lacks the 'allow-same-origin' flag.
Runtime Error: SecurityError: Failed to read the 'localStorage' property
from 'Window': The document is sandboxed and lacks the 'allow-same-origin' flag.

Root cause

PR #34 added sandbox="allow-scripts" to the preview <iframe> (correct security posture). The browser enforces that a sandboxed document cannot access window.localStorage unless allow-same-origin is also present in the sandbox attribute. TypeComposer's runtime (loaded from jsDelivr, post-PR #35) probes localStorage during initialization, which throws a SecurityError inside the iframe.

Why allow-same-origin is NOT the fix

The iframe is loaded via a blob: URL (see injectCode()). Blob URLs always carry an opaque origin — they are never same-origin with the parent page. Adding allow-same-origin to a blob-loaded iframe:

  • Would not grant access to the parent page's localStorage (opaque origin = no same-origin relationship).
  • Would, however, weaken the sandbox signal and confuse future readers into thinking the iframe is relaxed.
  • Adds no benefit since there is no same-origin relationship to exploit.

Fix — in-memory localStorage shim

Inject a tiny IIFE shim as the first <script> tag in <head> (before the importmap), so window.localStorage and window.sessionStorage are always available when TypeComposer imports.

Strategy:

  1. try { void window.localStorage } — probe real storage.
  2. If it throws (SecurityError) → install a Map-backed localStorage-shaped object via Object.defineProperty (or direct assignment as fallback).
  3. The shim is activation-only — a no-op in environments where real localStorage works.
  4. State is ephemeral (iframe lifetime only). This is correct for a preview pane.
<script>(function(){try{voidwindow.localStorage;// probe — succeeds → skip shim}catch(_e){var_store=Object.create(null);// ... Map-backed getItem / setItem / removeItem / clear / key / lengthObject.defineProperty(window,'localStorage',{value: _shimStorage,writable: false,configurable: true});Object.defineProperty(window,'sessionStorage',{value: _shimStorage,writable: false,configurable: true});}})();</script>

Security analysis

ApproachIsolation preservedFixes errorNotes
Add allow-same-origin⚠️ Weakens sandboxWrong: opaque origin means no real-storage access AND weakens sandbox
In-memory shim (this PR)✅ No change to sandboxSafest: no attribute change, opaque origin preserved, storage scoped to iframe

The sandbox="allow-scripts" attribute is unchanged. iframe isolation is identical to PR #34.


Files changed

FileChange
src/views/playground/PlaygroundView.tsShim <script> injected before importmap in createIframeHTML(); sandbox comment and JSDoc updated

Build result

✓ 3448 modules transformed.
dist/assets/esbuild-BHljloGq.wasm 12,332.68 kB
dist/assets/index-C2_sis7Q.css 48.96 kB
dist/assets/index-BzPRh-vd.js 1,756.71 kB
✓ built in 14.92s

Verified in dist:

  • In-memory localStorage shim comment present in bundle
  • SecurityError shim guard string present
  • esm.sh not in bundle
  • ✅ jsDelivr URL for typecomposer present
  • allow-same-origin appears only inside a comment string (not as attribute)

Manual browser verification still needed

  1. Open deployed docs → Playground tab
  2. DevTools Console: no SecurityError on load
  3. DevTools Network: no esm.sh requests
  4. Playground renders the demo AppPage (purple gradient + click button works)

cc @zico15@joaodibba

…ror in sandbox
sandbox="allow-scripts" (added in PR TypeComposer#34) blocks real localStorage access
inside the iframe, causing TypeComposer's runtime to throw:
SecurityError: Failed to read the 'localStorage' property from 'Window':
The document is sandboxed and lacks the 'allow-same-origin' flag.
Adding allow-same-origin is not the right fix:
- The iframe loads a blob: URL, which always has an opaque origin, so
allow-same-origin cannot grant access to the parent page's storage anyway.
- It would also weaken the sandbox comment/intent and confuse future readers.
The correct fix is to shim window.localStorage and window.sessionStorage with
a lightweight in-memory Map-backed implementation that is installed before any
module code runs (as the first <script> tag in <head>, before the importmap).
The shim only activates when a try/catch probe detects that real localStorage
throws a SecurityError — no-op if the browser ever allows it.
The shim is deliberately non-persistent (lifetime = iframe document). Any
TypeComposer code that probes storage for feature detection or theme prefs
will work correctly; actual persistence is not meaningful inside a preview
iframe anyway.
Files changed:
src/views/playground/PlaygroundView.ts — shim <script> injected before
importmap; sandbox attribute comment and JSDoc updated.
Reported by: João (post-merge of PR TypeComposer#35)
@joaodibba
joaodibba merged commit 453f156 into TypeComposer:mainJun 12, 2026
1 check failed
Sign up for freeto 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

@lucas-spin@joaodibba
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(playground): inject in-memory localStorage shim — fixes SecurityError in sandboxed iframe - #36

Merged
joaodibba merged 1 commit into
TypeComposer:mainfrom
lucas-spin:fix/playground-localstorage-sandbox-shim
Jun 12, 2026
Merged

fix(playground): inject in-memory localStorage shim — fixes SecurityError in sandboxed iframe#36
joaodibba merged 1 commit into
TypeComposer:mainfrom
lucas-spin:fix/playground-localstorage-sandbox-shim

Conversation

@lucas-spin

Copy link
Copy Markdown
Contributor

Bug reported by João (post-PR #35)

[Playground] Initialization failed: SecurityError: Failed to read the
'localStorage' property from 'Window': The document is sandboxed and
lacks the 'allow-same-origin' flag.
Runtime Error: SecurityError: Failed to read the 'localStorage' property
from 'Window': The document is sandboxed and lacks the 'allow-same-origin' flag.

Root cause

PR #34 added sandbox="allow-scripts" to the preview <iframe> (correct security posture). The browser enforces that a sandboxed document cannot access window.localStorage unless allow-same-origin is also present in the sandbox attribute. TypeComposer's runtime (loaded from jsDelivr, post-PR #35) probes localStorage during initialization, which throws a SecurityError inside the iframe.

Why allow-same-origin is NOT the fix

The iframe is loaded via a blob: URL (see injectCode()). Blob URLs always carry an opaque origin — they are never same-origin with the parent page. Adding allow-same-origin to a blob-loaded iframe:

  • Would not grant access to the parent page's localStorage (opaque origin = no same-origin relationship).
  • Would, however, weaken the sandbox signal and confuse future readers into thinking the iframe is relaxed.
  • Adds no benefit since there is no same-origin relationship to exploit.

Fix — in-memory localStorage shim

Inject a tiny IIFE shim as the first <script> tag in <head> (before the importmap), so window.localStorage and window.sessionStorage are always available when TypeComposer imports.

Strategy:

  1. try { void window.localStorage } — probe real storage.
  2. If it throws (SecurityError) → install a Map-backed localStorage-shaped object via Object.defineProperty (or direct assignment as fallback).
  3. The shim is activation-only — a no-op in environments where real localStorage works.
  4. State is ephemeral (iframe lifetime only). This is correct for a preview pane.
<script>(function(){try{voidwindow.localStorage;// probe — succeeds → skip shim}catch(_e){var_store=Object.create(null);// ... Map-backed getItem / setItem / removeItem / clear / key / lengthObject.defineProperty(window,'localStorage',{value: _shimStorage,writable: false,configurable: true});Object.defineProperty(window,'sessionStorage',{value: _shimStorage,writable: false,configurable: true});}})();</script>

Security analysis

ApproachIsolation preservedFixes errorNotes
Add allow-same-origin⚠️ Weakens sandboxWrong: opaque origin means no real-storage access AND weakens sandbox
In-memory shim (this PR)✅ No change to sandboxSafest: no attribute change, opaque origin preserved, storage scoped to iframe

The sandbox="allow-scripts" attribute is unchanged. iframe isolation is identical to PR #34.


Files changed

FileChange
src/views/playground/PlaygroundView.tsShim <script> injected before importmap in createIframeHTML(); sandbox comment and JSDoc updated

Build result

✓ 3448 modules transformed.
dist/assets/esbuild-BHljloGq.wasm 12,332.68 kB
dist/assets/index-C2_sis7Q.css 48.96 kB
dist/assets/index-BzPRh-vd.js 1,756.71 kB
✓ built in 14.92s

Verified in dist:

  • In-memory localStorage shim comment present in bundle
  • SecurityError shim guard string present
  • esm.sh not in bundle
  • ✅ jsDelivr URL for typecomposer present
  • allow-same-origin appears only inside a comment string (not as attribute)

Manual browser verification still needed

  1. Open deployed docs → Playground tab
  2. DevTools Console: no SecurityError on load
  3. DevTools Network: no esm.sh requests
  4. Playground renders the demo AppPage (purple gradient + click button works)

cc @zico15@joaodibba

…ror in sandbox
sandbox="allow-scripts" (added in PR TypeComposer#34) blocks real localStorage access
inside the iframe, causing TypeComposer's runtime to throw:
SecurityError: Failed to read the 'localStorage' property from 'Window':
The document is sandboxed and lacks the 'allow-same-origin' flag.
Adding allow-same-origin is not the right fix:
- The iframe loads a blob: URL, which always has an opaque origin, so
allow-same-origin cannot grant access to the parent page's storage anyway.
- It would also weaken the sandbox comment/intent and confuse future readers.
The correct fix is to shim window.localStorage and window.sessionStorage with
a lightweight in-memory Map-backed implementation that is installed before any
module code runs (as the first <script> tag in <head>, before the importmap).
The shim only activates when a try/catch probe detects that real localStorage
throws a SecurityError — no-op if the browser ever allows it.
The shim is deliberately non-persistent (lifetime = iframe document). Any
TypeComposer code that probes storage for feature detection or theme prefs
will work correctly; actual persistence is not meaningful inside a preview
iframe anyway.
Files changed:
src/views/playground/PlaygroundView.ts — shim <script> injected before
importmap; sandbox attribute comment and JSDoc updated.
Reported by: João (post-merge of PR TypeComposer#35)
@joaodibba
joaodibba merged commit 453f156 into TypeComposer:mainJun 12, 2026
1 check failed
Sign up for freeto 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

@lucas-spin@joaodibba
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(playground): inject in-memory localStorage shim — fixes SecurityError in sandboxed iframe - #36

Merged
joaodibba merged 1 commit into
TypeComposer:mainfrom
lucas-spin:fix/playground-localstorage-sandbox-shim
Jun 12, 2026
Merged

fix(playground): inject in-memory localStorage shim — fixes SecurityError in sandboxed iframe#36
joaodibba merged 1 commit into
TypeComposer:mainfrom
lucas-spin:fix/playground-localstorage-sandbox-shim

Conversation

@lucas-spin

Copy link
Copy Markdown
Contributor

Bug reported by João (post-PR #35)

[Playground] Initialization failed: SecurityError: Failed to read the
'localStorage' property from 'Window': The document is sandboxed and
lacks the 'allow-same-origin' flag.
Runtime Error: SecurityError: Failed to read the 'localStorage' property
from 'Window': The document is sandboxed and lacks the 'allow-same-origin' flag.

Root cause

PR #34 added sandbox="allow-scripts" to the preview <iframe> (correct security posture). The browser enforces that a sandboxed document cannot access window.localStorage unless allow-same-origin is also present in the sandbox attribute. TypeComposer's runtime (loaded from jsDelivr, post-PR #35) probes localStorage during initialization, which throws a SecurityError inside the iframe.

Why allow-same-origin is NOT the fix

The iframe is loaded via a blob: URL (see injectCode()). Blob URLs always carry an opaque origin — they are never same-origin with the parent page. Adding allow-same-origin to a blob-loaded iframe:

  • Would not grant access to the parent page's localStorage (opaque origin = no same-origin relationship).
  • Would, however, weaken the sandbox signal and confuse future readers into thinking the iframe is relaxed.
  • Adds no benefit since there is no same-origin relationship to exploit.

Fix — in-memory localStorage shim

Inject a tiny IIFE shim as the first <script> tag in <head> (before the importmap), so window.localStorage and window.sessionStorage are always available when TypeComposer imports.

Strategy:

  1. try { void window.localStorage } — probe real storage.
  2. If it throws (SecurityError) → install a Map-backed localStorage-shaped object via Object.defineProperty (or direct assignment as fallback).
  3. The shim is activation-only — a no-op in environments where real localStorage works.
  4. State is ephemeral (iframe lifetime only). This is correct for a preview pane.
<script>(function(){try{voidwindow.localStorage;// probe — succeeds → skip shim}catch(_e){var_store=Object.create(null);// ... Map-backed getItem / setItem / removeItem / clear / key / lengthObject.defineProperty(window,'localStorage',{value: _shimStorage,writable: false,configurable: true});Object.defineProperty(window,'sessionStorage',{value: _shimStorage,writable: false,configurable: true});}})();</script>

Security analysis

ApproachIsolation preservedFixes errorNotes
Add allow-same-origin⚠️ Weakens sandboxWrong: opaque origin means no real-storage access AND weakens sandbox
In-memory shim (this PR)✅ No change to sandboxSafest: no attribute change, opaque origin preserved, storage scoped to iframe

The sandbox="allow-scripts" attribute is unchanged. iframe isolation is identical to PR #34.


Files changed

FileChange
src/views/playground/PlaygroundView.tsShim <script> injected before importmap in createIframeHTML(); sandbox comment and JSDoc updated

Build result

✓ 3448 modules transformed.
dist/assets/esbuild-BHljloGq.wasm 12,332.68 kB
dist/assets/index-C2_sis7Q.css 48.96 kB
dist/assets/index-BzPRh-vd.js 1,756.71 kB
✓ built in 14.92s

Verified in dist:

  • In-memory localStorage shim comment present in bundle
  • SecurityError shim guard string present
  • esm.sh not in bundle
  • ✅ jsDelivr URL for typecomposer present
  • allow-same-origin appears only inside a comment string (not as attribute)

Manual browser verification still needed

  1. Open deployed docs → Playground tab
  2. DevTools Console: no SecurityError on load
  3. DevTools Network: no esm.sh requests
  4. Playground renders the demo AppPage (purple gradient + click button works)

cc @zico15@joaodibba

…ror in sandbox
sandbox="allow-scripts" (added in PR TypeComposer#34) blocks real localStorage access
inside the iframe, causing TypeComposer's runtime to throw:
SecurityError: Failed to read the 'localStorage' property from 'Window':
The document is sandboxed and lacks the 'allow-same-origin' flag.
Adding allow-same-origin is not the right fix:
- The iframe loads a blob: URL, which always has an opaque origin, so
allow-same-origin cannot grant access to the parent page's storage anyway.
- It would also weaken the sandbox comment/intent and confuse future readers.
The correct fix is to shim window.localStorage and window.sessionStorage with
a lightweight in-memory Map-backed implementation that is installed before any
module code runs (as the first <script> tag in <head>, before the importmap).
The shim only activates when a try/catch probe detects that real localStorage
throws a SecurityError — no-op if the browser ever allows it.
The shim is deliberately non-persistent (lifetime = iframe document). Any
TypeComposer code that probes storage for feature detection or theme prefs
will work correctly; actual persistence is not meaningful inside a preview
iframe anyway.
Files changed:
src/views/playground/PlaygroundView.ts — shim <script> injected before
importmap; sandbox attribute comment and JSDoc updated.
Reported by: João (post-merge of PR TypeComposer#35)
@joaodibba
joaodibba merged commit 453f156 into TypeComposer:mainJun 12, 2026
1 check failed
Sign up for freeto 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

@lucas-spin@joaodibba