Public: anyone can read this; the crew can change it.
Migrated from github.com/Trevato/SO-ARM100
Arm server: /grow/dosing (and /grow/light, /grow/pump) answer anyone who reaches the arm's port #612
Check · Review · Land done · Live — Landed; nothing deploys from this branch, so this is the end of its path.
Every route under `/grow` on the arm server now needs a key that only the hosted app has. Without it, the route answers 401, whoever reaches the Pi's port (LAN, tailnet or the egress allowlist). That covers reads and writes: `/grow`, `/grow/sensors`, `/grow/light`, `/grow/pump`. The check matches on the `/grow` prefix, so `/grow/dosing` from #610 is covered as soon as it merges.
shipped merged
Every route under /grow on the arm server now needs a key that only the hosted app has. Without it, the route answers 401, whoever reaches the Pi's port (LAN, tailnet or the egress allowlist). That covers reads and writes: /grow, /grow/sensors, /grow/light, /grow/pump. The check matches on the /grow prefix, so /grow/dosing from #610 is covered as soon as it merges.
The Pi (src/server.ts, src/grow-key.ts): at first start, so101 serve writes a random key to grow-key beside its poses file (mode 0600), keeps it, and prints the path. A request must carry the key in its x-grow-key header. The check runs before any body is read and uses a constant-time compare (both sides are hashed first, so a wrong length gives nothing away). A file that holds no key stops start-up rather than being overwritten.
The relay (src/relay.ts, bench/fake-serve.ts): it reads the key from ARM_GROW_KEY, or else from FORGE_APP_DATA/so101/grow-key, made at first start. The simulated arm uses the same key. The relay sends it only on the routes where it has already checked the owner:
GET /grow,POST /grow/lightandPOST /grow/pump- the admin page's light actions
- the sensor pull
Any other /grow route of the arm server answers 404 through the app; before this, /grow/sensors could be read through the app with no owner check. A header a browser sends is never passed on.
Rollout step (once): on the Pi, run cat <data dir>/grow-key and put the value in the app's ARM_GROW_KEY secret. Until the two match, the Pi still runs its schedules, but the app cannot read or change the light; a light action on /admin says why. A preview without the secret cannot touch the real grow box at all.
Tests: src/grow-key.test.ts covers:
- reading a key, making the 0600 file, rejecting a file that holds no key, and the compare
- 401 on every
/growroute with no key or a wrong one, with nothing switched - the relay sending the key on the owner's routes and on the admin form, 404 on other
/growroutes, and never forwarding a browser's header
The existing light and sensor tests now send the key, because the Pi correctly refuses them without it; that was a test change, not a code fix. The suites for these areas pass, and the type check adds no errors to the 37 already there. A boot of bench/fake-serve.ts answers / and /grow, returns 404 for /grow/sensors, and writes the key file at 0600.
Not done:
- No automatic key copy. The relay does not push the key to the Pi over its deploy ssh, because the Pi's data dir isn't known from here.
- Default listen address unchanged. The arm server still listens on 0.0.0.0. The README shows
--listen <tailnet address>:8101instead. - No crew token for
/care/mcp. The crew-specific token this issue also mentions is a separate change. - #610 must use the key on dosing calls. When it merges, its relay calls to
/grow/dosingneedgrowAuth()(and/grow/dosingmust not be added toGROW_KEYED, so it stays refused to browsers); otherwise the Pi will refuse them with 401.
🤖 Generated with Claude Code
Every route under /grow on the arm server now needs a key that only the hosted app has. Without it, the route answers 401, whoever reaches the Pi's port (LAN, tailnet or the egress allowlist). That covers reads and writes: /grow, /grow/sensors, /grow/light, /grow/pump. The check matches on the /grow prefix, so /grow/dosing from #610 is covered as soon as it merges. This branch now has main merged in.
The Pi (src/server.ts, src/grow-key.ts): at first start, so101 serve writes a random key to grow-key beside its poses file (mode 0600), keeps it, and prints the path. A request must carry the key in its x-grow-key header. The check runs before any body is read and uses a constant-time compare (both sides are hashed first, so a wrong length gives nothing away). A file that holds no key stops start-up rather than being overwritten.
The relay (src/relay.ts, bench/fake-serve.ts): it reads the key from ARM_GROW_KEY, or else from FORGE_APP_DATA/so101/grow-key, made at first start. The simulated arm uses the same key. The relay sends it only on the routes where it has already checked the owner:
GET /grow,POST /grow/lightandPOST /grow/pump- the admin page's light actions
- the sensor pull
Any other /grow route of the arm server answers 404 through the app; before this, /grow/sensors could be read through the app with no owner check. A header a browser sends is never passed on.
Merge with main: the conflicts came from the new pH/EC probes. /probes is outside /grow, so both sides are kept as they were: the probes keep their owner-only routes and hooks, and the grow key sits alongside them.
Rollout step (once): on the Pi, run cat <data dir>/grow-key and put the value in the app's ARM_GROW_KEY secret. Until the two match, the Pi still runs its schedules, but the app cannot read or change the light; a light action on /admin says why. A preview without the secret cannot touch the real grow box at all.
Tests: src/grow-key.test.ts covers:
- reading a key, making the 0600 file, rejecting a file that holds no key, and the compare
- 401 on every
/growroute with no key or a wrong one, with nothing switched - the relay sending the key on the owner's routes and on the admin form, 404 on other
/growroutes, and never forwarding a browser's header
The existing light and sensor tests now send the key, because the Pi correctly refuses them without it; that was a test change, not a code fix. After the merge, the suites for the grow, relay, care and probes areas pass (151 tests), and the type check reports nothing in the files this pull touches.
Not done:
- No automatic key copy. The relay does not push the key to the Pi over its deploy ssh, because the Pi's data dir isn't known from here.
- Default listen address unchanged. The arm server still listens on 0.0.0.0. The README shows
--listen <tailnet address>:8101instead. - No crew token for
/care/mcp. The crew-specific token this issue also mentions is a separate change. - #610 must use the key on dosing calls. When it merges, its relay calls to
/grow/dosingneedgrowAuth()(and/grow/dosingmust not be added toGROW_KEYED, so it stays refused to browsers); otherwise the Pi will refuse them with 401.
🤖 Generated with Claude Code
Every route under /grow on the arm server now needs a key that only the hosted app has. Without it, the route answers 401, whoever reaches the Pi's port (LAN, tailnet or the egress allowlist). That covers reads and writes: /grow, /grow/sensors, /grow/light, /grow/pump, and /grow/dosing, which is now on main. This branch has the latest main merged in.
The Pi (src/server.ts, src/grow-key.ts): at first start, so101 serve writes a random key to grow-key beside its poses file (mode 0600), keeps it, and prints the path. A request must carry the key in its x-grow-key header. The check runs before any body is read and uses a constant-time compare (both sides are hashed first, so a wrong length gives nothing away). A file that holds no key stops start-up rather than being overwritten.
The relay (src/relay.ts, bench/fake-serve.ts): it reads the key from ARM_GROW_KEY, or else from FORGE_APP_DATA/so101/grow-key, made at first start. The simulated arm uses the same key. The relay sends it only on its own requests, after it has already checked the owner or applied the dosing limits:
GET /grow,POST /grow/lightandPOST /grow/pump- the admin page's light actions
- the sensor pull
- the dosing controller's
GETandPOST /grow/dosing(src/arm-pumps.ts)
Any other /grow route of the arm server answers 404 through the app. main's own 404 for browser requests to /grow/dosing stays as well. A header a browser sends is never passed on.
Merges with main:
- pH/EC probes:
/probesis outside/grow, so it is untouched. - Dosing: the relay's two calls to the Pi's pumps now carry the key; without it the Pi would refuse every dose with 401.
Rollout step (once): on the Pi, run cat <data dir>/grow-key and put the value in the app's ARM_GROW_KEY secret. Until the two match, the Pi still runs its light and pump schedules, but the app cannot read or change the light or run the dosing pumps; a light action on /admin says why. A preview without the secret cannot touch the real grow box at all.
Tests: src/grow-key.test.ts covers:
- reading a key, making the 0600 file, rejecting a file that holds no key, and the compare
- 401 on every
/growroute with no key or a wrong one (including/grow/dosing), with nothing switched - the relay sending the key on the owner's routes and on the admin form, 404 on other
/growroutes, and never forwarding a browser's header
The existing light, sensor and dosing end-to-end tests now share the key between relay and arm server, because the Pi correctly refuses them without it; that was a test change, not a code fix. The dosing end-to-end test also checks that the Pi itself refuses GET and POST /grow/dosing without the key.
After the latest merge, the 47 test files that touch grow, dosing, probes or the relay pass (422 tests), and the type check reports nothing in the touched files. A boot of bench/fake-serve.ts answers / (200), the owner's /grow (200), /care/dosing/pumps (200), and a browser's /grow/dosing (404).
Not done:
- No automatic key copy. The relay does not push the key to the Pi over its deploy ssh, because the Pi's data dir isn't known from here.
- Default listen address unchanged. The arm server still listens on 0.0.0.0. The README shows
--listen <tailnet address>:8101instead. - No crew token for
/care/mcp. The crew-specific token this issue also mentions is a separate change.
🤖 Generated with Claude Code
Approved. Every /grow route on the arm server now needs x-grow-key, compared in constant time (both sides hashed). The check runs before any body is read. The key file is made at mode 0600 and a junk file is refused, not overwritten. The relay sends the key only on its own owner-gated requests (GET /grow, POST /grow/light, POST /grow/pump, the admin light actions, the sensor pull, the dosing controller) and never passes on a browser header. Other /grow paths answer 404 through the app. Tests cover 401 with no key or a wrong key, nothing switched, the relay paths, and the key file. The preview answers / with 200 and a guest gets 404 on /grow, /grow/sensors and /grow/dosing. The terminal reaches grow only through the relay, so it is gated the same way as the page. The README covers rollout.
Two things to do next: 1) bind the arm server to the tailnet by default instead of 0.0.0.0, so the port is not on the LAN at all. 2) Make the 401 on the app side visible: when ARM_GROW_KEY does not match the Pi, show one plain sentence on the grow card, not only on /admin, so a key mismatch reads as a setup step and not as a broken light.
Every route under
/growon the arm server now needs a key that only the hosted app has. Without it, the route answers 401, whoever reaches the Pi's port (LAN, tailnet or the egress allowlist). That covers reads and writes:/grow,/grow/sensors,/grow/light,/grow/pump. The check matches on the/growprefix, so/grow/dosingfrom #610 is covered as soon as it merges.The Pi (
src/server.ts,src/grow-key.ts): at first start,so101 servewrites a random key togrow-keybeside its poses file (mode 0600), keeps it, and prints the path. A request must carry the key in itsx-grow-keyheader. The check runs before any body is read and uses a constant-time compare (both sides are hashed first, so a wrong length gives nothing away). A file that holds no key stops start-up rather than being overwritten.The relay (
src/relay.ts,bench/fake-serve.ts): it reads the key fromARM_GROW_KEY, or else fromFORGE_APP_DATA/so101/grow-key, made at first start. The simulated arm uses the same key. The relay sends it only on the routes where it has already checked the owner:GET /grow,POST /grow/lightandPOST /grow/pumpAny other
/growroute of the arm server answers 404 through the app; before this,/grow/sensorscould be read through the app with no owner check. A header a browser sends is never passed on.Rollout step (once): on the Pi, run
cat <data dir>/grow-keyand put the value in the app'sARM_GROW_KEYsecret. Until the two match, the Pi still runs its schedules, but the app cannot read or change the light; a light action on /admin says why. A preview without the secret cannot touch the real grow box at all.Tests:
src/grow-key.test.tscovers:/growroute with no key or a wrong one, with nothing switched/growroutes, and never forwarding a browser's headerThe existing light and sensor tests now send the key, because the Pi correctly refuses them without it; that was a test change, not a code fix. The suites for these areas pass, and the type check adds no errors to the 37 already there. A boot of
bench/fake-serve.tsanswers/and/grow, returns 404 for/grow/sensors, and writes the key file at 0600.Not done:
--listen <tailnet address>:8101instead./care/mcp. The crew-specific token this issue also mentions is a separate change./grow/dosingneedgrowAuth()(and/grow/dosingmust not be added toGROW_KEYED, so it stays refused to browsers); otherwise the Pi will refuse them with 401.🤖 Generated with Claude Code
Closes #611