Scanning a QR should open the camera, not wait for a pasted payload #60
Labels
No labels
needs-info
needs-triage
ready-for-agent
ready-for-human
v2
wayfinder:grilling
wayfinder:map
wayfinder:prototype
wayfinder:research
wayfinder:task
wayfinder:ticket
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
aiko/denpa#60
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Left over from #40, which landed the rest of ADR-0011's client half in
0d323ed.The 読み取り path on 周波数 decodes a QR payload — origin, code and the reserved
fp— and enrols from it without the operator typing an address. What it does not do is open a camera. Today a phone or the pad scans with its own camera app and the decoded text is pasted in, which works but is not what #42 decided the scan path would feel like.Two ways to close it, and they are not equivalent:
tauri-plugin-barcode-scanner— native, mobile only, and the ADR-0002-shaped answer. Needs acfg(mobile)dependency, an Android permission, and a build on a device: none of that could be verified in the session that landed #40.getUserMediadecoder — ADR-0002 rules it out for the same reason it rules out the mic.Not blocking a conversation: the code still reaches the device either way.
Map: aiko/denpa#41
Decided 2026-07-31: neither of the two options above. The OS camera keeps the camera, and hands off by deep link. v1.
The issue framed this as native scanner vs webview decoder, and both carry a cost this does not need to pay. There is a third path: the operator scans with whatever camera app the device already has, and the QR carries a URL that opens Denpa.
The QR payload becomes
denpa://enrol?origin=…&code=…&fp=…rather than raw text.tauri-plugin-deep-link(v2) generates theandroid.intent.action.VIEW+DEFAULT+BROWSABLEintent-filter into the manifest fromtauri.conf.json > plugins > deep-link. A custom scheme is declared withappLink: falseand needs no.well-known/assetlinks.jsonand no verified host — which is what makes it the right shape here, because the origin is per-install and self-hosted, so Android App Links verification could never have covered it. The scheme registers on the desktop from the same config.What this buys: no camera permission, no
cfg(mobile)scanner dependency, and nogetUserMedia. No media enters the webview at all, so ADR-0002 is untouched rather than argued with. The enrolment path beneath is unchanged — the same origin, code and reservedfparrive at the same decoder.Two things that must be verified on a device, not from the code:
httpscheme. It reliably chipshttps://; custom-scheme handling varies by scanner. If it will not, the fallback is an unverifiedhttps://intent filter, which shows an app chooser instead of going straight through — uglier, still no camera code. Decide this by scanning a real code on the Pixel and the pad.ollvt-hermes-bridge; the payload changing from raw text to a URL is a coordinated change there. Old-shaped payloads pasted by hand should keep working — the paste path is the fallback and is not being removed.Scope: v1, by decision on 2026-07-31. Small enough to ride, even though it crosses a repo.
Acceptance
tauri-plugin-deep-linkregistered,denpa://declared for mobile (appLink: false) and desktopdenpa://enrol?…reaches the existing 読み取り decoder and enrols, cold-start and already-running bothollvt-hermes-bridgeemits the URL-shaped payloadThe two halves that do not need a device are merged. Both repos, in lockstep — see the caveat at the bottom.
The payload (
denpatou, merged):qr_payloademitsdenpa://enrol?v=1&origin=…&code=…&fp=…instead of bare JSON. A custom scheme needs no.well-known/assetlinks.jsonand no verified host, which is the reason this shape fits: the origin is per-install and self-hosted, so App Links verification could never have covered it.The decoder (client, merged):
scannedEnrollmentFromreads the link, and still reads a pasted JSON payload. Both shapes funnel through oneenrollmentFrom, so neither can enrol on terms the other would refuse.The link is matched by hand rather than through
URL, whose treatment of a non-special scheme's authority differs between engines — this has to read the same in a webview, in a Tauri window and in the suite. The action is pinned toenrol, so a futuredenpa://verb cannot be replayed into an enrolment.Two things review found, both fixed, both on the enrolment path where being wrong means a device bound to a server the operator never named:
(.+), so anything after a#was parsed as query. Nothing got through —#fails the origin pattern and the code shape — but that is downstream validation catching what the parse should not have produced. It stops at the fragment now.vwas written by the go-between and never read, which made the version field decoration. A version this client does not know is now refused rather than read on today's terms.Still open, and why
tauri-plugin-deep-linkregistered,denpa://declared for mobile (appLink: false) and desktopdenpa://enrol?…reaches the decoder cold-start and already-runningThe plugin wiring is deliberately not done: it needs a device build to mean anything, and the decisive question — whether the Pixel's camera app offers to open a non-
httpscheme — is answerable only by scanning a real code. If it will not, the recorded fallback is an unverifiedhttps://intent filter, which shows a chooser instead of going straight through. Neither path needs camera code.Caveat while both are on
main: the go-between now prints the link shape, and only a client built from currentmainreads it. Between restarting the gateway and rebuilding the client, a freshly minted QR will not parse — the code itself still works typed in by hand.Gate: go-between
247 passed; client 66 test files, 882 tests,npm run typecheckexits 0.The wiring is done and merged.
tauri-plugin-deep-link2.4.9 is registered,denpa://is declared for mobile withappLink: falseand for desktop, and an inbound link reachesonScan— the same door a pasted payload uses, so a link cannot enrol on terms a paste could not.Both deliveries are handled, because they are different code paths and only one is easy to test by hand: a link arriving while the app runs is an event (
onOpenUrl), and a scan that launches the app has no event to hear — its url waits ingetCurrent().Verified on the Pixel, not just in tests. The generated manifest carries exactly what this issue specified:
No host, no verification — no
assetlinks.jsonanywhere. Firing a real intent at a force-stopped app launched it:So Android resolves the scheme to this app and cold-starts it from a link. That is the mechanism this issue turns on.
What that test could not show. The Pixel is already enrolled, so onboarding is not mounted and the hook is not listening — the payload reached nobody, by design. A link arriving at a working device re-enrolling it would be a worse failure than one being ignored, and
#37records that a wrong enrolment is recoverable only by editing state by hand. Delivery into the decoder is covered by tests and by the sharedonScanpath, but has not been watched on a device that still needs enrolling.What is left
httpscheme is the one thing no amount of wiring answers. If it will not, the recorded fallback is an unverifiedhttps://filter — a chooser instead of a straight-through, still no camera code.New APK carrying this is installed on the Pixel (
app-universal-debug.apk,arm64-v8a). Gate: 66 test files, 905 tests,npm run typecheckexits 0,cargo checkpasses.Moved back to
ready-for-human. The camera scan can be built blind, but its acceptance is a stock-camera scan on the phone; merging it would mean merging unverified. Flip it back toready-for-agentif you would rather have the code waiting for you than the ticket.Closed by scope change rather than by building a scanner.
6018580lets the OS hand the scan over, so nothing here opens a camera: the phone's own camera app decodes and fires adenpa://deep link, taken by7e71cc6, bounded at the fragment byed66a94, and routed to the screen that can use it bye72d59b. Notauri-plugin-barcode-scanner, nogetUserMedia, so ADR-0002 stands.