mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-26 08:01:53 +01:00
Support chunked official Decky frontends in Framey
This commit is contained in:
1 parent
5c9ca110ed
commit
b0c4647b85
16 files changed
+198
-20
No files matched your search
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 59 KiB |
@@ -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 |
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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. '
|
||||
|
||||
@@ -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)
|
||||
|
||||
BIN
Binary file not shown.
+10
-1
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
@@ -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'<script id="framey-bundle" type="application/json">([^<]+)</script>', page)
|
||||
encoded = re.search(r'<script id="framey-bundle" type="application/json"[^>]*>([^<]+)</script>', 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__':
|
||||
|
||||
@@ -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()
|
||||
@@ -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
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
+10
-4
@@ -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;
|
||||
|
||||
+49
-7
@@ -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 ('''<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Framey plugin</title><style>body{font:21px/1.4 system-ui;background:#0d1925;color:#f3f7fa;margin:0;padding:20px}
|
||||
button,select,input{font:inherit;min-height:52px;background:#23618c;color:#fff;border:1px solid #72baf0;border-radius:9px;padding:8px}
|
||||
button:focus-visible,select:focus-visible,input:focus-visible{outline:4px solid #f8d370}button{margin:4px 8px 4px 0}button:disabled{opacity:.55}section{padding:12px;margin:12px 0;background:#1b3042;border-radius:10px}
|
||||
label{display:grid;gap:6px;margin:10px 0}small{color:#b9c9d6}</style>
|
||||
<p id="framey-error" role="status"></p><div id="framey-toasts" aria-live="polite"></div><div id="framey-root"></div><div id="framey-modal"></div>
|
||||
<script id="framey-bundle" type="application/json">''' + encoded + '''</script>
|
||||
<script id="framey-bundle" type="application/json"''' + module + '''>''' + encoded + '''</script>
|
||||
<script src="/framey-runtime/react.js"></script><script src="/framey-runtime/react-dom.js"></script>
|
||||
<script src="/framey-runtime/host.js"></script></html>''').encode()
|
||||
|
||||
|
||||
@@ -3691,6 +3691,16 @@ class Handler(BaseHTTPRequestHandler):
|
||||
self.send_bytes(framey_panel.panel(plugin_id), 'text/html; charset=utf-8', framey=True)
|
||||
except (ValueError, FileNotFoundError, OSError):
|
||||
self.send_json({'error':'Framey panel unavailable'}, 404)
|
||||
elif path.startswith('/framey/module/'):
|
||||
try:
|
||||
plugin_id, name = path.removeprefix('/framey/module/').split('/', 1)
|
||||
module = framey_panel.local_module(int(plugin_id), name)
|
||||
# The plugin iframe has an opaque origin; only its module
|
||||
# imports need cross-origin access to these public JS files.
|
||||
self.send_bytes(module, 'text/javascript; charset=utf-8', framey=True,
|
||||
headers=[('Access-Control-Allow-Origin', 'null')])
|
||||
except (ValueError, FileNotFoundError, OSError):
|
||||
self.send_json({'error':'Framey module unavailable'}, 404)
|
||||
elif path.startswith('/framey-runtime/'):
|
||||
files = {'react.js':'vendor/react-18.3.1.min.js',
|
||||
'react-dom.js':'vendor/react-dom-18.3.1.min.js',
|
||||
|
||||
Reference in new issue
Block a user