docs(bf-29wyl): confirm token-supply path dead — header-only, unread
Verify the sim/header-side half of the tokenless-sim REJECT root cause against current source (second link of the bf-34lwt split). Three facts re-checked against current source: - Sim supplies its token ONLY as the X-Spaxel-Token HTTP header on the WS dial — cmd/sim/main.go:313, mothership/cmd/sim/main.go:633, mothership/cmd/sim/scenario.go:318 (all three connect paths). - The hello JSON map the sim sends has NO token field — cmd/sim/main.go:348-359, mothership/cmd/sim/main.go:652-665, mothership/cmd/sim/scenario.go:331-342. Only "--token" hits are CLI flag defs, never a JSON body key. - The mothership NEVER reads that header on the WS path — grep 'X-Spaxel-Token' mothership/ excluding _test.go and cmd/sim/ returns zero hits. HandleNodeWS (server.go:455-469) passes r straight to Upgrade; the only Header ref on the path is the outbound w.Header().Set response header. Conclusion: header ignored -> hello.Token (message.go:22) empty -> tokenOK (server.go:513) always false. The "have the sim supply valid tokens" option is currently DEAD. Companion to bf-5ig3e (validator half); both roll up into bf-34lwt. Co-Authored-By: Claude <noreply@anthropic.com> Bead-Id: bf-29wyl
This commit is contained in:
parent
3c5b79b791
commit
4dcc06af2b
1 changed files with 152 additions and 0 deletions
152
docs/notes/token-supply-path-dead-confirm.md
Normal file
152
docs/notes/token-supply-path-dead-confirm.md
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
# Confirmation: sim supplies its token only as the unread `X-Spaxel-Token` header → token-supply path is DEAD
|
||||
|
||||
**Tracking:** bead `bf-29wyl` (second link of the `bf-34lwt` split).
|
||||
**Date:** 2026-07-07.
|
||||
**Scope:** isolate and re-verify the *sim/header-side* half of the
|
||||
tokenless-sim REJECT root cause against current source. This confirms the
|
||||
three statements "the sim sets the token only as the `X-Spaxel-Token`
|
||||
header", "the hello JSON body has no `token` field", and "the mothership
|
||||
never reads that header on the WS path" — i.e. the **token-supply option is
|
||||
currently dead**. The validator-side half is covered by the sibling note
|
||||
[`token-validator-wiring-confirm.md`](./token-validator-wiring-confirm.md)
|
||||
(bead `bf-5ig3e`); both roll up into the parent
|
||||
[`token-reject-root-cause.md`](./token-reject-root-cause.md) (bead
|
||||
`bf-34lwt`). This note re-checks the three header-side facts that chain into
|
||||
"dead path", so a drift in any of them is detectable on its own.
|
||||
|
||||
> **Path convention.** Citations are full repo-relative paths. The `bf-29wyl`
|
||||
> task body drops the `mothership/` module prefix, so its `cmd/sim/main.go`
|
||||
> is `mothership/cmd/sim/main.go` here.
|
||||
>
|
||||
> **Two sim copies — both behave identically for this finding.**
|
||||
> - `cmd/sim/main.go` — the module `go.work` lists (`use ./cmd/sim`); the
|
||||
> dev/test path. This copy has no `scenario.go`.
|
||||
> - `mothership/cmd/sim/` — the copy the **Dockerfile actually builds** (it
|
||||
> does `COPY mothership/ ./` then `go build ./cmd/sim`, so `./cmd/sim`
|
||||
> resolves here).
|
||||
|
||||
## Fact 1 — the sim sets its token ONLY as the `X-Spaxel-Token` HTTP header
|
||||
|
||||
The token is attached to the WebSocket *dial* request as a request header in
|
||||
all three connect paths, and nowhere else:
|
||||
|
||||
- `cmd/sim/main.go:313`: `reqHeader.Set("X-Spaxel-Token", token)` — fed to
|
||||
`websocket.DefaultDialer.DialContext(ctx, u.String(), reqHeader)` (the dial
|
||||
a few lines below).
|
||||
- `mothership/cmd/sim/main.go:633`: `headers.Set("X-Spaxel-Token", token)`.
|
||||
- `mothership/cmd/sim/scenario.go:318`: `headers.Set("X-Spaxel-Token", token)`.
|
||||
|
||||
`token` here is the provisioned node token (auto-generated to a 64-hex dummy
|
||||
if `--token` is empty — `cmd/sim/main.go:120-125`,
|
||||
`mothership/cmd/sim/main.go:596-601`). It is supplied to the mothership
|
||||
exclusively via this HTTP header on the upgrade request. There is no second
|
||||
delivery channel.
|
||||
|
||||
## Fact 2 — the hello JSON body has NO `token` field
|
||||
|
||||
The hello map built and sent as the node's first WS frame omits `token`
|
||||
entirely:
|
||||
|
||||
- `cmd/sim/main.go:348-359`:
|
||||
```
|
||||
hello := map[string]interface{}{
|
||||
"type": "hello",
|
||||
"mac": macToString(n.MAC),
|
||||
"firmware_version": "sim-1.0.0",
|
||||
"capabilities": []string{"csi", "ble", "tx", "rx"},
|
||||
"chip": "ESP32-S3",
|
||||
"flash_mb": 16,
|
||||
"uptime_ms": 1000,
|
||||
"pos_x": n.Position.X,
|
||||
"pos_y": n.Position.Y,
|
||||
"pos_z": n.Position.Z,
|
||||
}
|
||||
```
|
||||
- `mothership/cmd/sim/main.go:652-665`: keys are
|
||||
`type, mac, firmware_version, capabilities, chip, flash_mb, uptime_ms,
|
||||
wifi_rssi, ip, pos_x, pos_y, pos_z`.
|
||||
- `mothership/cmd/sim/scenario.go:331-342`: keys are
|
||||
`type, mac, firmware_version, capabilities, chip, flash_mb, uptime_ms,
|
||||
wifi_rssi, ip`.
|
||||
|
||||
None of the three contains a `token` key. A targeted grep confirms it — the
|
||||
only `"token"` hits in each file are the `--token` *flag definition* (the CLI
|
||||
argument), never a JSON body key:
|
||||
|
||||
```
|
||||
$ grep -n '"token"' cmd/sim/main.go mothership/cmd/sim/main.go mothership/cmd/sim/scenario.go
|
||||
cmd/sim/main.go:54: flagToken = flag.String("token", "", "Provisioning token (auto-generated if empty)")
|
||||
mothership/cmd/sim/main.go:49: flagToken = flag.String("token", "", "Provisioning token (auto-generated if empty)")
|
||||
mothership/cmd/sim/scenario.go: (no "token" hit at all)
|
||||
```
|
||||
|
||||
`HelloMessage.Token` carries the JSON tag `json:"token,omitempty"`
|
||||
(`mothership/internal/ingestion/message.go:22`). Since the body never sets
|
||||
`token`, that field deserializes to `""` from either sim binary.
|
||||
|
||||
## Fact 3 — the mothership NEVER reads `X-Spaxel-Token` on the WS path
|
||||
|
||||
A grep for the literal header name across the entire mothership tree,
|
||||
excluding the sim sources that *set* it and the test files that document the
|
||||
contract, returns **zero** hits:
|
||||
|
||||
```
|
||||
$ grep -rn 'X-Spaxel-Token' mothership/ | grep -v '_test.go' | grep -v 'cmd/sim/'
|
||||
(no output; exit status 1)
|
||||
```
|
||||
|
||||
The only remaining references in the tree are the sim's own `Set(...)` calls
|
||||
and an e2e test *comment* that asserts this exact fact
|
||||
(`mothership/tests/e2e/e2e_test.go:86-88`: *"spaxel-sim nodes present no
|
||||
token in their hello message (only the X-Spaxel-Token header, which the
|
||||
ingestion server does not read)"*). There is no production code that reads
|
||||
it.
|
||||
|
||||
The WS upgrade handler inspects nothing off the incoming request either.
|
||||
`mothership/internal/ingestion/server.go:455-469`:
|
||||
|
||||
```go
|
||||
func (s *Server) HandleNodeWS(w http.ResponseWriter, r *http.Request) {
|
||||
...
|
||||
w.Header().Set("Content-Type", "application/json") // :463 — OUTBOUND response header
|
||||
...
|
||||
conn, err := s.upgrader.Upgrade(w, r, nil) // :469 — r passed through, never read for headers
|
||||
```
|
||||
|
||||
The sole `Header` reference on this path is `w.Header().Set(...)` on the
|
||||
*outbound* response (`:463`); `r` (the request bearing the `X-Spaxel-Token`
|
||||
header) is handed straight to `Upgrade` and never inspected. (Every other
|
||||
`Header.Get` / `Header.Set` hit under `mothership/internal/` is on an
|
||||
*outbound* webhook/notify path or in its tests — `webhook`, `notify`,
|
||||
`notifications/ntfy.go`, etc. — none touches the node WS path.)
|
||||
|
||||
## Conclusion — the token-supply path is currently DEAD
|
||||
|
||||
The validator reads only `hello.Token`
|
||||
(`mothership/internal/ingestion/server.go:513`:
|
||||
`tokenOK := hello.Token != "" && validator(hello.MAC, hello.Token)`), and
|
||||
that field comes only from the JSON body (`message.go:22`). Combining the
|
||||
three facts above:
|
||||
|
||||
- The sim supplies its token **only** as the `X-Spaxel-Token` HTTP header
|
||||
(Fact 1);
|
||||
- which the mothership **never reads** on the WS path (Fact 3);
|
||||
- and the hello body the mothership *does* read has **no `token`** field
|
||||
(Fact 2).
|
||||
|
||||
Therefore for every sim node the header is silently dropped, `hello.Token`
|
||||
is `""`, and `hello.Token != ""` short-circuits `tokenOK` to `false` at
|
||||
`server.go:513` **regardless of what token the sim provisioned**. The
|
||||
parent bead's "have the sim supply valid tokens" option is **currently
|
||||
NON-FUNCTIONAL** — the token is supplied to a header nobody reads.
|
||||
|
||||
> The token-supply path is dead: header ignored → `hello.Token` empty →
|
||||
> `tokenOK` always `false`.
|
||||
|
||||
This isolates the sim/header-side half of `bf-34lwt`. Combined with the
|
||||
validator-side facts in the sibling note (`bf-5ig3e`: the validator is
|
||||
always wired, and acceptance/rejection hinges entirely on `hello.Token`),
|
||||
it follows that a sim node is accepted only while the migration window is
|
||||
open (the default 24h mask), then `sendReject`-ed with `invalid_token`
|
||||
(`server.go:519-528`) once it closes — because the supplied token never
|
||||
reaches the validator at all.
|
||||
Loading…
Add table
Reference in a new issue