mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-25 23:02:40 +01:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe4e09032d | ||
|
|
1356ea4df1 | ||
|
|
c79ba7571f | ||
|
|
76dcd0c426 |
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.
|
||||
@@ -0,0 +1,28 @@
|
||||
# Steam Status in the local Framey browser, 25 October 2026
|
||||
|
||||
This check used the unmodified official Steam Status 1.2.3 archive already
|
||||
pinned in `tests/fixtures/framey-official/123.zip`. Its SHA-256 is
|
||||
`9053308e04a74f44745b45b9f4f50af3673348d367e7b14bacc5677de707ac4e`.
|
||||
The archive declares Decky API v2. No headset was contacted for this check.
|
||||
|
||||
On a Linux PC, we extracted the archive to a disposable plugin folder with a
|
||||
Frame Control store receipt. A local HTTP server served the page from
|
||||
`framey_panel.panel(123)`, Frame Control's React 18.3.1 runtime and its
|
||||
`framey-host.js`. A parent page put the panel in an iframe with
|
||||
`sandbox="allow-scripts"` and returned `{}` for the plugin's `get_settings`
|
||||
backend calls. This was a **stub**, not the Decky service.
|
||||
|
||||
Chromium reported `framey:ready` and four `get_settings` calls. The page showed
|
||||
Steam Status, Store, Community, WebAPI, Connection Managers, Players Online,
|
||||
and other controls. Its own public `https://steamstatus.schelstraete.org/api/v1/status`
|
||||
request returned HTTP 200 and populated the status view. A click on **Refresh**
|
||||
sent Framey's trusted pointer event to the parent; this test did not prove a
|
||||
fresh status fetch or settings write. The [browser screenshot](framey-steam-status-browser-2026-10-25.png)
|
||||
contains only the local test page and public service data.
|
||||
|
||||
The plugin logged `SecurityError` while reading and writing `localStorage` in
|
||||
the opaque-origin iframe. It caught the errors and rendered anyway. Its cache
|
||||
and offline behavior in Framey are **not verified**. The real Decky backend,
|
||||
live settings, SteamVR visibility, controller input and headset performance
|
||||
were not tested. This result supports only local frontend rendering for this
|
||||
exact archive and Frame Control v0.4.34 host code.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 69 KiB |
@@ -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,
|
||||
@@ -55,6 +60,13 @@ live Decky settings persistence, or badges on Steam game pages. The last item
|
||||
has no supported injection target in the Frame Control panel.
|
||||
The [browser and fake Frame evidence](evidence/framey-browser-2026-10-25.md)
|
||||
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 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
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in new issue
Block a user