Skip to content

page-token-widget 0.1.0: an iPhone home screen widget for your quota - #1062

Open
liuzq1103 wants to merge 5 commits into
magpie-community:mainfrom
liuzq1103:feat/page-token-widget
Open

liuzq1103 wants to merge 5 commits into
magpie-community:mainfrom
liuzq1103:feat/page-token-widget

Conversation

@liuzq1103

@liuzq1103 liuzq1103 commented Nov 13, 2026 •

Copy link
Copy Markdown

What this changes

Adds packages/page-token-widget: a page in Magpie's window that puts a quota widget on the home screen of an iPhone.

The window shows a QR code. The camera of the phone opens the pairing page in Safari, the widget script is downloaded, Scriptable opens it, and the widget goes on the home screen. The widget shows the remaining quota of every subscription, the percentage and the reset countdown of the 5-hour and the weekly window, today's tokens, the last seven days and the ranking per provider. The plugin page picks which subscriptions the medium and the large widget show, and shows the state of Tailscale and the iPhones in the same tailnet.

Tapping the widget opens the page of the phone in Safari: the same numbers, written for a narrow screen. The page carries no token of its own — the phone brings its own in the address and the page drops it as soon as it has read it.

The plugin runs the service itself: no install script, no Node.js, no scheduled task, no manual port forwarding. It finds the tailnet address through Tailscale and publishes the port with a tailscale serve --tcp forwarding of its own.

