mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-26 18:01:57 +01:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b0d8bf3968 | ||
|
|
b05a666e50 | ||
|
|
8feb19a6d3 |
No files matched your search
@@ -0,0 +1,32 @@
|
||||
# Chunked Framey frontend audit route, 26 October 2026
|
||||
|
||||
The local 116-plugin Chromium audit served chunked modules at a route that
|
||||
omitted the store receipt, while the production panel requested
|
||||
`/framey/module/<id>/<receipt>/<file>`. The audit parsed that receipt as part
|
||||
of the file path and returned 404. This made its result for chunked frontends
|
||||
weaker than the separate targeted panel checks.
|
||||
|
||||
The audit now accepts the production URL shape only when the plugin ID,
|
||||
receipt and JavaScript path are valid, the receipt equals the catalog SHA-256,
|
||||
and the cached archive bytes match that SHA-256. Invalid IDs, receipts, paths
|
||||
and changed archive bytes fail before the module is served. The production
|
||||
Framey panel and installer are unchanged.
|
||||
|
||||
## Exact archived browser check
|
||||
|
||||
Pinned official Deck Shelves 2.0.0 (store ID 135, SHA-256
|
||||
`b566280ad38fd3ecd584b5b46a855f301539eb9a86063834c64b7045f7063f91`)
|
||||
ran through the repaired audit in an opaque Chromium iframe. Its entry module
|
||||
and all eight JavaScript chunks returned HTTP 200 at receipt-bound URLs. The
|
||||
frontend reported ready and called `get_settings`; the audit denied that
|
||||
backend call and a Steam library read. Plugin network access remained blocked
|
||||
by the audit policy. The previous
|
||||
catalog browser observation had failed to resolve `./chunks/...` from a blob
|
||||
URL. This check verifies the current audit route and frontend initializer,
|
||||
not the official backend or a live shelf.
|
||||
|
||||
The focused route regression covers entry and chunk reads plus wrong receipt,
|
||||
path traversal, malformed ID and changed archive bytes. The catalog's
|
||||
**4 works / 50 partial / 51 broken / 11 N/A** statuses are unchanged. Deck
|
||||
Shelves remains partial; installed settings, home-shelf behavior, worn VR and
|
||||
physical controller input remain unverified.
|
||||
@@ -0,0 +1,58 @@
|
||||
# v0.4.46 tagged release, 26 October 2026
|
||||
|
||||
[PR #326](https://github.com/saphid/frame-control/pull/326) passed every required
|
||||
check on exact head `fc978a2afdfa47e60477fcb0bb573e7b6181fa89` and merged
|
||||
at 12:31:06 UTC. Main became `f84a4da9b1ae938d15b658af17903c0e10a56e94`.
|
||||
All seven jobs in the [post-merge main run](https://github.com/saphid/frame-control/actions/runs/40323149126)
|
||||
passed, including iOS and fake-Frame e2e. The app and lockfile both say
|
||||
`0.4.46`. Annotated tag `v0.4.46` dereferences to that main commit. The
|
||||
[tagged release workflow](https://github.com/saphid/frame-control/actions/runs/40324176903)
|
||||
passed its draft, Linux, Windows, Apple Silicon Mac, Intel Mac and checksum jobs.
|
||||
|
||||
## Frame and package gates
|
||||
|
||||
The [candidate evidence](v0446-candidate-2026-10-26.md) records a locked
|
||||
exact-version real Frame smoke: **12 passed / 1 optional x86 launch skipped /
|
||||
0 failed**, with owned cleanup and lock release. The x86 launch needs Steam
|
||||
Linux Runtime 4, which was absent. A separate 12:25 UTC read-only check acquired
|
||||
and released its own `/tmp/frame-test.lock`; SteamVR activity was **3**
|
||||
(standby), proximity raw **3.0**, and battery **100%**. No wearer was confirmed,
|
||||
so no wake, controller input or interactive worn-check script ran.
|
||||
|
||||
The draft contained exactly ten installers plus `SHA256SUMS`, each with a
|
||||
GitHub SHA-256 digest. A fresh downloaded checksum file matched the ten digest
|
||||
lines regenerated from GitHub metadata byte for byte; its digest was
|
||||
`365e7f6d9549ff993162964920a6560480069ed04ce1eca5551696ce7bbc4ae8`.
|
||||
`scripts/publish-release.sh --dry-run v0.4.46` passed the exact tag, installer,
|
||||
checksum and update-manifest gates after release notes were attached.
|
||||
|
||||
Independent downloads passed these checks:
|
||||
|
||||
| Package | Result |
|
||||
| --- | --- |
|
||||
| Linux x86_64 AppImage | SHA-256 `65f66f1bbb5908d49c179307950f93b643c2b8da8ac4da48a778c5c9bac5d41b`; launched under isolated Xvfb, bundled server returned HTTP 200 with `FrameControl/1`, then stopped. |
|
||||
| Mac arm64 ZIP | SHA-256 `9af88f75db7936472590022ca03a03da9de4f91de7b35c5d4d130b6dc78ab4b5`; ZIP CRC valid, bundle version 0.4.46, main Mach-O CPU arm64. |
|
||||
| Mac x64 ZIP | SHA-256 `28d9ac00c9c918a7cb9f2caac54505faa8a953285bb40f82e02d9233b775d37a`; ZIP CRC valid, bundle version 0.4.46, main Mach-O CPU x86_64. |
|
||||
| Windows x64 Setup | SHA-256 `68e303dcabb2c82ba5b9a5a58bace2ca8154f9645f1da2214ca4c6dbb05f157d`; PE/Nullsoft installer format. Tagged Windows CI checked packaged files and Python libraries. |
|
||||
| Linux arm64 DEB | SHA-256 `da9b00bd44ca7a91c542df26d8c5711ad564d618add34f89aef946785c0bd538`; package version 0.4.46 and architecture arm64. |
|
||||
|
||||
Tagged Mac and Windows packages were not launched on physical machines during
|
||||
this release gate. Earlier physical Intel Mac acceptance covers older bytes.
|
||||
|
||||
## Publication and limits
|
||||
|
||||
`scripts/publish-release.sh v0.4.46` published
|
||||
[v0.4.46](https://github.com/saphid/frame-control/releases/tag/v0.4.46) at
|
||||
12:48:03 UTC. GitHub's latest-release API returned the expected tag,
|
||||
`draft=false`, `prerelease=false`, ten installers, `SHA256SUMS` and
|
||||
`update.json`. A fresh public latest-download of `update.json` reported
|
||||
version 0.4.46, the stable release page, ten hashed installers and the
|
||||
worn-VR limit in its notes.
|
||||
|
||||
The exact-version official catalog is **4 works / 50 partial / 51 broken /
|
||||
11 N/A** out of 116. Free Loader is partial for package, local frontend and
|
||||
isolated settings/empty-deals evidence. Its actual ARM64 Decky load, live
|
||||
feeds and notifications remain unverified. The real Frame was in standby;
|
||||
official-plugin wearer visibility, physical controller usability, keyboard
|
||||
and optical comfort remain unverified. The hourly shared-lock worn check stays
|
||||
enabled.
|
||||
@@ -11,6 +11,7 @@ import io
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
from unittest.mock import patch
|
||||
import zipfile
|
||||
@@ -41,6 +42,27 @@ def render_panel(record, data):
|
||||
return framey_panel.panel(record['id']).replace(
|
||||
b'<script id="framey-bundle"', PRELUDE.encode()+b'<script id="framey-bundle"')
|
||||
|
||||
|
||||
def read_module_asset(path, records, cache):
|
||||
"""Serve the same receipt-bound module URL shape as the installed panel."""
|
||||
prefix = '/framey/module/'
|
||||
if not path.startswith(prefix):
|
||||
raise ValueError('Invalid module path')
|
||||
parts = path[len(prefix):].split('/', 2)
|
||||
if len(parts) != 3 or not re.fullmatch(r'[1-9][0-9]*', parts[0]):
|
||||
raise ValueError('Invalid plugin ID')
|
||||
plugin_id, receipt, name = parts
|
||||
if not framey_panel.RECEIPT.fullmatch(receipt) or not framey_panel.MODULE_PATH.fullmatch(name):
|
||||
raise ValueError('Invalid module receipt or path')
|
||||
record = records[int(plugin_id)]
|
||||
if receipt != record['sha256']:
|
||||
raise ValueError('Plugin receipt changed')
|
||||
data = (cache/(receipt+'.zip')).read_bytes()
|
||||
if hashlib.sha256(data).hexdigest() != receipt:
|
||||
raise ValueError('Archive hash mismatch')
|
||||
with zipfile.ZipFile(io.BytesIO(data)) as archive:
|
||||
return archive.read(record['folder']+'/dist/'+name)
|
||||
|
||||
PAGE = '''<!doctype html><meta charset=utf-8><title>Framey catalog frontend audit</title>
|
||||
<h1>Framey catalog frontend audit</h1>
|
||||
<p>Exact archived frontend code runs in an opaque iframe. All backend calls and network requests are denied.
|
||||
@@ -134,15 +156,7 @@ def main():
|
||||
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)
|
||||
body = read_module_asset(path, records, args.cache)
|
||||
content_type = 'text/javascript; charset=utf-8'
|
||||
module_asset = True
|
||||
except (KeyError, ValueError, OSError, zipfile.BadZipFile):
|
||||
|
||||
@@ -5,6 +5,7 @@ import hashlib
|
||||
import importlib.util
|
||||
import io
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import unittest
|
||||
import zipfile
|
||||
|
||||
@@ -21,6 +22,7 @@ class ReceiptBoundAudit(unittest.TestCase):
|
||||
stream = io.BytesIO()
|
||||
with zipfile.ZipFile(stream, 'w') as archive:
|
||||
archive.writestr('Example/dist/index.js', bundle)
|
||||
archive.writestr('Example/dist/chunks/frontend.js', b'export const ready = true;')
|
||||
data = stream.getvalue()
|
||||
digest = hashlib.sha256(data).hexdigest()
|
||||
record = {'id':42, 'folder':'Example', 'sha256':digest}
|
||||
@@ -30,3 +32,20 @@ class ReceiptBoundAudit(unittest.TestCase):
|
||||
self.assertIn(b'const report=(kind,message)', page)
|
||||
with self.assertRaisesRegex(ValueError, 'hash mismatch'):
|
||||
audit.render_panel({**record, 'sha256':'0'*64}, data)
|
||||
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
cache = Path(directory)
|
||||
(cache/(digest+'.zip')).write_bytes(data)
|
||||
records = {42:record}
|
||||
route = f'/framey/module/42/{digest}/'
|
||||
self.assertEqual(audit.read_module_asset(route+'index.js', records, cache), bundle)
|
||||
self.assertEqual(audit.read_module_asset(route+'chunks/frontend.js', records, cache),
|
||||
b'export const ready = true;')
|
||||
for bad in (f'/framey/module/42/{"0"*64}/index.js',
|
||||
route+'chunks/../frontend.js', route+'other.json',
|
||||
f'/framey/module/042/{digest}/index.js'):
|
||||
with self.subTest(bad=bad), self.assertRaises((ValueError, KeyError)):
|
||||
audit.read_module_asset(bad, records, cache)
|
||||
(cache/(digest+'.zip')).write_bytes(data+b'changed')
|
||||
with self.assertRaisesRegex(ValueError, 'hash mismatch'):
|
||||
audit.read_module_asset(route+'index.js', records, cache)
|
||||
Reference in new issue
Block a user