- Security (2-model, security+correctness): the public payload zeroed
Garden.OwnerID but referenced custom plants still carried Plant.OwnerID,
leaking the owner's user id to anonymous viewers. Redact plant owner ids
too, and assert it in the service test.
- Set Cache-Control: no-store on the public read so a capability-URL response
isn't held in a shared proxy cache and always reflects the live garden.
- createShareLink now 400s on a present-but-malformed JSON body instead of
silently enabling (an empty body still means enable-without-rotate).
- Extract the transient-editor-state reset into a shared resetTransient store
action (3-model finding) and use it from PublicGardenPage.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01JdQpdYYsTgtkJBxbcpAszi
A per-garden public link anyone can open read-only, with no login and no OIDC.
Backend:
- Migration 0004 adds gardens.public_token (nullable, unique over non-NULL).
- Owner-only management: GET/POST/DELETE /gardens/:id/share-link (state /
enable-or-rotate / disable). The token is stored raw (it must be shown back
to the owner) and never selected into the normal garden payload.
- Unauthenticated GET /api/v1/public/gardens/:token returns the read-only /full
payload — the token is the capability, so no requireAuth and no redirect. The
public view drops MyRole and OwnerID to minimize what an anonymous viewer
learns. Unknown/rotated/disabled tokens are masked as 404.
- assembleFull is extracted from GardenFull so both reads return one shape.
Frontend:
- /g/$token route with NO auth guard (SPA fallback already serves it), rendering
GardenCanvas read-only via usePublicGarden.
- Share dialog gains a Public link section: create, copy, regenerate, turn off.
Tests: service lifecycle + ACL (owner-only, non-owner masked, rotate/disable
invalidation) and the HTTP surface incl. the cookieless public read.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01JdQpdYYsTgtkJBxbcpAszi