How it is put together:

  • page/ holds a launcher only. The page is in a sandbox frame whose CSP is connect-src 'none', so it cannot fetch anything; it navigates its frame to the local service and the service answers with the console.
  • The host part (index.mjs) starts the service on 127.0.0.1. It listens first and asks Tailscale afterwards, because the host is only given a few seconds to come up.
  • The service reads the local quota API of Magpie (127.0.0.1:3425) and writes nothing in the configuration of Magpie.
  • The phone pairs with a one-time code. The widget exchanges that code for a token of its own and keeps it in the keychain of Scriptable.
  • The service knows two rooms instead of one. The console listens on 127.0.0.1:<port> and the phone side on 127.0.0.1:<phonePort>; the forwarding of Tailscale points at the phone side, so a request that comes in through the tailnet is never taken for a request of this computer. The Host header decides nothing: a device in the tailnet cannot read the console or the token whatever it puts in it. Every console route answers 403 on the phone side; the page of the phone and the API of the phone (/api/v1/overview, /api/v1/session, /scriptable/*) answer there instead. The page that carries the token may only be opened by hand (sec-fetch-mode: navigate), so another page cannot fetch it either.

Registry text (verbatim)

{ "package": "@magpie-community/page-token-widget", "name": "iPhone Quota Widget", "kind": "page", "community": true,
  "summary": { "en": "A quota widget on the home screen of your iPhone: scan one code and the widget shows the remaining quota, the reset countdown and the real token usage of Codex, MiniMax and GLM. Unlike the usage pages listed above, the numbers land on another device, and the quota stays the quota window of the subscription rather than a per-model split of tokens; the phone reaches the local service over your Tailscale tailnet.", "zh": "把额度组件放到 iPhone 桌面:扫一次码,组件显示 Codex、MiniMax、GLM 各家套餐的剩余额度、重置倒计时和真实 Token 用量。和上面那些用量页面不同,数字落在另一台设备上;额度保持订阅本身的额度窗口,而不是按模型摊开的 token;手机通过 Tailscale 尾网连进本机服务。" } },

Difference declaration: the registry entry, magpie.summary in package.json and this description carry the same text.

The listed usage pages answer on the machine that runs Magpie. This one answers on another device: the home screen of an iPhone, over the tailnet of the user, with the quota window of the subscription itself rather than a per-model split of tokens.

Testing

  • bun scripts/check.mjs — every package of the repository passes, the new one included. (This machine has no bun, so the same script ran under node with import.meta.dir replaced by import.meta.dirname: exit 0, every package ✓, including the new one.)
  • node --test test/*.test.mjs in packages/page-token-widget — 18 pass, 0 fail.
  • On a real Magpie (0.1.3681) the page of the plugin opens in the window and draws the console: the QR code, the tailnet address, the subscriptions and the state of Tailscale.
  • The same service answers on the tailnet address, and the phone of the tailnet fetches the widget script with the one-time code.
  • The root of the phone side answers with the page of the phone (200); every console route answers 403 there, with a Host header of 127.0.0.1:<port> and without one. The page of the phone carries no token of the service.

Standards kept: the page reads nothing of Magpie and reaches no network (the page files carry no fetch, no magpie.read); the package has no runtime dependency; the port is declared in package.json and the launcher falls back to it; the summary is in English and Chinese; the README has a Chinese part.

Not tried: the phone side was not run through the whole pair by someone else than the author of the package; the widget on a device without a tailnet was not checked.

🤖 Generated with Claude Code

liuzq1103 and others added 2 commits November 13, 2026 09:03
The usage pages that are listed read what magpie already knows and show it
in the window. This one puts the same numbers on another device: the
iPhone shows the remaining quota of Codex, MiniMax and GLM, the reset
countdown and the real token usage on its home screen.

The page in the window is a launcher. It carries no data and reads
nothing: the plugin's host part starts a local service, and the page
navigates its own frame to it. The service reads magpie's local quota
API, holds the phone's read-only token, and publishes its port in the
Tailscale tailnet, so the phone reaches it over the reader's own
network and never over the public internet. The pairing code is worth
one try and then it is spent.

The service answers only to a Host it knows and to a token, and the
console page that holds the token may only be opened by hand, never
fetched from another page.

How it differs from the pages that are listed: usage, page-usage-tree,
page-usage-board and page-model-quota all answer on the machine that
runs magpie, in the window. This one answers on the phone, over the
reader's tailnet, and its numbers are the quota windows of the
subscriptions themselves rather than a model-by-model split of them.

Reviewed at 753a61f. Run (on this machine, Windows, temp HOME):
bun scripts/check.mjs page-token-widget ok; bun scripts/check.mjs all
packages ok; packages/page-token-widget bun test 16 pass 0 fail.
Not tried: an iPhone, and a tailnet other than the reader's own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the summary and in Chinese

The summary in registry.json and the Chinese part of the README now say what the
new entry does that the listed usage pages can't: the numbers land on another
device, and the quota stays the quota window of the subscription.
@yetone

yetone commented Nov 13, 2026

Copy link
Copy Markdown
Contributor

Harm scan at 1f6c0af: nothing hostile. The vendored qrcode-generator is npm's 1.4.4 with one change (row/col in renderTo2dContext's fillRect). The plugin talks to the local gateway and to tailscale only.

One security gap to fix before merge: local in lib/server.mjs (line 304) is decided by the Host header, not by where the connection came from. The server also listens on the tailnet IP (line 494), and allowedHosts accepts 127.0.0.1/localhost there. So any device in the tailnet can do

curl -H 'Host: 127.0.0.1:3498' http://<tailnet-ip>:3498/

and get ui/index.html with __SERVICE_TOKEN__ filled in (no Sec-Fetch-Mode from curl, so the navigate check passes), and reach /api/v1/console/state, /console/pair and /console/options. The same holds through the tailscale serve --tcp forward, whose connections arrive from 127.0.0.1 too. A shared-in node or anyone the tailnet ACL lets in reads every subscription's quota and changes the plugin's settings.

Decide "local" by the socket: the 127.0.0.1 listener's requests are local, the tailnet listener's never are, whatever their Host says. Two listeners already exist, so a flag per listener is enough. A test: a request on the tailnet listener with Host: 127.0.0.1:<port> gets 403 on / and the console routes.

Also worth saying in the README: tailscale serve --bg --tcp=3498 is a standing Tailscale setting that outlives the plugin until it is removed.

liuzq1103 and others added 3 commits November 13, 2026 14:14
The console page and its API decide "is this request from this computer"
by the Host header, so a device in the tailnet could send
"Host: 127.0.0.1:3498" and read the console, which carries the token
that lets it read every subscription's quota, and could change the
settings of the plugin.

The service now listens on two sockets: the console on 127.0.0.1:3498,
the phone side on 127.0.0.1:3499. The tailnet reaches the phone side
through "tailscale serve --bg --tcp=3498 tcp://127.0.0.1:3499", so the
port in the tailnet stays 3498 and the widget, the pairing code and the
qr code do not change. Every console route answers 403 on the phone
side, whatever Host says.
Tapping the widget on the home screen asked the service for the root of the
phone, and the root of the phone was the console of the computer. The console
is only opened on the loopback address, so the phone was told that this page
is only opened on the computer of the service.

The service now has two rooms. The console keeps the console and its API; the
phone side answers with the page of the phone, which shows the same numbers.
That page carries no token: the phone brings its own in the address and the
page drops it as soon as it has read it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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