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

main
Last change on this file since 5143ccf was 5143ccf, checked in by Robin <roboburr@…>, 7 weeks ago

Docs: the Shaer to Klonkt C2S API

Documents the contract the Shaer apps use to drive a Klonkt account:
discovery (WebFinger -> actor -> endpoints), OAuth (register, PKCE
authorize, token, bearer), the owner reads (actor, outbox, inbox,
enriched followers/following with name + avatar), the outbox writes
(Create/Like/Announce/Follow/Undo, the to/cc visibility mapping,
direct notes, attachments, shaer:helpRequest, the not_public
hardening), uploadMedia, the shaer: vocabulary (FEP-633c) and an error
reference. Linked from FEDERATION.md.

Changed files:
docs/shaer-c2s-api.md

  • the full C2S reference

New file:
docs/shaer-c2s-api.md

FEDERATION.md

  • link to the client API doc under Additional documentation

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

  • Property mode set to 100644
File size: 8.6 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| `GET /ap/users/:slug/followers` | see below |
83| `GET /ap/users/:slug/following` | see below |
84
85**Followers and following** are count-only for the public (privacy). With the
86owner's bearer they return the real entries as AS2 actor references with display,
87so a client shows names and avatars instead of bare ids:
88
89```json
90{
91 "type": "OrderedCollection",
92 "totalItems": 2,
93 "orderedItems": [
94 { "id": "https://r.example/users/anna", "type": "Person",
95 "name": "Anna", "preferredUsername": "anna",
96 "icon": { "type": "Image", "url": "https://r.example/anna.png" } }
97 ]
98}
99```
100
101Display priority for a contact is `name`, then `preferredUsername`, then a handle
102derived from the id. Entries may also arrive as bare id strings (other servers,
103or entries with no cached display); a client handles both shapes.
104
105## 4. Writes: `POST /ap/users/:slug/outbox` (bearer)
106
107Post an Activity, or a bare object which the server wraps in a `Create` per the
108AP spec. Supported: `Create` (Note), `Like`, `Announce`, `Follow`, and `Undo` of
109`Follow` / `Like` / `Announce`. `Delete` and `Update` over C2S are not yet
110implemented.
111
112A `Note` carries `content` (HTML) and a `source` object with the plain text:
113
114```json
115{ "type": "Note",
116 "content": "<p>hoi</p>",
117 "source": { "content": "hoi", "mediaType": "text/plain" } }
118```
119
120### 4.1 Visibility (addressing)
121
122Visibility comes from the note's `to` / `cc`, the Mastodon model. There is no
123separate flag.
124
125| App choice | Addressing | Result |
126| --- | --- | --- |
127| Public | `to: [as:Public]` | public, boostable, in timelines |
128| Quiet public | `to: [<followers>]`, `cc: [as:Public]` | unlisted |
129| Friends | `to: [<followers>]` | followers-only, not boostable |
130| Participants only | `to: [<actor uris>]`, no Public | a private mention (direct message), see 4.2 |
131
132`as:Public` is `https://www.w3.org/ns/activitystreams#Public`. `<followers>` is
133the actor's followers collection URL. No addressing at all is treated as public
134(legacy).
135
136### 4.2 Direct notes (private mentions)
137
138A note addressed only to actor URIs (no Public, no followers) is a direct
139message, not a post. Klonkt delivers it S2S to exactly those inboxes: no
140followers fan-out, empty `cc`, so it can never be boosted or appear in a
141timeline. A guardian on any AP server (even plain Mastodon) receives it as a
142private mention. Address the recipients as `Mention` tags so their servers
143notify them. A direct note with no resolvable recipient is refused
144(`400 no_recipients`).
145
146### 4.3 Attachments
147
148Put AS2 `attachment` items on the note (`Image` / `Document` with `url`,
149`mediaType`, `name`). Upload first (section 5); Klonkt accepts only its own
150`/media/...` URLs, image/audio/video, up to 4.
151
152### 4.4 Help request (FEP-633c)
153
154A ward's call for help is a direct note carrying `"shaer:helpRequest": true`,
155addressed to all its guardians, optionally with a capture as an `attachment`, the
156subject as an FEP-e232 object link (never a `Mention`, which would add the
157subject to the conversation), and a short text quote. Guardian-side clients may
158render it as an alert; other servers read a normal private mention. See FEP-633c
1595.2.1.
160
161### 4.5 Hardening
162
163- A `Like` or `Announce` of a non-public local post returns `403 not_public`.
164- An inbound boost or like of a non-public post is dropped, not stored.
165
166## 5. Media upload: `POST /ap/users/:slug/uploadMedia` (bearer)
167
168`multipart/form-data`, field name `file`, one image/audio/video, up to 32 MB.
169Returns `201 { "url": "/media/reply-media/...", "mediaType": "image/png", "name": "..." }`.
170Use `url` in an `attachment` on the next note.
171
172## 6. The `shaer:` vocabulary (FEP-633c)
173
174Namespace `https://ns.klonkt.com/shaer#`, declared in the emitted `@context`.
175
176| Term | On | Meaning |
177| --- | --- | --- |
178| `shaer:guardians` | Actor | array of guardian actor URIs; non-empty marks a ward |
179| `shaer:isGuardian` | Actor | `true` marks a guardian |
180| `shaer:hasGuardians` | Object | per-object routing hint that the author is a ward |
181| `shaer:helpRequest` | Note | marks a direct note as a ward's call for help (4.4) |
182
183The full model (the handshake, gating, emancipation, escalation) is FEP-633c in
184`work/klonkt/fep-633c.md`.
185
186## 7. Error reference
187
188| Status | When |
189| --- | --- |
190| `400 invalid_activity` / `missing_object` / `empty_note` | malformed write |
191| `400 no_recipients` | direct note with no resolvable recipient |
192| `400 unsupported_type` | a verb C2S does not implement (Delete/Update) |
193| `403` (reads) | not the owner (inbox, owner followers/following) |
194| `403 not_public` | Like/Announce of a non-public local post |
195| `401` | dead or missing bearer: re-authenticate |
196| `502 cannot_resolve_inReplyTo` / `direct_failed` | a delivery target could not be resolved |
197
198## 8. Notes and limits
199
200- The enriched followers/following display is a Klonkt convenience. A generic AP
201 server returns bare ids; the client falls back to a derived handle.
202- This is ActivityPub C2S. There is no Mastodon-compatible client API.
203- See `FEDERATION.md` for the server-to-server surface (activities, signatures,
204 extension terms, supported FEPs).
Note: See TracBrowser for help on using the repository browser.