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

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

Feature: enrichment is opt-in via Prefer (FEP-9876 conformance)

Klonkt is the reference implementation of FEP-9876. The owner
followers/following collections now enrich member representations only
when the client asks with Prefer: return=representation (RFC 7240),
echo Preference-Applied and always set Vary: Prefer; the default is
bare id strings, so the collections match the AP norm and existing
consumers are unaffected. The Prefer predicate is a pure, tested
function.

Changed files:
src/services/ActivityPubService.js

  • prefersEnriched(preferHeader): pure Prefer detector, exported

src/routes/activitypub.js

  • owner followers/following: enrich only on the preference, set Preference-Applied + Vary: Prefer

test/c2s-contacts.test.js

  • prefersEnriched cases

docs/shaer-c2s-api.md

  • document the Prefer opt-in

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

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