diff --git a/docs/evidence/framey-deck-shelves-browser-2026-10-26.json b/docs/evidence/framey-deck-shelves-browser-2026-10-26.json new file mode 100644 index 0000000..2d6fc17 --- /dev/null +++ b/docs/evidence/framey-deck-shelves-browser-2026-10-26.json @@ -0,0 +1,32 @@ +{ + "dateUtc": "2026-10-26", + "plugin": { + "storeId": 135, + "name": "Deck Shelves", + "version": "2.0.0", + "sha256": "b566280ad38fd3ecd584b5b46a855f301539eb9a86063834c64b7045f7063f91" + }, + "mode": "opaque allow-scripts iframe in local Chromium, exact archived frontend, backend and network denied", + "baselineError": "relative JavaScript chunk imports cannot resolve from a blob URL", + "result": { + "initializerReady": true, + "error": null, + "moduleResponses200": 9, + "settingsVisible": [ + "Enable shelves on Home", + "Create default shelves", + "Behavior", + "Shelves" + ], + "attemptedBackendMethods": ["get_settings"], + "backendMethodsExecuted": [], + "steamLibraryRead": "denied by audit", + "pluginNetwork": "denied by audit" + }, + "limits": [ + "No Steam Home shelf injection or settings write was verified", + "No official backend executed", + "No real Frame or worn VR interaction occurred", + "Settings visibility alone does not prove the plugin works" + ] +} diff --git a/docs/evidence/framey-deck-shelves-browser-2026-10-26.md b/docs/evidence/framey-deck-shelves-browser-2026-10-26.md new file mode 100644 index 0000000..56c3cf8 --- /dev/null +++ b/docs/evidence/framey-deck-shelves-browser-2026-10-26.md @@ -0,0 +1,7 @@ +# Deck Shelves chunked frontend, 26 October 2026 + +The exact official Deck Shelves 2.0.0 archive (store ID 135, SHA-256 `b566280ad38fd3ecd584b5b46a855f301539eb9a86063834c64b7045f7063f91`) contains `dist/index.js` and eight JavaScript chunks. Its API v1 `connect(1, "Deck Shelves")` bridge sits in `chunks/decky-api-WgbNuFYS.js`, not in the entry file; the original static scanner therefore reported its frontend API as unknown. Framey's prior blob import failed before initialization because a blob URL has no directory in which to resolve `./chunks/...`. The [numeric observation](framey-deck-shelves-browser-2026-10-26.json) and [local Chromium capture](framey-deck-shelves-browser-2026-10-26.png) record a targeted follow-up using the unchanged official frontend. + +Framey now serves JavaScript files from the managed plugin's `dist` directory using hierarchical module URLs. Its opaque `allow-scripts` iframe still has no Frame UI key. Local Chromium received HTTP 200 for the entry and eight chunks, reported the initializer ready, and rendered the settings panel with “Enable shelves on Home,” “Create default shelves,” “Behavior,” and “Shelves.” The browser audit denied the attempted `get_settings` backend call, Steam library read and plugin network access. No backend method executed. + +The catalog moves this exact version from broken to **partial** for visible local settings only. The plugin's main feature injects shelves into Steam Home; Framey has no verified Steam Home patch target. Settings writes, official backend behavior, real Frame module serving, wearer-visible VR, and physical controller input remain unverified. The [compatibility table](../framey-compatibility.md) and store views retain these limits. diff --git a/docs/evidence/framey-deck-shelves-browser-2026-10-26.png b/docs/evidence/framey-deck-shelves-browser-2026-10-26.png new file mode 100644 index 0000000..ccb7ca6 Binary files /dev/null and b/docs/evidence/framey-deck-shelves-browser-2026-10-26.png differ diff --git a/docs/framey-compatibility.md b/docs/framey-compatibility.md index 411005e..36ca39f 100644 --- a/docs/framey-compatibility.md +++ b/docs/framey-compatibility.md @@ -4,7 +4,7 @@ Snapshot: 2026-10-25T23:19:48.854504+00:00; 116 current visible official plugins This table and both store views use [`ui/framey-compatibility.json`](../ui/framey-compatibility.json). Results apply only to the exact store ID, version and SHA-256. A changed digest/version is **unverified**. Regenerate this document with `python3 scripts/framey-compatibility-doc.py`; use `--check` in review. -**Works: 1 · Partial: 48 · Broken: 56 · N/A: 11.** +**Works: 1 · Partial: 49 · Broken: 55 · N/A: 11.** - **Works** requires verified useful plugin behavior with its real backend. Read the reason for the verified scope and remaining limits; it does not imply worn-VR verification. - **Partial** means only a subset or a local fixture scope was verified, often just frontend/settings initialization. The remaining backend/device functionality is unverified; read the reason for the exact scope. @@ -27,7 +27,7 @@ Open the browser audit URL in local Chromium, run `runAudit()` and inspect/downl The current published digests are pinned in the JSON; if the store changed, retain/download those exact archives for reproduction. Audit downloads permit larger ZIPs for inspection; production installer limits remain 50 MiB ZIP / 150 MiB unpacked / 30 MiB per member. -After the VR job released the shared lock, this audit acquired it and used a private temporary Decky 3.3.0 ARM64 user service. Read-only ProtonDB `get_setting` and Steam Status `get_settings` calls succeeded through the live Steam CEF websocket. The service and private base were removed and the lock released; normal managed services stayed inactive, battery was 100 → 99, and no power setting was changed. [Redacted results](evidence/framey-official-backend-2026-10-25.json) record exactly what ran. A [2026-10-26 follow-up](evidence/framey-events-backend-2026-10-26.json) verified default Notebook drawing/save/reopen across a real backend restart and Simple Timer start/cancel, countdown/completion events, a visible toast and persisted recents/subtle mode. Only private test settings were written. Simple Timer remains partial because its own expired-timer startup raises a missing `timer_task` error; sound/suspend are unsupported. A [local audio follow-up](evidence/framey-audio-backends-2026-10-26.md) exercised Volume Mixer, Volume Boost and Mixer through real Decky 3.3.0 amd64 backends on private silent virtual audio targets. Sliders, numeric volume entry, independent server readbacks and Mixer filter persistence across a runtime restart passed. These three stay partial: real ARM64 Frame audio dependencies/session mapping remain unverified because the shared headset lock was occupied. A [library context follow-up](evidence/framey-library-context-2026-10-26.md) read 1,106 app overviews, 895 game IDs and the current public Steam ID from real Frame CEF under the shared lock. Local Chromium replayed this detached snapshot to render Deck Settings game selection, Steamback's empty save panel and PlayTime statistics/settings. Steamback and PlayTime used actual Decky 3.3.0 amd64 backends in an isolated container; their ARM64 backends and automatic game hooks remain unverified. Shotty's frontend now starts, but its published backend fails on missing `click`; it stays broken. Other official backends remain unexecuted. The separate VR job proves Frame Control/sample compositor rendering, not any official plugin. No worn-VR official-plugin result is claimed. A [local UI follow-up](evidence/framey-ui-fallbacks-2026-10-26.json) rendered Pause Games and Tailscale Control settings with all backend/network actions denied; it does not verify process pausing or Tailscale changes. A [read-only Steam bridge follow-up](evidence/framey-steam-reads-2026-10-26.json) verified actual Frame country, install folders and system information, plus IsThereAnyDeal's country dropdown in local Chromium. The installed-plugin status was fixture-shaped in that relay; plugin backends stayed denied and the deployed headset HTTP endpoint remains unverified. Earlier focused checks cover [ProtonDB](evidence/framey-browser-2026-10-25.md), [Steam Status/cache](evidence/framey-cache-browser-2026-10-25.md), [Storage Cleaner](evidence/framey-api-v1-browser-2026-10-25.md), and [legacy MusicControl](evidence/framey-legacy-2026-10-25.md). +After the VR job released the shared lock, this audit acquired it and used a private temporary Decky 3.3.0 ARM64 user service. Read-only ProtonDB `get_setting` and Steam Status `get_settings` calls succeeded through the live Steam CEF websocket. The service and private base were removed and the lock released; normal managed services stayed inactive, battery was 100 → 99, and no power setting was changed. [Redacted results](evidence/framey-official-backend-2026-10-25.json) record exactly what ran. A [2026-10-26 follow-up](evidence/framey-events-backend-2026-10-26.json) verified default Notebook drawing/save/reopen across a real backend restart and Simple Timer start/cancel, countdown/completion events, a visible toast and persisted recents/subtle mode. Only private test settings were written. Simple Timer remains partial because its own expired-timer startup raises a missing `timer_task` error; sound/suspend are unsupported. A [local audio follow-up](evidence/framey-audio-backends-2026-10-26.md) exercised Volume Mixer, Volume Boost and Mixer through real Decky 3.3.0 amd64 backends on private silent virtual audio targets. Sliders, numeric volume entry, independent server readbacks and Mixer filter persistence across a runtime restart passed. These three stay partial: real ARM64 Frame audio dependencies/session mapping remain unverified because the shared headset lock was occupied. A [library context follow-up](evidence/framey-library-context-2026-10-26.md) read 1,106 app overviews, 895 game IDs and the current public Steam ID from real Frame CEF under the shared lock. Local Chromium replayed this detached snapshot to render Deck Settings game selection, Steamback's empty save panel and PlayTime statistics/settings. Steamback and PlayTime used actual Decky 3.3.0 amd64 backends in an isolated container; their ARM64 backends and automatic game hooks remain unverified. Shotty's frontend now starts, but its published backend fails on missing `click`; it stays broken. A [chunked frontend follow-up](evidence/framey-deck-shelves-browser-2026-10-26.md) loaded Deck Shelves 2.0.0 through a managed JavaScript module route and rendered its settings in an opaque local Chromium panel. Its backend was denied; Steam Home shelves and worn VR remain unverified. Other official backends remain unexecuted. The separate VR job proves Frame Control/sample compositor rendering, not any official plugin. No worn-VR official-plugin result is claimed. A [local UI follow-up](evidence/framey-ui-fallbacks-2026-10-26.json) rendered Pause Games and Tailscale Control settings with all backend/network actions denied; it does not verify process pausing or Tailscale changes. A [read-only Steam bridge follow-up](evidence/framey-steam-reads-2026-10-26.json) verified actual Frame country, install folders and system information, plus IsThereAnyDeal's country dropdown in local Chromium. The installed-plugin status was fixture-shaped in that relay; plugin backends stayed denied and the deployed headset HTTP endpoint remains unverified. Earlier focused checks cover [ProtonDB](evidence/framey-browser-2026-10-25.md), [Steam Status/cache](evidence/framey-cache-browser-2026-10-25.md), [Storage Cleaner](evidence/framey-api-v1-browser-2026-10-25.md), and [legacy MusicControl](evidence/framey-legacy-2026-10-25.md). ## Every current plugin @@ -140,7 +140,7 @@ The API column shows manifest / actual bundled frontend API. Many manifests say | 132: SuggestMe 1.5.3 | 1 / 2 | partial | Initializer succeeds; recommendation backend and Steam library data unverified. | static archive, local browser, fake-Frame | | 133: Non-Steam Badges 0.1.0 | 1 / 2 | partial | Initializer succeeds; Non-Steam game badges have no supported Steam page injection target. | static archive, local browser, fake-Frame | | 134: Deck Progress Tracker 1.3.6 | 1 / 2 | partial | Initializer succeeds; Steam game/library statistics and page patches unverified. | static archive, local browser, fake-Frame | -| 135: Deck Shelves 2.0.0 | 1 / unknown | broken | Frontend audit failed: Failed to resolve module specifier "./chunks/pluginApi-B8sV7970.js". Invalid relative url or base scheme isn't hierarchical.. | static archive, local browser, fake-Frame | +| 135: Deck Shelves 2.0.0 | 1 / 1 | partial | The exact chunked frontend now initializes and renders Deck Shelves settings in an opaque local Chromium panel. Backend calls were denied; Home-screen shelf injection has no verified Steam UI target in Framey. Settings writes, real backend behavior and worn VR are unverified. | static archive, local browser, fake-Frame | | 136: Decky-Clipper 1.0.2 | 1 / 2 | broken | Plugin contains a non-ARM64 Linux binary: Decky-Clipper/bin/gstreamer-1.0/libgstproxy.so. | static archive, local browser, fake-Frame | | 137: Decky Proton Launch 0.9.0 | 1 / 2 | partial | Initializer succeeds; Proton wrapper installation and launch changes were not run. | static archive, local browser, fake-Frame | | 138: Docky 1.4.13 | legacy / legacy | broken | This plugin needs root and is not supported by the Frame user service. | static archive, local browser, fake-Frame | diff --git a/scripts/framey-audit-browser.py b/scripts/framey-audit-browser.py index 7c85504..cd9d667 100644 --- a/scripts/framey-audit-browser.py +++ b/scripts/framey-audit-browser.py @@ -94,6 +94,7 @@ def main(): def do_GET(self): path = self.path.split('?')[0] content_type = 'text/html; charset=utf-8' + module_asset = False if path == '/': # Only names/IDs are needed by the UI; never embed raw executable source. catalog = json.dumps([{'id': p['id'], 'name': p['name']} for p in plugins]).replace('<', '\\u003c') @@ -113,6 +114,22 @@ def main(): except (KeyError, ValueError, OSError, zipfile.BadZipFile): self.send_error(404, 'Verified frontend archive unavailable') return + elif path.startswith('/framey/module/'): + try: + plugin_id, name = path.removeprefix('/framey/module/').split('/', 1) + if not framey_panel.MODULE_PATH.fullmatch(name): + raise ValueError('Invalid module path') + record = records[int(plugin_id)] + data = (args.cache/(record['sha256']+'.zip')).read_bytes() + if hashlib.sha256(data).hexdigest() != record['sha256']: + raise ValueError('Archive hash mismatch') + with zipfile.ZipFile(io.BytesIO(data)) as archive: + body = archive.read(record['folder']+'/dist/'+name) + content_type = 'text/javascript; charset=utf-8' + module_asset = True + except (KeyError, ValueError, OSError, zipfile.BadZipFile): + self.send_error(404, 'Verified frontend module unavailable') + return else: files = {'/framey-runtime/react.js': 'ui/vendor/react-18.3.1.min.js', '/framey-runtime/react-dom.js': 'ui/vendor/react-dom-18.3.1.min.js', @@ -124,6 +141,8 @@ def main(): content_type = 'text/javascript' self.send_response(200) self.send_header('Content-Type', content_type) + if module_asset: + self.send_header('Access-Control-Allow-Origin', 'null') self.send_header('Content-Security-Policy', "default-src 'none'; script-src 'self' 'unsafe-inline' blob:; style-src 'unsafe-inline'; img-src data:; frame-src 'self'; connect-src 'none'") self.end_headers() self.wfile.write(body) diff --git a/scripts/framey-compatibility-doc.py b/scripts/framey-compatibility-doc.py index 1545900..50dfa6a 100644 --- a/scripts/framey-compatibility-doc.py +++ b/scripts/framey-compatibility-doc.py @@ -60,6 +60,9 @@ def render(data): 'to render Deck Settings game selection, Steamback\'s empty save panel and PlayTime statistics/settings. ' 'Steamback and PlayTime used actual Decky 3.3.0 amd64 backends in an isolated container; their ARM64 backends and automatic game hooks remain unverified. ' 'Shotty\'s frontend now starts, but its published backend fails on missing `click`; it stays broken. ' + 'A [chunked frontend follow-up](evidence/framey-deck-shelves-browser-2026-10-26.md) loaded Deck Shelves 2.0.0 ' + 'through a managed JavaScript module route and rendered its settings in an opaque local Chromium panel. ' + 'Its backend was denied; Steam Home shelves and worn VR remain unverified. ' 'Other official backends remain unexecuted. ' 'The separate VR job proves Frame Control/sample compositor rendering, not any official plugin. ' 'No worn-VR official-plugin result is claimed. ' diff --git a/tests/e2e/test_framey.py b/tests/e2e/test_framey.py index aa67cb4..62d65c1 100644 --- a/tests/e2e/test_framey.py +++ b/tests/e2e/test_framey.py @@ -177,6 +177,14 @@ u=h/'.config/systemd/user/frame-control-decky.service'; u.parent.mkdir(parents=T expected = hashlib.sha256(bundle.read(plugin['folder']+'/dist/index.js')).hexdigest() self.assertEqual(ssh(f'sha256sum {installed}/dist/index.js').split()[0], expected) self.assertEqual(json.loads(ssh(f'cat {installed}/plugin.json')).get('api_version', 'legacy'), plugin['api_version']) + if plugin['id'] == 135: + chunk = 'chunks/pluginApi-B8sV7970.js' + with zipfile.ZipFile(io.BytesIO(official_archive(plugin))) as bundle: + expected_chunk = bundle.read(plugin['folder']+'/dist/'+chunk) + status, served, headers = api('GET', '/framey/module/135/'+chunk) + self.assertEqual((status, served), (200, expected_chunk)) + self.assertEqual(headers['Access-Control-Allow-Origin'], 'null') + self.assertEqual(api('GET', '/framey/module/135/chunks/../plugin.json')[0], 404) job = finished(ok('POST', '/api/decky/plugin', {'action':'remove','id':plugin['id'],'acknowledged':True})) self.assertFalse(job.get('error'), job) diff --git a/tests/fixtures/framey-official/135.zip b/tests/fixtures/framey-official/135.zip new file mode 100644 index 0000000..d198ada Binary files /dev/null and b/tests/fixtures/framey-official/135.zip differ diff --git a/tests/fixtures/framey-official/README.md b/tests/fixtures/framey-official/README.md index 015e0bb..3a06bf5 100644 --- a/tests/fixtures/framey-official/README.md +++ b/tests/fixtures/framey-official/README.md @@ -7,20 +7,26 @@ from a local fixture so CI does not depend on the store being online. | File | Official plugin | Version | Store SHA-256 | | --- | --- | --- | --- | +| `7.zip` | CSS Loader | 2.1.3 | `99beaf1374af82408e6d056e0b0147e5fca544e1035fa7ab88419231697bc231` | | `12.zip` | MusicControl | 1.1.6 | `246b89fd653c60a735b1cc401d1b0937d5cb969eea9024561661ff741f081d62` | | `14.zip` | ProtonDB Badges | 1.3.4 | `90a3453ac02709cad64096e7539fd8141fa56f99ae3441fc24a4990dc9684358` | | `123.zip` | Steam Status | 1.2.3 | `9053308e04a74f44745b45b9f4f50af3673348d367e7b14bacc5677de707ac4e` | | `50.zip` | Storage Cleaner | 1.4.0 | `1afa1a6f129c5d419de0e0bf0f6db8d8e50385a5659179346e996031ea372591` | | `18.zip` | Bluetooth | 3.1.1 | `f07f7b74f31da15b5e3a8234a3f00faa01365b56b8f496aca7bb79120be7b8a5` | | `26.zip` | HLTB for Deck | 2.0.12 | `bd54a23b0b8ec0798aa590e5be236522a593529d7680e77fb888e2971bf3d558` | +| `36.zip` | SteamGridDB | 1.8.1 | `e84f5b1f111af8862b06218479491fbf61036e1bfd9f16f93f9901e9b9ebc5e0` | +| `135.zip` | Deck Shelves | 2.0.0 | `b566280ad38fd3ecd584b5b46a855f301539eb9a86063834c64b7045f7063f91` | The matching source projects are +[CSS Loader](https://github.com/suchmememanyskill/SDH-CssLoader), [MusicControl](https://github.com/nikurasu/MusicControl), [ProtonDB Badges](https://github.com/bschelst/protondb-decky), [Steam Status](https://github.com/bschelst/steamstatus-decky), [Storage Cleaner](https://github.com/mcarlucci/decky-storage-cleaner), [Bluetooth](https://github.com/Outpox/Bluetooth), and -[HLTB for Deck](https://github.com/morwy/hltb-for-deck). +[HLTB for Deck](https://github.com/morwy/hltb-for-deck), +[SteamGridDB](https://github.com/SteamGridDB/decky-steamgriddb), and +[Deck Shelves](https://github.com/santojon/Deck-Shelves). The test verifies the ZIP digest, archive policy, API v1/v2 manifest, installed frontend bytes, managed store receipt, and removal. Separate local host tests start Bluetooth's frontend and its initial panel without running effects or @@ -29,3 +35,6 @@ module lookup. They do **not** run plugin Python backends, Bluetooth actions, HLTB searches, or prove JavaScript panels render in the VR shell. The Bluetooth result and limits are in [the fixture evidence](../../../docs/evidence/framey-bluetooth-fixture-2026-10-25.md). +Deck Shelves' chunked frontend renders its settings in local Chromium with +backend/network calls denied. Its Steam Home shelves cannot be verified in the +isolated VR panel; no worn-headset test is claimed. diff --git a/tests/framey_official.py b/tests/framey_official.py index dde05dc..1064d52 100644 --- a/tests/framey_official.py +++ b/tests/framey_official.py @@ -17,6 +17,8 @@ PLUGINS = ( 'folder': 'hltb-for-deck', 'sha256': 'bd54a23b0b8ec0798aa590e5be236522a593529d7680e77fb888e2971bf3d558'}, {'id': 36, 'name': 'SteamGridDB', 'version': '1.8.1', 'api_version': 1, 'folder': 'decky-steamgriddb', 'sha256': 'e84f5b1f111af8862b06218479491fbf61036e1bfd9f16f93f9901e9b9ebc5e0'}, + {'id': 135, 'name': 'Deck Shelves', 'version': '2.0.0', 'api_version': 1, + 'folder': 'Deck-Shelves', 'sha256': 'b566280ad38fd3ecd584b5b46a855f301539eb9a86063834c64b7045f7063f91'}, ) # This archive has a legacy IIFE frontend with no manifest API version. diff --git a/tests/test_framey_official.py b/tests/test_framey_official.py index 9b73314..79af0ca 100644 --- a/tests/test_framey_official.py +++ b/tests/test_framey_official.py @@ -74,9 +74,14 @@ class OfficialBundles(unittest.TestCase): (folder / '.frame-control-store.json').write_text(json.dumps({'storeId': plugin['id']})) with patch.object(framey_panel, 'PLUGINS', plugins): page = framey_panel.panel(plugin['id']).decode() - encoded = re.search(r'', page) + encoded = re.search(r'', page) self.assertIsNotNone(encoded) self.assertEqual(base64.b64decode(encoded.group(1)), (folder / 'dist/index.js').read_bytes()) + if plugin['id'] == 135: + self.assertIn('data-module="/framey/module/135/index.js"', page) + with patch.object(framey_panel, 'PLUGINS', plugins): + self.assertEqual(framey_panel.local_module(135, 'chunks/pluginApi-B8sV7970.js'), + (folder/'dist/chunks/pluginApi-B8sV7970.js').read_bytes()) if __name__ == '__main__': diff --git a/tests/test_framey_panel.py b/tests/test_framey_panel.py index f1a4830..98cb69b 100644 --- a/tests/test_framey_panel.py +++ b/tests/test_framey_panel.py @@ -187,6 +187,26 @@ assert.throws(()=>eval(EXPRESSION),/library is unavailable/); with self.assertRaises(FileNotFoundError): framey_panel.panel(14) + def test_chunked_frontend_serves_only_owned_javascript(self): + (self.folder/'dist/index.js').write_text( + "import './chunks/view.js'; export default () => ({content: 'OK'});") + chunks = self.folder/'dist/chunks' + chunks.mkdir() + (chunks/'view.js').write_text('export const title = "Settings";') + page = framey_panel.panel(14).decode() + self.assertIn('data-module="/framey/module/14/index.js"', page) + self.assertEqual(framey_panel.local_module(14, 'chunks/view.js'), + b'export const title = "Settings";') + for name in ('../plugin.json', 'chunks/../../plugin.json', + 'chunks/view.js.map', 'other.js', 'chunks/%2e%2e/view.js'): + with self.subTest(name=name), self.assertRaises(FileNotFoundError): + framey_panel.local_module(14, name) + (chunks/'linked.js').symlink_to(chunks/'view.js') + with self.assertRaises(FileNotFoundError): + framey_panel.local_module(14, 'chunks/linked.js') + with self.assertRaises(FileNotFoundError): + framey_panel.local_module(15, 'chunks/view.js') + def test_legacy_keyword_rpc_is_scoped_and_validated(self): import shlex def fake_ssh(command, stdin=None, **_): @@ -273,6 +293,15 @@ assert.throws(()=>eval(EXPRESSION),/library is unavailable/); self.assertIn(b'framey-bundle', body) self.assertEqual(get('/framey/panel/15')[0], 404) + (self.folder/'dist/chunks').mkdir() + (self.folder/'dist/chunks/view.js').write_text('export const value = 1;') + status, headers, body = get('/framey/module/14/chunks/view.js') + self.assertEqual(status, 200) + self.assertEqual(headers['Access-Control-Allow-Origin'], 'null') + self.assertEqual(body, b'export const value = 1;') + self.assertEqual(get('/framey/module/14/chunks/../plugin.json')[0], 404) + self.assertEqual(get('/framey/module/15/chunks/view.js')[0], 404) + if __name__ == '__main__': unittest.main() diff --git a/ui/framey-compatibility.json b/ui/framey-compatibility.json index 692433e..794f492 100644 --- a/ui/framey-compatibility.json +++ b/ui/framey-compatibility.json @@ -11886,9 +11886,9 @@ "archiveBytes": 862269, "api": 1, "flags": [], - "modernBridge": false, + "modernBridge": true, "frontend": true, - "frontendApi": "unknown", + "frontendApi": "1", "pythonFiles": [ "Deck-Shelves/main.py" ], @@ -11916,8 +11916,8 @@ "https://github.com/santojon/Deck-Shelves.git", "https://github.com/sponsors/santojon" ], - "status": "broken", - "reason": "Frontend audit failed: Failed to resolve module specifier \"./chunks/pluginApi-B8sV7970.js\". Invalid relative url or base scheme isn't hierarchical..", + "status": "partial", + "reason": "The exact chunked frontend now initializes and renders Deck Shelves settings in an opaque local Chromium panel. Backend calls were denied; Home-screen shelf injection has no verified Steam UI target in Framey. Settings writes, real backend behavior and worn VR are unverified.", "blockInstall": false, "verification": [ "static archive", @@ -11951,6 +11951,12 @@ "sha256": "b566280ad38fd3ecd584b5b46a855f301539eb9a86063834c64b7045f7063f91", "archiveValidation": "accepted", "result": "installed-removed" + }, + "chunkedFollowUp": { + "evidence": "docs/evidence/framey-deck-shelves-browser-2026-10-26.md", + "mode": "exact archive in opaque local Chromium; backend and network denied", + "frontendReady": true, + "settingsRendered": true } }, { diff --git a/ui/framey-host.js b/ui/framey-host.js index 7346904..79cac81 100644 --- a/ui/framey-host.js +++ b/ui/framey-host.js @@ -506,7 +506,8 @@ async function start() { try { await seeded; - const bytes = Uint8Array.from(atob(document.getElementById('framey-bundle').textContent), c=>c.charCodeAt(0)); + const bundleNode = document.getElementById('framey-bundle'); + const bytes = Uint8Array.from(atob(bundleNode.textContent), c=>c.charCodeAt(0)); const bundleText = new TextDecoder().decode(bytes); // Only panels referencing library globals need the extra read. A failed // read leaves them unavailable rather than fabricating an empty library. @@ -517,10 +518,15 @@ // Old Decky builds are expressions returning the initializer, rather than // ES modules. A module wrapper preserves the sandbox and avoids eval. const legacy = /^\s*\(function\s*\(/.test(bundleText); - const url = URL.createObjectURL(new Blob(legacy ? - ['const __frameyLegacy =\n', bytes, '\nexport default __frameyLegacy;'] : [bytes], {type:'text/javascript'})); let module; - try { module = await import(url); } finally { URL.revokeObjectURL(url); } + if (bundleNode.dataset.module && !legacy) { + // A hierarchical URL lets official Vite chunks resolve relative imports. + module = await import(new URL(bundleNode.dataset.module, location.href).href); + } else { + const url = URL.createObjectURL(new Blob(legacy ? + ['const __frameyLegacy =\n', bytes, '\nexport default __frameyLegacy;'] : [bytes], {type:'text/javascript'})); + try { module = await import(url); } finally { URL.revokeObjectURL(url); } + } if (typeof module.default !== 'function') throw Error('The Decky frontend has no plugin entry point'); plugin = await module.default(legacyServerAPI); if (legacy && plugin?.title && !plugin.titleView) plugin.titleView = plugin.title; diff --git a/ui/framey_panel.py b/ui/framey_panel.py index 56ea646..c1a55c2 100644 --- a/ui/framey_panel.py +++ b/ui/framey_panel.py @@ -11,6 +11,7 @@ import frame_decky METHOD = re.compile(r'[A-Za-z][A-Za-z0-9_]{0,63}\Z') EVENT = re.compile(r'[A-Za-z0-9_:/.-]{1,128}\Z') SESSION = re.compile(r'[A-Za-z0-9_.-]{1,100}\Z') +MODULE_PATH = re.compile(r'[A-Za-z0-9_-]+(?:/[A-Za-z0-9_-]+)*/[A-Za-z0-9_.-]+\.js\Z|index\.js\Z') PLUGINS = Path.home()/'.local/share/frame-control/decky/plugins' @@ -18,8 +19,8 @@ class PanelError(ValueError): pass -def local_bundle(plugin_id): - """Read a modern Decky bundle with a store receipt; reject linked paths.""" +def _owned_dist(plugin_id): + """Locate a compatible store-owned frontend without following linked paths.""" if type(plugin_id) is not int or plugin_id <= 0 or not PLUGINS.is_dir() or PLUGINS.is_symlink(): raise FileNotFoundError('Framey plugin unavailable') for folder in PLUGINS.iterdir(): @@ -55,24 +56,65 @@ def local_bundle(plugin_id): legacy = re.match(rb'\s*\(function\s*\(', bundle) is not None if version is None and not legacy: raise FileNotFoundError('This legacy frontend is not a supported Decky IIFE bundle') - if version == 1 and not legacy and b'__DECKY_SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED_deckyLoaderAPIInit' not in bundle: - raise FileNotFoundError('This API v1 frontend does not use the supported Decky API bridge') - return bundle + if version == 1 and not legacy: + marker = b'__DECKY_SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED_deckyLoaderAPIInit' + supported = marker in bundle + if not supported and b'./chunks/' in bundle: + chunks = dist/'chunks' + if chunks.is_symlink() or not chunks.is_dir(): + raise FileNotFoundError('Framey plugin chunks unavailable') + files = list(chunks.iterdir()) + if len(files) > 128: + raise FileNotFoundError('Framey plugin has too many chunks') + for chunk in files: + if chunk.suffix != '.js': + continue + if chunk.is_symlink() or not chunk.is_file() or chunk.stat().st_size > 8*1024*1024: + raise FileNotFoundError('Framey plugin chunk unavailable') + if marker in chunk.read_bytes(): + supported = True + if not supported: + raise FileNotFoundError('This API v1 frontend does not use the supported Decky API bridge') + return dist, bundle except (OSError, ValueError): raise FileNotFoundError('Framey plugin files are invalid') raise FileNotFoundError('Plugin is not managed by the Frame Control Decky store') +def local_bundle(plugin_id): + """Read a modern Decky bundle with a store receipt; reject linked paths.""" + return _owned_dist(plugin_id)[1] + + +def local_module(plugin_id, name): + """Serve only JavaScript within a store-owned dist directory for module imports.""" + if not isinstance(name, str) or not MODULE_PATH.fullmatch(name): + raise FileNotFoundError('Framey module path unavailable') + dist, bundle = _owned_dist(plugin_id) + if name == 'index.js': + return bundle + target = dist + for part in name.split('/'): + target = target/part + if target.is_symlink(): + raise FileNotFoundError('Framey module path unavailable') + if not target.is_file() or target.stat().st_size > 8*1024*1024: + raise FileNotFoundError('Framey module unavailable') + return target.read_bytes() + + def panel(plugin_id): """The plugin receives no Frame UI key and is sandboxed to an opaque origin.""" - encoded = base64.b64encode(local_bundle(plugin_id)).decode('ascii') + bundle = local_bundle(plugin_id) + encoded = base64.b64encode(bundle).decode('ascii') + module = f' data-module="/framey/module/{plugin_id}/index.js"' if b'./chunks/' in bundle else '' return ('''