capsite: social.coop tree at the ActivityPub level (fedi-cache)
Mastodon is one implementation; the cache speaks the protocol: outbox for backfill, and fedi-cache as a follower actor so Update/Delete are delivered rather than polled for. Its own keypair replaces an access token, so webapp drops out of this one. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
99ce5b6964
commit
1e6436e089
1 file changed
+78
-72
+78
-72
@@ -4,84 +4,90 @@ The same conventions as [the diigo goal tree](capsite-checklist.md): each
|
|||||||
`- [ ]` is a goal, its indented children must pass first, working notes
|
`- [ ]` is a goal, its indented children must pass first, working notes
|
||||||
sit under each goal.
|
sit under each goal.
|
||||||
|
|
||||||
- [ ] **madmode's weekly roundup includes my social.coop activity**
|
- [ ] **madmode's weekly roundup includes my fediverse activity**
|
||||||
([#23](https://github.com/dckc/madmode-blog/issues/23), PESOS):
|
([#23](https://github.com/dckc/madmode-blog/issues/23), PESOS):
|
||||||
a cache of [@dckc@social.coop](https://social.coop/@dckc) — posts,
|
`fedi-cache` holds the posts and boosts of
|
||||||
boosts, and (with discretion) bookmarks and favourites; the access
|
[@dckc@social.coop](https://social.coop/@dckc), speaking
|
||||||
token stays inside webapp, so madmode holds no credential
|
ActivityPub only — no server-specific API, so the cache works
|
||||||
|
against any server and survives an account move. social.coop
|
||||||
|
grants it nothing; madmode holds no credential
|
||||||
|
|
||||||
- [ ] **0. Decide the shape** (user's call before code):
|
- [ ] **0. Decide the shape** (user's call before code):
|
||||||
- [ ] 0-i. New worker `masto-cache/` beside `diigo-cache/`, or
|
- [ ] 0-i. A new worker `fedi-cache/` beside `diigo-cache/`;
|
||||||
generalize diigo-cache? Recommendation: a new worker;
|
extract shared pieces (date window/`q` query module, fill
|
||||||
extract shared pieces once two caches work.
|
job, webkey page) once two caches work.
|
||||||
- [ ] 0-ii. Which feeds? Public posts and boosts need no
|
- [ ] 0-ii. Pull, push, or both? **Pull**: read the actor's
|
||||||
credential (`GET /api/v1/accounts/:id/statuses`, or the
|
`outbox` (an `OrderedCollection` of `Create` and
|
||||||
ActivityPub outbox). Bookmarks, favourites, and
|
`Announce`) — public posts only. **Push**: fedi-cache is
|
||||||
followers-only posts need a token. Bookmarks are private
|
itself an actor that follows @dckc; social.coop delivers
|
||||||
by nature: publish them only behind an explicit opt-in
|
`Create`/`Update`/`Delete`/`Announce` to its inbox,
|
||||||
(what counts as opting in is open).
|
including followers-only posts once the follow is
|
||||||
- [ ] 0-iii. Mastodon API or ActivityPub? The API is simpler
|
approved. Recommendation: both — outbox for backfill,
|
||||||
and covers bookmarks/favourites; ActivityPub works
|
inbox for freshness.
|
||||||
against any server (portability if the account moves).
|
- [ ] 0-iii. What's out of reach at the protocol level: likes
|
||||||
Recommendation: the API now, keyed by the post's
|
(`Like` goes to the post's author, not to followers, and
|
||||||
ActivityPub `uri` so a later move keeps ids stable.
|
Mastodon serves no `liked` collection) and bookmarks (not
|
||||||
|
ActivityPub at all). Accept the gap, or take them from
|
||||||
|
the account archive (`likes.json`, `bookmarks.json`) as a
|
||||||
|
manual import.
|
||||||
|
|
||||||
- [ ] **1. Credential discipline**: a token from social.coop →
|
- [ ] **1. Authority**: fedi-cache's own actor keypair, generated in
|
||||||
Preferences → Development → New application, scopes
|
its DO and never exported; HTTP Signatures on outgoing GETs
|
||||||
**read-only and narrow** (`read:statuses read:accounts
|
(servers in authorized-fetch mode require them) and on the
|
||||||
read:bookmarks read:favourites`, per 0-ii) — attenuation at
|
`Follow`. The authority it holds is exactly what @dckc grants by
|
||||||
the source. It rides as `Authorization: Bearer`, which webapp's
|
approving the follow, and removing the follower revokes it. No
|
||||||
mint form already blanks. Token → webapp DO storage only; ids
|
token, no cookie, so webapp isn't needed. Ids → password manager,
|
||||||
→ password manager. Revoking the app on social.coop is the
|
as with diigo.
|
||||||
kill switch.
|
|
||||||
|
|
||||||
- [ ] **2. The cache** (`masto-cache/`, the 1c pattern from diigo):
|
- [ ] **2. Pull: the outbox cache**:
|
||||||
- [ ] 2-i. Mint and wire against `https://social.coop/api/`;
|
- [ ] 2-i. Resolve the actor: WebFinger
|
||||||
`accounts/lookup?acct=dckc` gives the account id.
|
(`/.well-known/webfinger?resource=acct:dckc@social.coop`)
|
||||||
- [ ] 2-ii. Storage: one table per feed (statuses, bookmarks,
|
→ actor document → `outbox`.
|
||||||
favourites), keyed by `uri`, storing the status JSON and
|
- [ ] 2-ii. Storage: activities and objects keyed by their
|
||||||
its `edited_at`. **Durable state** from day one: a
|
`id` URI, objects with `updated`. **Durable state** from
|
||||||
`SCHEMA_V1` step.
|
day one: a `SCHEMA_V1` step.
|
||||||
- [ ] 2-iii. Fill: page backward with `max_id` (40 per page)
|
- [ ] 2-iii. Fill: follow the collection's `first`/`next` pages
|
||||||
following the `Link` header — bookmarks and favourites
|
with `Accept: application/activity+json`; politeness
|
||||||
paginate *only* by `Link` (their cursors aren't status
|
delay per request; back off on 429.
|
||||||
ids). Honor `X-RateLimit-Remaining`/`-Reset` (300 per
|
|
||||||
5 minutes is Mastodon's default).
|
|
||||||
- [ ] 2-iv. Tests: `pnpm lint && pnpm test`, the real worker
|
- [ ] 2-iv. Tests: `pnpm lint && pnpm test`, the real worker
|
||||||
against a fake Mastodon, including `Link` pagination and
|
against a fake ActivityPub server (WebFinger, actor,
|
||||||
a rate-limit response.
|
paged outbox, a 429).
|
||||||
- [ ] 2-v. Complete: a filled cache matches the account archive
|
- [ ] 2-v. Complete: a filled cache matches the account
|
||||||
(Preferences → Import and export → Request your archive:
|
archive's `outbox.json` — the archive is itself an
|
||||||
`outbox.json`, `likes.json`, `bookmarks.json`), the
|
ActivityPub collection, so the comparison stays at the
|
||||||
analogue of diigo's `scripts/compare-export.ts`.
|
protocol level (the analogue of diigo's
|
||||||
|
`scripts/compare-export.ts`).
|
||||||
|
|
||||||
- [ ] **3. Freshness**: new posts are cheap (`min_id` = newest
|
- [ ] **3. Push: fedi-cache as a follower**:
|
||||||
stored, page forward). Edits and deletions of older posts are
|
- [ ] 3-i. Serve its own actor: WebFinger, actor document with
|
||||||
not announced to an API poller; options: re-fetch the last N
|
`publicKey`, `inbox`. Needs a public HTTPS origin, so dev
|
||||||
days on each refresh (the window a roundup cares about), or
|
wants a tunnel or a deployed test instance (and the user
|
||||||
follow the streaming API. A test: a post added, one edited, and
|
runs it, per AGENTS.md).
|
||||||
one deleted in the fake; the weekly window reflects all three.
|
- [ ] 3-ii. Send `Follow`; record `Accept`.
|
||||||
A cron alarm in the cache DO drives it.
|
- [ ] 3-iii. Inbox: verify the HTTP Signature against the
|
||||||
|
sender's key, accept only activities whose `actor` is
|
||||||
|
@dckc, apply `Create`/`Update`/`Delete`/`Announce`/`Undo`.
|
||||||
|
- [ ] 3-iv. Tests: signed deliveries in; a forged one and one
|
||||||
|
from another actor rejected; an edit and a delete reflected.
|
||||||
|
|
||||||
- [ ] **4. Read surfaces**: `/public/dckc` with the date window and
|
- [ ] **4. Read surfaces**: `/public/dckc` with diigo 1d's date
|
||||||
`q` filter of diigo's 1d (the same query module, if 0-i shares
|
window and `q` filter, HTML and JSON; JSON shape pinned by a
|
||||||
it), HTML and JSON; JSON shape pinned by a test. Serve only
|
test. Only objects addressed to `as:Public` appear there:
|
||||||
`visibility: public` posts (and opted-in bookmarks) there —
|
followers-only posts (which only push brings in) never reach a
|
||||||
followers-only and direct posts never reach a public page;
|
public page; a test says so.
|
||||||
a test says so.
|
|
||||||
|
|
||||||
- [ ] **5. madmode's weekly page includes it**: diigo's goal 4
|
- [ ] **5. madmode's weekly page includes it**: diigo's goal 4
|
||||||
(`/bookmarks/2026-W41`) grows a section from this cache, or
|
(`/bookmarks/2026-W41`) grows a section from this cache, or
|
||||||
becomes a combined `/weekly/2026-W41`. Same editorial question
|
becomes a combined `/weekly/2026-W41`. Same editorial question
|
||||||
as there: raw list, or a draft the user edits.
|
as there: a raw list, or a draft the user edits.
|
||||||
|
|
||||||
- [ ] **6. Deployed**: after diigo's goal 5; rate-limit `/public/*`;
|
- [ ] **6. Deployed**: rate-limit `/public/*`; the actor's origin is
|
||||||
mint against prod; fill.
|
its identity (moving it changes the follower), so choose the
|
||||||
|
hostname once.
|
||||||
|
|
||||||
- [ ] **7. Token revocation**: when social.coop rejects the token
|
- [ ] **7. Revocation**: if the follow is removed or deliveries stop,
|
||||||
(401), refreshes fail; the public page keeps serving what's
|
the public page keeps serving what's stored and says when it
|
||||||
stored and says so. Re-mint is manual; same re-wire question
|
last heard from @dckc; outbox polling still covers public posts.
|
||||||
as diigo's goal 6.
|
|
||||||
|
|
||||||
## How to run
|
## How to run
|
||||||
|
|
||||||
@@ -96,12 +102,12 @@ config types.
|
|||||||
|
|
||||||
## Pointers
|
## Pointers
|
||||||
|
|
||||||
- [Mastodon API: accounts](https://docs.joinmastodon.org/methods/accounts/),
|
- [ActivityPub](https://www.w3.org/TR/activitypub/) (outbox, inbox,
|
||||||
[bookmarks](https://docs.joinmastodon.org/methods/bookmarks/),
|
delivery) and [Activity Streams 2.0](https://www.w3.org/TR/activitystreams-core/)
|
||||||
[favourites](https://docs.joinmastodon.org/methods/favourites/),
|
(collections, paging)
|
||||||
[OAuth scopes](https://docs.joinmastodon.org/api/oauth-scopes/),
|
- [WebFinger, RFC 7033](https://www.rfc-editor.org/rfc/rfc7033)
|
||||||
[rate limits](https://docs.joinmastodon.org/api/rate-limits/),
|
- [Mastodon's ActivityPub notes](https://docs.joinmastodon.org/spec/activitypub/)
|
||||||
[pagination](https://docs.joinmastodon.org/api/guidelines/#pagination)
|
and [its HTTP Signatures profile](https://docs.joinmastodon.org/spec/security/)
|
||||||
|
— what social.coop actually sends and requires (the server is
|
||||||
|
Mastodon; the cache doesn't depend on it)
|
||||||
- [#23](https://github.com/dckc/madmode-blog/issues/23) — the PESOS goal
|
- [#23](https://github.com/dckc/madmode-blog/issues/23) — the PESOS goal
|
||||||
- [webapp/README.md](webapp/README.md) — the mint form and its secret
|
|
||||||
header list
|
|
||||||
Reference in new issue
Block a user