Scanning a QR should open the camera, not wait for a pasted payload #60

Closed
opened 2026-07-30 20:17:51 +00:00 by aiko · 5 comments
Owner

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 a cfg(mobile) dependency, an Android permission, and a build on a device: none of that could be verified in the session that landed #40.
  • A webview getUserMedia decoder — 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

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 a `cfg(mobile)` dependency, an Android permission, and a build on a device: none of that could be verified in the session that landed #40. - A webview `getUserMedia` decoder — 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
Author
Owner

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 the android.intent.action.VIEW + DEFAULT + BROWSABLE intent-filter into the manifest from tauri.conf.json > plugins > deep-link. A custom scheme is declared with appLink: false and needs no .well-known/assetlinks.json and 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 no getUserMedia. 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 reserved fp arrive at the same decoder.

Two things that must be verified on a device, not from the code:

  • Whether the Pixel's camera app offers to open a non-http scheme. It reliably chips https://; custom-scheme handling varies by scanner. If it will not, the fallback is an unverified https:// 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.
  • The generator is not in this repo. The QR is emitted by 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-link registered, denpa:// declared for mobile (appLink: false) and desktop
  • An inbound denpa://enrol?… reaches the existing 読み取り decoder and enrols, cold-start and already-running both
  • ollvt-hermes-bridge emits the URL-shaped payload
  • Pasting a payload by hand still enrols
  • Verified by scanning a real code with the stock camera app on the Pixel and on the pad
**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 the `android.intent.action.VIEW` + `DEFAULT` + `BROWSABLE` intent-filter into the manifest from `tauri.conf.json > plugins > deep-link`. A custom scheme is declared with `appLink: false` and needs **no `.well-known/assetlinks.json` and 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 no `getUserMedia`. **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 reserved `fp` arrive at the same decoder. Two things that must be verified on a device, not from the code: - **Whether the Pixel's camera app offers to open a non-`http` scheme.** It reliably chips `https://`; custom-scheme handling varies by scanner. If it will not, the fallback is an unverified `https://` 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. - **The generator is not in this repo.** The QR is emitted by `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-link` registered, `denpa://` declared for mobile (`appLink: false`) and desktop - [ ] An inbound `denpa://enrol?…` reaches the existing 読み取り decoder and enrols, cold-start and already-running both - [ ] `ollvt-hermes-bridge` emits the URL-shaped payload - [ ] Pasting a payload by hand still enrols - [ ] Verified by scanning a real code with the stock camera app on the Pixel and on the pad
Author
Owner

This was generated by AI during triage.

The 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_payload emits denpa://enrol?v=1&origin=…&code=…&fp=… instead of bare JSON. A custom scheme needs no .well-known/assetlinks.json and 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): scannedEnrollmentFrom reads the link, and still reads a pasted JSON payload. Both shapes funnel through one enrollmentFrom, 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 to enrol, so a future denpa:// 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:

  • The link pattern matched (.+), 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.
  • v was 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-link registered, denpa:// declared for mobile (appLink: false) and desktop
  • An inbound denpa://enrol?… reaches the decoder cold-start and already-running
  • Verified by scanning a real code with the stock camera app on the Pixel and on the pad

The 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-http scheme — is answerable only by scanning a real code. If it will not, the recorded fallback is an unverified https:// 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 current main reads 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 typecheck exits 0.

> *This was generated by AI during triage.* **The 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_payload` emits `denpa://enrol?v=1&origin=…&code=…&fp=…` instead of bare JSON. A custom scheme needs no `.well-known/assetlinks.json` and 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): `scannedEnrollmentFrom` reads the link, and still reads a pasted JSON payload. Both shapes funnel through one `enrollmentFrom`, 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 to `enrol`, so a future `denpa://` 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: - The link pattern matched `(.+)`, 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. - `v` was 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-link` registered, `denpa://` declared for mobile (`appLink: false`) and desktop - [ ] An inbound `denpa://enrol?…` reaches the decoder cold-start and already-running - [ ] **Verified by scanning a real code with the stock camera app on the Pixel and on the pad** The 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-`http` scheme* — is answerable only by scanning a real code. If it will not, the recorded fallback is an unverified `https://` 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 current `main` reads 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 typecheck` exits 0.
Author
Owner

This was generated by AI during triage.

The wiring is done and merged. tauri-plugin-deep-link 2.4.9 is registered, denpa:// is declared for mobile with appLink: false and for desktop, and an inbound link reaches onScan — 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 in getCurrent().

Verified on the Pixel, not just in tests. The generated manifest carries exactly what this issue specified:

<intent-filter>
  <action android:name="android.intent.action.VIEW" />
  <category android:name="android.intent.category.DEFAULT" />
  <category android:name="android.intent.category.BROWSABLE" />
  <data android:scheme="denpa" />

No host, no verification — no assetlinks.json anywhere. Firing a real intent at a force-stopped app launched it:

am start -a android.intent.action.VIEW -d 'denpa://enrol?v=1&origin=…&code=…&fp='
→ topResumedActivity=works.aiko.ollvt.debug/works.aiko.ollvt.MainActivity

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 #37 records that a wrong enrolment is recoverable only by editing state by hand. Delivery into the decoder is covered by tests and by the shared onScan path, but has not been watched on a device that still needs enrolling.

What is left

  • Scan a real code with the stock camera app. Everything above is the app's side; whether the Pixel's scanner offers to open a non-http scheme is the one thing no amount of wiring answers. If it will not, the recorded fallback is an unverified https:// filter — a chooser instead of a straight-through, still no camera code.
  • A real enrolment through the link, on a device that is not yet enrolled.

New APK carrying this is installed on the Pixel (app-universal-debug.apk, arm64-v8a). Gate: 66 test files, 905 tests, npm run typecheck exits 0, cargo check passes.

> *This was generated by AI during triage.* **The wiring is done and merged.** `tauri-plugin-deep-link` 2.4.9 is registered, `denpa://` is declared for mobile with `appLink: false` and for desktop, and an inbound link reaches `onScan` — 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 in `getCurrent()`. **Verified on the Pixel, not just in tests.** The generated manifest carries exactly what this issue specified: ```xml <intent-filter> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <data android:scheme="denpa" /> ``` No host, no verification — no `assetlinks.json` anywhere. Firing a real intent at a force-stopped app launched it: ``` am start -a android.intent.action.VIEW -d 'denpa://enrol?v=1&origin=…&code=…&fp=' → topResumedActivity=works.aiko.ollvt.debug/works.aiko.ollvt.MainActivity ``` 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 `#37` records that a wrong enrolment is recoverable only by editing state by hand. Delivery into the decoder is covered by tests and by the shared `onScan` path, but has not been watched on a device that still needs enrolling. ## What is left - [ ] **Scan a real code with the stock camera app.** Everything above is the app's side; whether the Pixel's scanner *offers* to open a non-`http` scheme is the one thing no amount of wiring answers. If it will not, the recorded fallback is an unverified `https://` filter — a chooser instead of a straight-through, still no camera code. - [ ] A real enrolment through the link, on a device that is not yet enrolled. New APK carrying this is installed on the Pixel (`app-universal-debug.apk`, `arm64-v8a`). Gate: 66 test files, 905 tests, `npm run typecheck` exits 0, `cargo check` passes.
Author
Owner

This was generated by AI during triage.

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 to ready-for-agent if you would rather have the code waiting for you than the ticket.

> *This was generated by AI during triage.* 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 to `ready-for-agent` if you would rather have the code waiting for you than the ticket.
aiko referenced this issue from a commit 2026-08-07 08:57:16 +00:00
Author
Owner

Closed by scope change rather than by building a scanner. 6018580 lets the OS hand the scan over, so nothing here opens a camera: the phone's own camera app decodes and fires a denpa:// deep link, taken by 7e71cc6, bounded at the fragment by ed66a94, and routed to the screen that can use it by e72d59b. No tauri-plugin-barcode-scanner, no getUserMedia, so ADR-0002 stands.

Closed by scope change rather than by building a scanner. `6018580` lets the OS hand the scan over, so nothing here opens a camera: the phone's own camera app decodes and fires a `denpa://` deep link, taken by `7e71cc6`, bounded at the fragment by `ed66a94`, and routed to the screen that can use it by `e72d59b`. No `tauri-plugin-barcode-scanner`, no `getUserMedia`, so ADR-0002 stands.
aiko closed this issue 2026-08-10 22:51:47 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
aiko/denpa#60
No description provided.