source: Klonkt/docs/shaer-c2s-api.md@ a6d703a

main
Last change on this file since a6d703a was a6d703a, checked in by roboburr <roboburr@…>, 5 weeks ago

Het wachten gedocumenteerd, inclusief waarom er geen achtergrondkanaal is

De verwijzing naar een push-notitie wees nergens heen; die notitie bestond niet.
Nu wel, want het is precies wat een client-bouwer moet weten: web push met VAPID
kan een zelfgehoste instance helemaal zelf, maar alleen naar een browser of een
geinstalleerde PWA. Een native app krijgt push via APNs, ondertekend met de
sleutel van de APPLICATIE, en die heeft een zelfgehoste Klonkt niet en hoort hij
ook niet te hebben.

Daarmee is het wachten op de inbox voor een zelfgehoste opzet geen tussenoplossing
tot er push komt, maar voorlopig HET verse kanaal.

Co-Authored-By: Claude Opus 5 <noreply@…>

  • Property mode set to 100644
File size: 11.1 KB
Line 
1# Shaer to Klonkt: the C2S API
2
3This documents the contract the Shaer apps (iOS, Android) use to drive a Klonkt
4account. It is ActivityPub Client-to-Server (C2S) over OAuth 2.0, plus a few
5`shaer:` extension terms from FEP-633c. It is not the Mastodon client API:
6Mastodon apps use their own API and are not served here.
7
8Everything is discovered, never hardcoded: a client resolves a handle to an
9actor document and reads the endpoints from it, so the same client works against
10any Klonkt instance (and degrades gracefully against other AP servers).
11
12Base URL below is the instance origin, e.g. `https://klonkt.example`. All AP
13requests send and accept `application/activity+json`.
14
15## 1. Discovery
16
171. **WebFinger** the handle:
18 `GET /.well-known/webfinger?resource=acct:<user>@<host>`
19 Returns a JRD; the `self` link (`type: application/activity+json`) is the
20 actor id.
212. **Fetch the actor** at that id: `GET /ap/users/:slug`.
22 Read the collection URLs and the C2S endpoints from it, never construct them:
23
24 ```json
25 {
26 "id": "https://klonkt.example/ap/users/robin",
27 "type": "Person",
28 "preferredUsername": "robin",
29 "name": "Robin",
30 "inbox": ".../inbox",
31 "outbox": ".../outbox",
32 "followers": ".../followers",
33 "following": ".../following",
34 "endpoints": {
35 "oauthAuthorizationEndpoint": "https://klonkt.example/oauth/authorize",
36 "oauthTokenEndpoint": "https://klonkt.example/oauth/token",
37 "uploadMedia": ".../uploadMedia"
38 }
39 }
40 ```
41
423. **Server metadata** (RFC 8414):
43 `GET /.well-known/oauth-authorization-server` returns
44 `authorization_endpoint`, `token_endpoint`, `registration_endpoint`,
45 `code_challenge_methods_supported: ["S256"]`,
46 `token_endpoint_auth_methods_supported: ["none"]`, `scopes_supported: ["c2s"]`.
47
48If the actor lacks the OAuth endpoints, the server does not support posting from
49apps; the client should degrade to read-only.
50
51## 2. Authentication (OAuth 2.0, public client + PKCE)
52
53Public clients only. No client secret. PKCE with S256 is required.
54
551. **Register** (RFC 7591, once per install):
56 `POST /oauth/register` with `{ "client_name": "...", "redirect_uris": ["com.klonkt.shaer:/oauth"] }`
57 returns `201 { "client_id": "..." }`. The redirect scheme must contain a dot
58 (reverse-DNS custom scheme).
592. **Authorize**: open `authorization_endpoint` in a system browser with
60 `response_type=code`, `client_id`, `redirect_uri`, `code_challenge`,
61 `code_challenge_method=S256`, `scope=c2s`, `state`. The user logs into Klonkt
62 and picks which of their sites the app may post as. For a non-http
63 redirect_uri Klonkt serves a small interstitial that forwards to the custom
64 scheme (mobile browsers drop a bare 302 to a custom scheme); web clients get a
65 302.
663. **Token**: `POST /oauth/token` (form-encoded) with
67 `grant_type=authorization_code`, `code`, `client_id`, `redirect_uri`,
68 `code_verifier`. Returns `{ "access_token": "...", "token_type": "Bearer" }`.
69
70The bearer is scoped to one user and one site. Send it as
71`Authorization: Bearer <token>` on every authenticated call. Tokens are stored
72hashed server-side; the client keeps the bearer in the platform keystore. A
73`401` means the token is dead: re-run the flow.
74
75## 3. Reads (bearer, owner only)
76
77| Call | Returns |
78| --- | --- |
79| `GET /ap/users/:slug` | the actor document (add the bearer to also read owner-only fields) |
80| `GET /ap/users/:slug/outbox` | OrderedCollection of the account's own `Create(Note)` |
81| `GET /ap/users/:slug/inbox` | OrderedCollection of recent inbound posts (accounts you follow) as `Create(Note)`. Owner only; `403` for anyone else. The unified home feed is outbox + inbox merged. |
82
83### Waiting for news on the inbox read
84
85The inbox read can hold the answer until there is something new, so a client does
86not have to poll. It is the **same call and the same response** — no separate
87endpoint and no separate "there is news" shape, because a second shape is a
88second description of a post that can drift from the first.
89
90```
91GET /ap/users/:slug/inbox?since=<cursor>&wait=25
92```
93
94- `since` is the `shaer:cursor` from your previous answer. Treat it as opaque.
95- `wait` is seconds, capped at 50 — well under what a proxy will hold.
96- Send neither and the route behaves exactly as it always did.
97
98The answer arrives as soon as anything the inbox would show has changed, or when
99`wait` runs out — whichever comes first. Either way it is the full, current
100collection with a fresh `shaer:cursor`. An empty-handed return is not an error:
101it means nothing happened, ask again.
102
103The cursor moves for **all four** sources this read merges: the timeline,
104messages, replies on your own posts, and your own sent notes.
105
106Limits worth knowing as a client author:
107
108- four waiting connections per account. A fifth gets the current state
109 immediately rather than an error, so a broken reconnect loop degrades to
110 ordinary polling instead of locking the account out.
111- hanging up ends the wait; there is no cost to abandoning a request.
112- the bearer is checked *before* any waiting, so an unauthenticated caller can
113 never hold a connection open.
114
115This works while your app is in the foreground, and only there. iOS freezes
116network activity the moment an app is backgrounded, and a held-open connection
117does not survive it.
118
119**There is no background channel to fall back on for a native app.** Klonkt's
120web push uses VAPID, which a self-hosted instance can generate and send entirely
121on its own — but only to a browser or an installed PWA. A native app receives
122push through APNs (or FCM), signed with the *application's* key, which a
123self-hosted Klonkt does not have and should not have: whoever holds it can push
124to every user of that app. Closing that gap needs a relay run by the app's
125publisher, and that is a decision about money and control, not a protocol
126detail.
127
128So for a self-hosted setup this is not a stopgap until push arrives. For now it
129is the freshness channel.
130| `GET /ap/users/:slug/followers` | see below |
131| `GET /ap/users/:slug/following` | see below |
132
133**Followers and following** are count-only for the public (privacy). With the
134owner's bearer they return the real entries. By default these are bare id
135strings; send `Prefer: return=representation` (FEP-9876) to get them enriched as
136AS2 actor references with display, so a client shows names and avatars instead of
137bare ids. The server echoes `Preference-Applied: return=representation` and sets
138`Vary: Prefer`:
139
140```json
141{
142 "type": "OrderedCollection",
143 "totalItems": 2,
144 "orderedItems": [
145 { "id": "https://r.example/users/anna", "type": "Person",
146 "name": "Anna", "preferredUsername": "anna",
147 "icon": { "type": "Image", "url": "https://r.example/anna.png" } }
148 ]
149}
150```
151
152Display priority for a contact is `name`, then `preferredUsername`, then a handle
153derived from the id. Entries may also arrive as bare id strings (other servers,
154or entries with no cached display); a client handles both shapes.
155
156## 4. Writes: `POST /ap/users/:slug/outbox` (bearer)
157
158Post an Activity, or a bare object which the server wraps in a `Create` per the
159AP spec. Supported: `Create` (Note), `Like`, `Announce`, `Follow`, and `Undo` of
160`Follow` / `Like` / `Announce`. `Delete` and `Update` over C2S are not yet
161implemented.
162
163A `Note` carries `content` (HTML) and a `source` object with the plain text:
164
165```json
166{ "type": "Note",
167 "content": "<p>hoi</p>",
168 "source": { "content": "hoi", "mediaType": "text/plain" } }
169```
170
171### 4.1 Visibility (addressing)
172
173Visibility comes from the note's `to` / `cc`, the Mastodon model. There is no
174separate flag.
175
176| App choice | Addressing | Result |
177| --- | --- | --- |
178| Public | `to: [as:Public]` | public, boostable, in timelines |
179| Quiet public | `to: [<followers>]`, `cc: [as:Public]` | unlisted |
180| Friends | `to: [<followers>]` | followers-only, not boostable |
181| Participants only | `to: [<actor uris>]`, no Public | a private mention (direct message), see 4.2 |
182
183`as:Public` is `https://www.w3.org/ns/activitystreams#Public`. `<followers>` is
184the actor's followers collection URL. No addressing at all is treated as public
185(legacy).
186
187### 4.2 Direct notes (private mentions)
188
189A note addressed only to actor URIs (no Public, no followers) is a direct
190message, not a post. Klonkt delivers it S2S to exactly those inboxes: no
191followers fan-out, empty `cc`, so it can never be boosted or appear in a
192timeline. A guardian on any AP server (even plain Mastodon) receives it as a
193private mention. Address the recipients as `Mention` tags so their servers
194notify them. A direct note with no resolvable recipient is refused
195(`400 no_recipients`).
196
197### 4.3 Attachments
198
199Put AS2 `attachment` items on the note (`Image` / `Document` with `url`,
200`mediaType`, `name`). Upload first (section 5); Klonkt accepts only its own
201`/media/...` URLs, image/audio/video, up to 4.
202
203### 4.4 Help request (FEP-633c)
204
205A ward's call for help is a direct note carrying `"shaer:helpRequest": true`,
206addressed to all its guardians, optionally with a capture as an `attachment`, the
207subject as an FEP-e232 object link (never a `Mention`, which would add the
208subject to the conversation), and a short text quote. Guardian-side clients may
209render it as an alert; other servers read a normal private mention. See FEP-633c
2105.2.1.
211
212### 4.5 Hardening
213
214- A `Like` or `Announce` of a non-public local post returns `403 not_public`.
215- An inbound boost or like of a non-public post is dropped, not stored.
216
217## 5. Media upload: `POST /ap/users/:slug/uploadMedia` (bearer)
218
219`multipart/form-data`, field name `file`, one image/audio/video, up to 32 MB.
220Returns `201 { "url": "/media/reply-media/...", "mediaType": "image/png", "name": "..." }`.
221Use `url` in an `attachment` on the next note.
222
223## 6. The `shaer:` vocabulary (FEP-633c)
224
225Namespace `https://ns.klonkt.com/shaer#`, declared in the emitted `@context`.
226
227| Term | On | Meaning |
228| --- | --- | --- |
229| `shaer:guardians` | Actor | array of guardian actor URIs; non-empty marks a ward |
230| `shaer:isGuardian` | Actor | `true` marks a guardian |
231| `shaer:hasGuardians` | Object | per-object routing hint that the author is a ward |
232| `shaer:helpRequest` | Note | marks a direct note as a ward's call for help (4.4) |
233
234The full model (the handshake, gating, emancipation, escalation) is FEP-633c in
235`work/klonkt/fep-633c.md`.
236
237## 7. Error reference
238
239| Status | When |
240| --- | --- |
241| `400 invalid_activity` / `missing_object` / `empty_note` | malformed write |
242| `400 no_recipients` | direct note with no resolvable recipient |
243| `400 unsupported_type` | a verb C2S does not implement (Delete/Update) |
244| `403` (reads) | not the owner (inbox, owner followers/following) |
245| `403 not_public` | Like/Announce of a non-public local post |
246| `401` | dead or missing bearer: re-authenticate |
247| `502 cannot_resolve_inReplyTo` / `direct_failed` | a delivery target could not be resolved |
248
249## 8. Notes and limits
250
251- The enriched followers/following display is a Klonkt convenience. A generic AP
252 server returns bare ids; the client falls back to a derived handle.
253- This is ActivityPub C2S. There is no Mastodon-compatible client API.
254- See `FEDERATION.md` for the server-to-server surface (activities, signatures,
255 extension terms, supported FEPs).
Note: See TracBrowser for help on using the repository browser.