Skip to content

Let a browser talk to Ember: CORS, and media keys - #118

Merged
arancool3000 merged 1 commit into
mainfrom
claude/kindlehub-link
Aug 6, 2026
Merged

arancool3000 merged 1 commit into
mainfrom
claude/kindlehub-link

Conversation

@arancool3000

Copy link
Copy Markdown
Owner

Two things stood between KindleHub's Ember Link card and actually working.

CORS

Ember Link is reached by a browser on a different origin — KindleHub runs on kindlehub.pro, Ember runs on your LAN — and without these headers the browser discards the reply. The only shape that worked at all was fetch(..., {mode:"no-cors"}), which can send a command but can never read one back. That makes pairing impossible from a web page, because /api/pair answers with the token in a body no-cors will not let the caller see.

A JSON POST is also not a "simple request", so the browser sends a preflight first; with no do_OPTIONS handler that 501'd and the command was never sent at all.

This is not a weakening of the auth model, and it's worth being precise about why. Every /api/* route still demands the PIN or a valid pairing token; the PIN is still refused off the LAN by _is_lan_ip; remote shell is still refused over the tunnel. CORS governs who may read a reply inside a browser — it has never been what stops a request arriving, since curl and any native app could always reach these routes.

The headers go on through an end_headers override rather than at each send_response, so the next route somebody adds cannot forget them.

Media keys

{"type":"media","action":"next"} presses the OS transport key, so it drives Spotify, a browser tab, VLC or a local file without Ember needing to know which is in front.

Actions are named by intent — playpause / next / prev / stop / volup / voldown / mute — because the caller is a Kindle and should not have to know the key is spelled nexttrack. An unknown action is refused by name rather than silently pressing nothing.

Testing

python3 -m py_compile passes, and the media branch is confirmed to sit inside _apply_locked's elif chain rather than after it. Not run against a live Ember instance — that needs your machine.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LTqV95KXLxBZuJhKULbmyp


Generated by Claude Code

Two things stood between KindleHub's Ember Link card and actually working.

CORS. Ember Link is reached by a BROWSER on a different origin — KindleHub runs
on kindlehub.pro, Ember runs on your LAN — and without these headers the browser
discards the reply. The only shape that worked at all was
fetch(..., {mode:"no-cors"}), which can send a command but can never READ one
back. That makes PAIRING impossible from a web page, because /api/pair answers
with the token in a body no-cors will not let the caller see. A JSON POST is also
not a "simple request", so the browser sends a preflight first; with no
do_OPTIONS handler that 501'd and the command was never sent at all.

This is not a weakening of the auth model, and it is worth being precise about
why. Every /api/* route still demands the PIN or a valid pairing token; the PIN
is still refused off the LAN by _is_lan_ip; remote shell is still refused over
the tunnel. CORS governs who may READ a reply inside a browser — it has never
been what stops a request arriving, since curl and any native app could always
reach these routes. The headers go on through an end_headers override rather
than at each send_response, so the next route somebody adds cannot forget them.

Media keys. `{"type":"media","action":"next"}` presses the OS transport key, so
it drives Spotify, a browser tab, VLC or a local file without Ember needing to
know which is in front. Actions are named by INTENT — playpause / next / prev /
stop / volup / voldown / mute — because the caller is a Kindle and should not
have to know the key is spelled "nexttrack". An unknown action is refused by
name rather than silently pressing nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LTqV95KXLxBZuJhKULbmyp
@arancool3000
arancool3000 merged commit 3eb2829 into main Aug 6, 2026
4 of 6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants