design: correct two claims the shipped relay made false

The phase 1 doc still said 'not yet implemented', and pointed at a
TagsSection path the frontend did not use.

More worth fixing: both documents offered an unguessable reader_id as a
weak secret for people who care. The reader registry that shipped
alongside undoes that entirely, since GET /api/v1/tag/reader hands every
recently-seen reader id to any caller by design, so the picker can list
them. Offering obscurity that the same API gives away is worse than
offering none.
This commit is contained in:
Donkie committed 2026-08-17 21:36:48 +02:00
1 parent 6d97cd34b3
commit 7e2e98b30a
2 files changed
+16 -6

No files matched your search

+11 -5
View File
@@ -1,6 +1,8 @@
# NFC Phase 1 — Tag identity, lookup, and the scan relay
Status: design, not yet implemented.
Status: implemented. This document is the design as built, kept for the reasoning behind the
decisions rather than as a plan; the endpoint reference that users need lives in
[`wiki-tag-scanners.md`](./wiki-tag-scanners.md) and the generated API docs.
Companion document: [`nfc-phase-2-openprinttag.md`](./nfc-phase-2-openprinttag.md).
---
@@ -353,9 +355,13 @@ Guard rails, all of which matter:
Spoolman ships with no authentication. Any device on the LAN can POST a scan and make paired
browsers navigate. The blast radius is navigation only — no data is written by a scan — but it
should be stated plainly in the docs rather than discovered. An unguessable `reader_id` acts as a
weak shared secret for users who care; do not present it as a real one. This is consistent with the
existing threat model, where anyone on the LAN can already `DELETE` a spool.
should be stated plainly in the docs rather than discovered. This is consistent with the existing
threat model, where anyone on the LAN can already `DELETE` a spool.
An earlier draft of this section offered an unguessable `reader_id` as a weak shared secret for
users who care. Shipping the reader registry killed that idea and the docs should not repeat it:
`GET /api/v1/tag/reader` hands every recently-seen reader id to any caller, because that is what
the "choose a reader" picker reads. The obscurity is worth nothing, so do not offer it.
---
@@ -609,7 +615,7 @@ builds the response. No background task, no lifecycle hook, nothing to shut down
| `client_v2/src/lib/api/scanRelay.ts` | The scan websocket client: connect, resubscribe, reconnect. |
| `client_v2/src/lib/stores/scanner.svelte.ts` | Paired `reader_id`, auto-navigate toggle, most recent scan. localStorage-backed, following `theme.svelte.ts`. |
| `client_v2/src/lib/utils/nfc.ts` | Web NFC wrapper plus the `'NDEFReader' in window` capability check, so no component touches the API directly. |
| `client_v2/src/lib/components/library/TagsSection.svelte` | The inspector's tag list and unlink control, modeled on `ExtraFieldsSection.svelte`. |
| `client_v2/src/lib/components/TagsSection.svelte` | The inspector's tag list and unlink control, modeled on `ExtraFieldsSection.svelte`. |
| `client_v2/src/lib/components/AddTagModal.svelte` | Link a tag by relay scan, by Web NFC, or by typing a UID. |
| `client_v2/src/lib/components/settings/ScannerSettings.svelte` | Pair by tap, show current pairing, unpair, auto-navigate toggle. |
+5 -1
View File
@@ -484,7 +484,11 @@ never sees a de-duplicated scan as a failed lookup.
**There is no authentication.** Spoolman has none, and the tag endpoints are the same. Anything on
your network can report a scan and navigate a paired browser. Worst case, it moves someone's page.
Scans don't write. Keep Spoolman off the open internet, as the [security page](https://github.com/Donkie/Spoolman/wiki/Security) advises. An unguessable `reader_id` makes it harder to target a specific browser. It's not a password.
Scans don't write. Keep Spoolman off the open internet, as the [security page](https://github.com/Donkie/Spoolman/wiki/Security) advises.
Do not treat a hard-to-guess `reader_id` as any kind of secret. `GET /api/v1/tag/reader` lists every
reader that has scanned recently, by id and name, to anyone who can reach the API, which is what
makes the "choose a reader" picker work. Guessing is not required.
**Reader state is per server process.** The recently-seen list and de-duplication window live in
memory. If you run Spoolman with multiple workers, each has its own, and readers come and go. The