Skip to content

A public page, the press kit, and the Flipper tap - #18

Merged
ryannzander merged 6 commits into
mainfrom
b/site-and-loose-ends
Sep 13, 2026
Merged

ryannzander merged 6 commits into
mainfrom
b/site-and-loose-ends

Conversation

@ryannzander

Copy link
Copy Markdown
Owner

Everything on b/flipper-and-recovery that landed after #17, replayed onto main so the diff is only the new work.

The site

site/ is a plain HTML/CSS/ES-module page — no build step, no dependencies — published to GitHub Pages by .github/workflows/pages.yml on pushes to main that touch it.

The hero is the argument rather than a picture of it. site/digest.js is a from-scratch keccak256 and EIP-712 encoder, so editing the transaction recomputes the real proposal hash in the visitor's browser:

  • Load the frozen vector fills in vectors/execute.json's inputs and reproduces its digest byte for byte, live.
  • Let the server lie sends a hash for a different recipient while the screen still shows the original, and the panel refuses the way the phone does.
  • It also shows sha256(digest) — what the Enclave actually signs — which matches vectors/p256.json.

digest.js is not a third implementation of the protocol to keep in sync; it is checked against the frozen vector in front of the visitor. If that check ever goes red, the page is wrong.

The rest: the custody chain marked by how many of the two signatures exist at each step, the four real screenshots with the press-kit captions, the prompt-injection scene, four claims linking into the repo, and an honest "what this is not, yet".

Also in here

  • The press kit, and making the app see proposals it did not create.
  • Tapping an NFC tag on the Flipper to approve.
  • start-hub reading keys from .env, and not letting a blank line clobber an exported value.

Before this is live

Pages has to be switched on once: Settings → Pages → Source → GitHub Actions. Then it deploys to https://ryannzander.github.io/Tappy/.

Checks

pnpm --filter @tappy/protocol test (19 passed) and pnpm typecheck both green locally. site/ is outside the pnpm workspace, so CI does not touch it.

🤖 Generated with Claude Code

ryannzander and others added 6 commits September 13, 2026 10:31
Background jobs started from a shell die with their parent, which meant the hub
kept vanishing between sessions and the phone would suddenly fail to connect for
no visible reason. nohup plus disown orphans it to init instead.

  ./scripts/start-hub.sh

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The start script carried the agent and relayer keys as inline defaults. The repo
checks testnet keys in on purpose, so this is not a leak — but a default in a
script is silently wrong in a way a missing variable is not. You cannot tell
whether you are running with your own key or one that shipped with the repo, and
the same shape would quietly accept a mainnet key later.

It now reads .env and refuses to start without SEPOLIA_RPC_URL, AGENT_KEY and
RELAYER_KEY, naming the file to fix. Consistent with the repo's own rule that
errors should be loud rather than degrade quietly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This is the interaction the project was always for, and it turns out to need no
Apple Developer account at all — because the Flipper reads the tag, not the phone.
Core NFC needs a paid entitlement; the Flipper needs a USB cable.

It also needs no app on the device. The Flipper's CLI exposes NFC over USB, so the
laptop drives the read: no C app, and no need for an NFC module in the JS engine,
which does not have one. That was the blocker recorded in DECISIONS #17 and it
turns out to be avoidable.

SIGNER_KIND=nfc shows the proposal, runs the read, and signs when a tag lands.
ALLOWED_UIDS restricts which tags count; empty accepts any, which is the right
default before anyone has registered one.

What a tap proves is physical presence, not identity — a UID is readable and
cloneable by anyone who can hold a reader near it. It stands exactly where a
button press would. The comment says so, and the pitch should too.

The CLI verb and the UID format differ across firmware, so `nfc:probe` asks the
device rather than guessing: it prints the real `nfc` help, runs a read, and dumps
the raw bytes when a tag is tapped. flipper-check.sh now runs it as step 5.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A restart silently did nothing: sourcing .env overwrote keys passed on the command
line with its own empty values, which then failed the required-variable check and
left the previous server running on the previous gate.
Screenshots are captured from the running app against the live Sepolia deployment
rather than mocked, so the balances, the proposal id and the agent's replies are
all real.

Getting them exposed two genuine bugs. The app only knew about proposals created
during its own session, so relaunching while one was awaiting approval showed
nothing at all and the request sat invisible until it expired — it now takes the
hub's list as the source of truth. And the chat had no history, so every relaunch
looked like the agent had amnesia; the hub already kept the transcript and the app
now loads it.

The mark is three rings closing on a point of contact: something was touched, and
only then did it happen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
site/ is plain HTML, CSS and ES modules with no build step, published to
GitHub Pages by .github/workflows/pages.yml on pushes to main.

The hero is the argument rather than a picture of it: site/digest.js is a
from-scratch keccak256 and EIP-712 encoder, so editing the transaction
recomputes the real proposal hash live. "Load the frozen vector" fills in
vectors/execute.json's inputs and reproduces its digest byte for byte, and
"Let the server lie" sends a hash for a different recipient so the panel
refuses the way the phone does. It also shows sha256(digest), which is what
the Enclave actually signs, and that matches vectors/p256.json.

digest.js is not a third implementation of the protocol to keep in sync — it
is checked against the frozen vector in front of the visitor. If that check
ever goes red, the page is wrong.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ryannzander
ryannzander merged commit a6bc694 into main Sep 13, 2026
2 checks passed
@ryannzander
ryannzander deleted the b/site-and-loose-ends branch September 13, 2026 14:33
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.

1 participant