**Note** from Bead: Android Photo Sync · [canonical source](https://redfish.acequia.io/guerin/.agents/06b7373f-2ba9-46c5-88bb-31bd93ce453f/2026-06-27/notes/00-photos-endpoint.md) · session 2026-06-27 · discussion: Talk: Android Photo Sync
> **Refactored 2026-06-27:** the policy below now lives in a **drop-in handler** > [`stephenguerin.live/photos/handler.mjs`](c:/Users/steph/Documents/sites/stephenguerin.live/photos/handler.mjs) > + [`config.json`](c:/Users/steph/Documents/sites/stephenguerin.live/photos/config.json), dispatched by > the generic filesystem router in [notes/01-filesystem-router.md](01-filesystem-router.md). It is no > longer an inline lane in `server.js`. The contract (verbs/auth/guards) is unchanged; `PHOTOS_ROOT` > is now simply the handler's own dir (`WEB_ROOT/photos`), and the token comes from `config.json`'s > `auth.tokenFile` (resolved by the runtime). The original inline-lane description is kept below for history. The generic bead runtime is the NSSM service `stephenguerin-live-origin`, run as `node server.js <redfish-bead-82bd6fa4> 3500`, behind Caddy TLS.
## Why a separate lane (not the bead subtree) The live server serves everything from `BEAD_DIR` = the redfish bead `82bd6fa4-...` via `safeBeadPath` (confined to that subtree). Photos must **not** land in the redfish commons (they'd sync as provenance). So `/photos` is intercepted **before** the bead handler and routed to a dedicated root that is a sibling tree, not part of any bead.
## Config (env-overridable) - `PHOTOS_ROOT` — default `c:/Users/steph/Documents/sites/stephenguerin.live/photos` - `PHOTOS_TOKEN_FILE` — default `c:/Users/steph/Documents/sites/.credentials/stephenguerin-photos-token.txt` (read once at startup into `PHOTOS_TOKEN`)
## Contract | Method | Path | Auth | Behavior | |---|---|---|---| | `GET`/`HEAD` | `/photos/**` | none | serve file from `PHOTOS_ROOT` (dir → `index.html`); 404 if absent | | `PUT` | `/photos/<dest>/<name>` | `Authorization: Bearer <token>` | binary-safe write to `PHOTOS_ROOT/<dest>/<name>`, 201 | | `OPTIONS` | `/photos/**` | none | 204 + `Allow` + permissive CORS | | other | `/photos/**` | — | 405 | Guards: `safePhotosPath` rejects any normalized path escaping `PHOTOS_ROOT` (403); `PUT` over an `index.html` or the root itself is 403; missing token file → 503. Binary integrity comes from a dedicated `readBodyRaw` (Buffer), separate from the runtime's utf8 `readBody` (which would corrupt image bytes).
## Verified 2026-06-27 (throwaway instance on :3599, temp PHOTOS_ROOT) 401 no/bad token · 201 valid token · GET round-trip byte-identical · 403 `../` escape · 403 over `index.html` · 204 OPTIONS. **Live :3500 still runs pre-edit code** until an elevated restart: ```powershell # run elevated Restart-Service stephenguerin-live-origin # verify the new lane is live: curl.exe -s -o NUL -w "%{http_code}\n" https://stephenguerin.live/photos/ ```
## Token Stored in [`.credentials/stephenguerin-photos-token.txt`](c:/Users/steph/Documents/sites/.credentials/stephenguerin-photos-token.txt). Rotate the file (and restart) to revoke. Future: per-device attenuated chain-tokens instead of one shared secret (see acequia token-minting pattern).