Provide session cache to isolated Framey panels

This commit is contained in:
SAPHID committed 2026-10-25 21:09:45 +00:00
1 parent 1356ea4df1
commit fe4e09032d
4 files changed
+77 -2

No files matched your search

@@ -0,0 +1,26 @@
# Framey session cache browser check, 25 October 2026
Frame Control v0.4.34's opaque Framey iframe made the official Steam Status
1.2.3 frontend log `SecurityError` for its `localStorage` cache. This follow-up
used a per-panel memory substitute. It did **not** change the iframe's sandbox
flags or give plugin code Frame Control's UI key.
A local Chromium page loaded the unmodified official Steam Status 1.2.3 archive
from `tests/fixtures/framey-official/123.zip` (SHA-256
`9053308e04a74f44745b45b9f4f50af3673348d367e7b14bacc5677de707ac4e`).
The page used `sandbox="allow-scripts allow-popups"` and returned `{}` for
four `get_settings` calls. That reply was a stub, not the Decky backend.
The iframe sent `framey:ready`. Its test script wrote and read a cache item.
The parent could not read the iframe document (`contentDocument` was null).
The plugin displayed Steam Status, Store, Community, WebAPI, Connection
Managers and Players Online. Its public status request returned HTTP 200.
No cache `SecurityError` appeared in the captured console, while the
same archive had logged that error with the v0.4.34 host.
Node host tests also checked item updates, removal, clear, key order, string
conversion, quota rejection, and preservation of an existing item after a
rejected write. The substitute keeps at most 256 items and 512 KiB of keys and
values **in memory**. Closing or reloading the panel loses this cache. This
check did not test offline behavior, persistent plugin settings, a live Decky
backend, SteamVR visibility, controller input, or headset performance.
+9 -2
View File
@@ -37,6 +37,11 @@ through Decky's *existing* websocket connection. Framey never opens a second
Decky websocket, because Decky allows only one frontend connection. The iframe
does not receive Frame Control's UI key. React and ReactDOM are bundled for
offline use under their MIT licenses in `ui/vendor`.
Framey gives each plugin panel a bounded, in-memory `localStorage` substitute
because browser storage is denied to an opaque-origin iframe. It supports the
usual item methods, up to 256 entries and 512 KiB of keys and values. The data
is cleared when the panel reloads or closes. Use the plugin backend for settings
that must survive a new panel session.
The host currently supplies sections, rows, buttons, toggles, text fields,
sliders, grouped dropdowns,
@@ -58,8 +63,10 @@ records exactly what was exercised.
The pinned official Steam Status 1.2.3 frontend also
[rendered in local Chromium](evidence/framey-steam-status-browser-2026-10-25.md)
with a stubbed settings reply. Its public status request worked, but its cache
logged `localStorage` errors in Framey's opaque iframe. No real Decky backend
or worn headset test was part of that check.
logged `localStorage` errors in Framey's v0.4.34 opaque iframe. A later
[local browser check](evidence/framey-session-cache-2026-10-25.md) confirmed
that the panel-lifetime substitute avoids those errors while preserving the
opaque origin. Neither check used a real Decky backend or a worn headset.
Fake Frame CI also installs and removes pinned official ProtonDB Badges 1.3.4
and Steam Status 1.2.3 archives, checks their API v2 manifests and frontend
bytes, and verifies they can be packed as Framey panels. It does not execute
+17
View File
@@ -19,6 +19,23 @@ const context=vm.createContext({window,document:{getElementById:id=>{
navigator:{language:'en'}, parent,addEventListener:(type,handler)=>listeners.push({type,handler}),
setTimeout:()=>1,clearTimeout(){}});
vm.runInContext(testSource,context);
// A plugin gets bounded, panel-lifetime storage without the parent's origin.
const storage=window.localStorage;
assert.equal(storage.getItem('missing'),null);
storage.setItem('theme','dark');
storage.setItem('count',2);
assert.equal(storage.length,2);
assert.equal(storage.key(0),'theme');
assert.equal(storage.getItem('count'),'2');
storage.setItem('theme','light');
assert.equal(storage.getItem('theme'),'light');
assert.throws(()=>storage.setItem('large','x'.repeat(262144)),{name:'QuotaExceededError'});
assert.equal(storage.getItem('theme'),'light');
storage.removeItem('count');
assert.equal(storage.length,1);
storage.clear();
assert.equal(storage.length,0);
assert.throws(()=>Object.defineProperty(window,'localStorage',{value:{}}),TypeError);
// Official @decky/rollup bundles use the automatic JSX runtime, including keyed lists.
const jsx=window.SP_JSX.jsx('p',{id:'message',children:'Saved'},'message-key');
assert.equal(jsx.type,'p');
+25
View File
@@ -1,6 +1,31 @@
/* Framey panel compatibility layer. The iframe has an opaque origin and no UI key. */
(() => {
'use strict';
// Opaque-origin iframes throw on localStorage access. Give plugins a small
// panel-lifetime cache without granting their code the parent page's origin.
const cache = new Map();
const cacheLimit = 262144; // UTF-16 code units: at most 512 KiB of keys and values.
const cacheQuotaError = () => {
const error = new Error('Framey panel cache is full');
error.name = 'QuotaExceededError';
return error;
};
const panelStorage = Object.freeze({
get length() { return cache.size; },
key(index) { return Number.isInteger(Number(index)) ? [...cache.keys()][Number(index)] ?? null : null; },
getItem(key) { return cache.get(String(key)) ?? null; },
setItem(key, value) {
key = String(key); value = String(value);
const previous = cache.get(key);
const size = [...cache].reduce((total, [name, data]) => total + name.length + data.length, 0)
- (previous === undefined ? 0 : key.length + previous.length) + key.length + value.length;
if ((previous === undefined && cache.size >= 256) || size > cacheLimit) throw cacheQuotaError();
cache.set(key, value);
},
removeItem(key) { cache.delete(String(key)); },
clear() { cache.clear(); },
});
Object.defineProperty(window, 'localStorage', {value:panelStorage, enumerable:true});
const R = window.SP_REACT = window.React;
window.SP_REACTDOM = window.ReactDOM;
const jsx = (type, props, key) => {