Add per-artifact public sharing

Any artifact can now be made publicly viewable via a share link, no login
required - opt-in per artifact, toggled from the feed UI next to Keep.

- Artifact.share_token: nullable, unique, high-entropy (secrets.token_urlsafe(32)).
  NULL = not shared; SQL treats each NULL as distinct so uniqueness still
  holds for the many not-shared rows.
- New public route GET /s/<token> (shared_artifact_view), completely
  unauthenticated, serving the same rendered/raw content as the owner
  route via a shared serve_artifact_file() helper.
- toggle_artifact_share: generates/clears the token: mirrors
  toggle_artifact_keep's pattern.
- Fixed a real gap this surfaced: embedded images in a shared artifact's
  content would otherwise break for an anonymous visitor, since asset_view
  was owner-only. It now also allows access when the asset's parent
  artifact is currently shared - unattached (orphaned) assets stay private
  regardless.
- serialize_artifact now reports is_shared/share_url so the API reflects
  sharing status (read-only for now; toggling stays UI-only).
- Feed UI: Share button (mirrors Keep's toggle interaction), a copyable
  share-link box that appears when shared, and a "shared" badge on the
  artifact row - distinct blue accent from Keep's green so both can be
  visible on the same row without confusion.

Verified live end-to-end (toggle on/off, public access, fake-token 404,
old-link-dies-on-unshare, embedded-asset access before/after sharing)
before writing the 12 automated tests in tests/views/test_sharing.py.
Also fixed two unrelated stale spots noticed in docs/api.md while adding
the Sharing section: the example response was missing is_kept/size_bytes,
and the raw-content table didn't list json/csv (added earlier this
session but never documented there).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
peter 2026-09-02 16:00:39 +01:00
parent e137725cab
commit 91ea19d26b
10 changed files with 414 additions and 4 deletions

View file

@ -196,7 +196,11 @@ Response shape:
"model": "gpt-5.5",
"run_id": "brief-001"
},
"read_at": null
"read_at": null,
"is_kept": false,
"is_shared": false,
"share_url": null,
"size_bytes": 702
}
```
@ -429,9 +433,35 @@ Raw currently exists for:
|---|---|
| `markdown` | Preserved `.md` source. |
| `html_full` | Stored HTML file served as text. |
| `json` | Preserved original JSON text. |
| `csv` | Preserved original CSV text. |
If raw content is unavailable, Daybook returns a small HTML fallback page.
## Sharing
Any artifact can be made publicly viewable — no login or token required —
via a per-artifact share link. This is toggled from the feed UI (a "Share"
button next to "Keep"), not through the ingest API; there's no `PATCH`
field to set it programmatically yet.
```http
GET /s/{share_token}
```
- `share_token` is a high-entropy random string (unrelated to the artifact's
own `id`), generated when sharing is turned on and cleared when turned
off — turning sharing off immediately invalidates the old link.
- The public route serves the same rendered content as `GET /a/{id}`
(including `?raw=1`), but with no authentication check at all — anyone
with the link can view it.
- Images embedded in a shared artifact's content are also served
unauthenticated while that artifact is shared, so inline images don't
break for a visitor without a Daybook account.
- `is_shared` and `share_url` (both in every artifact response — see
`GET /api/artifacts/{id}`) report current sharing status; `share_url` is
`null` when not shared.
## Capabilities
Endpoint: