Repository navigation
feat: reach a host from a packaged Samsung TV page, and build the package - #42
Merged
Merged
Conversation
added 4 commits
October 2, 2026 22:21
A packaged page on a Samsung TV is told apart by what the runtime installs — `window.tizen`, a location that is not http(s) — never by the user agent. From that: the device pairs as "Samsung <model>", AV1 is not offered (the set says yes to every codec string, which is a hint and not a measurement), and a sent log names the model, Tizen version, firmware and Chromium behind it.
A packaged page cannot fetch the host: nothing from file:// completes a TLS connection to a self-signed certificate, and nobody accepts one on a TV. The plane it can dial, pinned by hash. So a target with `tunnel` reads the plane's hash over plain HTTP on the management port (the host's bootstrap route), checks the attestation as before, and then rides the plane's /mgmt session for every call: one request per bidirectional stream, a length-prefixed JSON head and the body until FIN. `tunnelFetch` is a fetch the SDK takes unchanged, so the connection and the device key get it injected and nothing above them knows the API moved. Sixteen requests in flight, the rest queue; a session the host closes idle is reopened by the next call.
A packaged page has one way to a host, the plane's tunnel, so every target it builds says so and its reach probes use the bootstrap. It starts in console mode: an app on a TV, held by a remote, with no address bar to ask. The set can put it behind Home or into standby, and a stream into a dark panel ends: hidden leaves the stream (the host keeps the game, the library offers Resume), and a frame loop that skips five seconds is read as the set having slept. `_e2e.html?tunnel=1&host=<address>` drives the same route from a desktop browser, which is how the host side is proven without a set.
`vite build --mode tizen` writes dist-tizen/: the same page with no source maps, the widget's config.xml at the build's numeric version, the icon, and Samsung's webapis.js on the page. `npm run build:tizen` then zips it into punktfunk-tizen-<version>.wgt, unsigned: a set only installs a package signed for its own DUID, so each owner signs their copy, and CI needs no Tizen SDK. The manifest asks for game mode and the four public privileges the page needs, and no allow-navigation, which would switch the runtime to a CSP that blocks the page's inline styles.
This was referenced Oct 2, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
T2 of
design/tizen-client-implementation-plan.md(planning repo,main@6f6c989): the client half of §4, thetizenbuild mode, the lifecycle and the two platform gates. The host half (T1: the plain-HTTP bootstrap and the/mgmttunnel) is a monorepo PR; this one runs against it.What changed
@punktfunk/streamplatform.ts:packaged()(a location that is nothttp(s):) andtizen()(window.tizen), never the user agent.deviceName()moves here and names a setSamsung <model>.tizenInfo()for the log header,exitApp()for T3.tunnel.ts:tunnelFetch(url, hash), afetchover a WebTransport session to/mgmt, pinned byserverCertificateHasheswithallowPooling: false. One request per bidirectional stream,u32BE head length + JSON{m, p, h}+ body until FIN; the reply{s, h}+ body streamed into a realResponse. Sixteen in flight, the rest queue; reopens after the host's idle close; rejects with aTypeErrorasfetchdoes. Its own session, not the glue's.pf-connect.ts:HostTargetgainstunnel;bootstrapUrl(plain HTTP on the management port at the plane's address),bootstrap(one GET:okwith the plane,no-planeon the host's own JSON 404, elseunreachable),reachTargetfor the probes.engine.ts: a tunnel target bootstraps instead ofreachWhy+fetchPlane, keeps the attestation check unchanged, then opens the tunnel and hands it toHost.blockedcannot happen on that path; a plane-off host gets its own sentence.host.ts:connection({fetch})anddeviceKey({fetch})take the injected fetch;art()and the posts already go throughconn.fetch.video.ts: no AV1 offered on Tizen (its WebCodecs says yes to everything; a hint, not a measurement).webgpuUsable()already reads anulladapter as no, andcreatePlanefalls through to WebGL2 on the throwVideoSurfaceWebGPU.createmakes for one.logs.ts: the sent log's header carries model, Tizen version, firmware and Chromium major on a set.punktfunk-webtargetOfreturns a tunnel target on a packaged page; the reach probes follow.pickUistarts the console on a packaged page.visibilitychange→ hidden leaves the stream; a frame-loop gap over 5 s is read as standby and does the same.vite build --mode tizen:dist-tizen/, no source maps,config.xml(version fromgit describe, numeric part) andicon.pngemitted beside the page,$WEBAPIS/webapis/webapis.json the page.npm run build:tizenzips it intopunktfunk-tizen-<version>.wgt(unsigned;tools/wgt.mjsis a dependency-free zip writer).tools/tizen-icon.mjsrenders the 512×423 icon from the brand mark.apps/web/tizen/config.xmlper plan §6:use.game.mode, the four public privileges,<access origin="*">, noallow-navigation. Package idpunktfunk0, fixed from here on._e2e.html?tunnel=1&host=<address>drives the tunnel route from a desktop browser: that is T1's gate.Verified here
npm run checkandnpm testfor@punktfunk/stream: 41 tests pass (9 new,tunnel.test.ts, against a fake session: framing, headers, body, 204, refusals, reconnect, the in-flight cap).tsc --noEmitonapps/web(with a shim for@unom/ui, which this environment cannot fetch).tools/wgt.mjson a stand-indist-tizen/: a valid zip withconfig.xmlat the root.Gate (open)
On the monitor: the packaged app pairs with
.21by Request access and lists the library and covers. Needs a host build that includes T1 and a session on the Mac Studio. Not done in this PR.🤖 Generated with Claude Code
https://claude.ai/code/session_01AMdVnA5hEykPfKrc5UByY7
Generated by Claude Code