flare-plugin-sdk 0.0.0-stage → 0.1.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +131 -2
  3. package/package.json +33 -4
  4. package/skill/flare-plugin/ABI.md +334 -0
  5. package/skill/flare-plugin/README.md +204 -0
  6. package/skill/flare-plugin/SKILL.md +80 -0
  7. package/skill/flare-plugin/assets/github-template/README.md +71 -0
  8. package/skill/flare-plugin/assets/github-template/assets/icon.png +0 -0
  9. package/skill/flare-plugin/assets/github-template/manifest.json +29 -0
  10. package/skill/flare-plugin/assets/github-template/plugin.js +108 -0
  11. package/skill/flare-plugin/examples/pixelfed/README.md +78 -0
  12. package/skill/flare-plugin/examples/pixelfed/assets/icon.png +0 -0
  13. package/skill/flare-plugin/examples/pixelfed/locales/en.json +24 -0
  14. package/skill/flare-plugin/examples/pixelfed/locales/zh-Hans.json +24 -0
  15. package/skill/flare-plugin/examples/pixelfed/manifest.json +211 -0
  16. package/skill/flare-plugin/examples/pixelfed/plugin.js +793 -0
  17. package/skill/flare-plugin/flare-plugin.d.ts +501 -0
  18. package/skill/flare-plugin/pack.mjs +155 -0
  19. package/skill/flare-plugin/references/authentication.md +239 -0
  20. package/skill/flare-plugin/references/capabilities.md +88 -0
  21. package/skill/flare-plugin/references/manifest.md +192 -0
  22. package/skill/flare-plugin/references/publishing.md +111 -0
  23. package/skill/flare-plugin/references/recipes.md +328 -0
  24. package/skill/flare-plugin/references/runtime-http.md +207 -0
  25. package/skill/flare-plugin/references/testing.md +176 -0
  26. package/skill/flare-plugin/references/troubleshooting.md +116 -0
  27. package/skill/flare-plugin/references/wire-data.md +163 -0
@@ -0,0 +1,176 @@
1
+ # Validation and acceptance
2
+
3
+ Record checks against the actual plugin revision. Use the smallest checks that
4
+ exercise the edited behavior. Distinguish syntax, mocked behavior, real platform engines,
5
+ installation, live website behavior, and platform UI; none proves all the others.
6
+ No demonstration backend is required for authoring this SDK.
7
+
8
+ ## Checks in a generated repository
9
+
10
+ From the generated repository root, using Node:
11
+
12
+ ```sh
13
+ node --check plugin.js
14
+ node -e 'JSON.parse(require("node:fs").readFileSync("manifest.json", "utf8")); console.log("Manifest JSON parses")'
15
+ ```
16
+
17
+ These are syntax checks only: Node accepts APIs that the plugin Host does not
18
+ provide, and parsing JSON does not validate the manifest. Also inspect:
19
+
20
+ - Required root files and a valid supported icon; stable IDs and version/tag consistency.
21
+ - Every declared known operation has its required registration and full method
22
+ set. Login negotiates supported subsets with a displayable capability.
23
+ - Top-level initialization has no Host calls; bundled output has no module or
24
+ runtime platform dependency.
25
+ - Each remote response is mapped explicitly. Fixtures cover missing fields,
26
+ invalid JSON, HTTP errors, pagination continuation, and stable identities.
27
+ - Auth/cursor/action data is not accidentally inserted into public content or
28
+ media URLs. No real secrets enter checked-in fixtures.
29
+
30
+ Use the shipped `.d.ts` as editor/reference input, outside the release package.
31
+ For a TypeScript authoring project, reference that file and configure the actual
32
+ bundler to emit one classic script. Keep manifest/Host validation even when the
33
+ type checker passes: TypeScript optional service properties do not express all
34
+ capability requirements, and optionality does not make every null value valid.
35
+
36
+ ## Develop locally in Desktop
37
+
38
+ In Settings > Plugins, open the overflow menu and choose **Plugin development**.
39
+ Choose the directory containing `manifest.json`, `plugin.js`, and the plugin assets.
40
+ Click **Start development** and review access to your existing accounts. Both Compose Desktop and native macOS support this flow.
41
+
42
+ Edit `plugin.js`, `manifest.json`, `assets/icon.<extension>`, or `locales/*.json`. Flare
43
+ automatically validates the files; click **Reload plugin** to apply the validated
44
+ snapshot to this plugin's accounts and open pages. An invalid edit keeps the last
45
+ working version running. For TypeScript projects, select your build output directory
46
+ and run your own compiler/watch command; Flare does not execute build scripts.
47
+
48
+ Only one development directory can be active. Reload is refused while publishing,
49
+ uploading, deleting, or sending is in progress; retry after the operation finishes.
50
+ Reload clears this plugin's derived account caches and restarts its login flows;
51
+ drafts, credentials, routes, and other plugins are retained. Changing the plugin ID
52
+ or platform ID requires stopping first. An override must retain the installed
53
+ plugin's platform ID. A credential schema change may require logging in again.
54
+
55
+ **Stop development** restores the version that was running before development.
56
+ Restarting the app loads its installed plugins normally; the last directory is
57
+ remembered but never mounted automatically. Development uses real accounts and
58
+ data: changes made by the plugin are not rolled back. Stop development before
59
+ installing, updating, or uninstalling the same plugin. Recent-call
60
+ diagnostics show method names and outcomes only, never request/response payloads.
61
+
62
+ ## Checks with a Flare source checkout
63
+
64
+ Run from the Flare repository root:
65
+
66
+ ```sh
67
+ ./gradlew :feature:plugin:jvmTest \
68
+ --tests '*GitHubTemplateTest' \
69
+ --tests '*PixelfedSampleTest' \
70
+ --tests '*PluginGitHubInstallTest'
71
+ ```
72
+
73
+ `GitHubTemplateTest` constructs a GitHub-style source archive from the **actual
74
+ template files**, passes it through the real importer/registration checks,
75
+ activates it after reopening the store, and invokes real QuickJS. It verifies
76
+ token/profile identity, link DTOs, refresh/older/empty/end paging, typed HTTP
77
+ errors, and malformed website responses using an injected fixture transport.
78
+ It does not download a live GitHub repo or contact a website.
79
+
80
+ `PixelfedSampleTest` packages and exercises the complete Pixelfed sample with
81
+ fixtures; `PluginGitHubInstallTest` covers source archive/version/identity rules.
82
+ For a new adapter, replace/adapt the fixtures to its actual API evidence and
83
+ add checks at this seam. Read the generated test XML/results for executed counts,
84
+ failures, and skips; a cached/no-source task is not evidence of new execution.
85
+
86
+ For changes to manifests or wire contracts, relevant existing suites include
87
+ PluginManifestV1Test and WireV1Test. Runtime/HTTP/login suites cover their own Host
88
+ contracts. Run the affected suites rather than requiring a full app build for a
89
+ documentation edit. The opt-in Pixelfed live test requires PIXELFED_TEST_HOST and
90
+ PIXELFED_TEST_TOKEN; it is read-only unless PIXELFED_TEST_ALLOW_WRITE=1. Do not
91
+ enable live writes without the user's current authorization.
92
+
93
+ ## Website and UI acceptance
94
+
95
+ After an adapter has real endpoints and authorized credentials:
96
+
97
+ 1. Enter the GitHub repository URL and click **Install**. Confirm that Flare
98
+ downloads the latest stable tag and shows source/version and permissions in
99
+ the same dialog before confirmation. Check retry and cancellation, then
100
+ install and verify the platform/login/timeline entries without restarting Flare.
101
+ 2. Verify each supported login method and account identity; exercise cancelled,
102
+ rejected, or expired authentication where practical.
103
+ 3. Refresh, paginate, open internal/link details, and verify remote media where
104
+ declared. Exercise only the advertised read/write capabilities, using safe
105
+ test accounts/content authorized for those actions.
106
+ 4. For updates, test a new version without restarting, including credential compatibility
107
+ and removed permissions/capabilities that affect cached content.
108
+ 5. Record Android, JVM Desktop, iOS, and native macOS evidence separately.
109
+ Simulator/compilation results do not establish physical-device UI behavior.
110
+
111
+ The generic template's API is illustrative. Passing its fixture tests does not
112
+ turn it into a working adapter for an arbitrary website; replace and validate
113
+ the API contract before claiming it is ready for end users.
114
+
115
+ ## Skill portability and behavioral checks
116
+
117
+ Copy `feature/plugin/sdk/skill/flare-plugin` alone into a temporary directory.
118
+ Check that its directory name matches the frontmatter `name` and that all relative document/type/template/example references
119
+ stay within that copy. Exercise the pack command from the copy if local packaging
120
+ is relevant; no parent Flare path should be needed for authoring.
121
+
122
+ When testing the skill with an agent, use a concrete request with redacted API
123
+ fixtures: create a token feed, add article navigation, or diagnose a failed
124
+ capability declaration. Check the generated artifacts and behavior, not whether
125
+ the answer repeats the skill's wording. Ensure the agent reads only relevant
126
+ references, reports missing API evidence, and does not publish without authority.
127
+
128
+ ## Delivery evidence template
129
+
130
+ ```text
131
+ Plugin revision / API version:
132
+ Implemented features and capability mapping:
133
+ Syntax / manifest / registration checks:
134
+ Fixture tests and executed counts:
135
+ Real QuickJS / GitHub importer results:
136
+ Live website and platform UI checks:
137
+ Unverified assumptions / steps still requiring an account or device:
138
+ GitHub repository and stable tag, if publication was authorized and completed:
139
+ ```
140
+
141
+ ## Coverage map
142
+
143
+ | Documentation requirement | Resource / example |
144
+ | --- | --- |
145
+ | Quick start and AI workflow | README.md, SKILL.md, GitHub template |
146
+ | Manifest fields, defaults, validation | manifest.md; template and Pixelfed manifests |
147
+ | Capability/UI matrix and all twelve services | capabilities.md, recipes.md |
148
+ | Platform engines, HTTP, credentials, permissions | runtime-http.md; template requestJson helper |
149
+ | Five login methods and detection | authentication.md; template token login and Pixelfed OAuth |
150
+ | Mapping, identity, paging, tokens | wire-data.md; template and Pixelfed |
151
+ | Advanced features, guest, localization, Deep Links | recipes.md |
152
+ | Failure diagnosis, limits, recovery | troubleshooting.md |
153
+ | Verification and evidence boundaries | this document; Host SDK tests |
154
+ | GitHub distribution and schema upgrades | publishing.md; ABI.md |
155
+
156
+ ## Platform engine regressions
157
+
158
+ The same engine contract suite runs against actual backends. From the Flare repository:
159
+
160
+ ```sh
161
+ ./gradlew :feature:plugin:jvmTest
162
+ ./gradlew :feature:plugin:connectedAndroidDeviceTest
163
+ ./gradlew :feature:plugin:macosArm64Test :feature:plugin:iosSimulatorArm64Test
164
+ ```
165
+
166
+ Android needs a connected supported device/emulator and its current WebView provider;
167
+ Apple tests need macOS/Xcode and an available iOS simulator. These check string/JSON
168
+ and Unicode transport, Host reply correlation, task draining, awaited versus detached
169
+ errors, isolation, policy enforcement, and close/cancellation. The existing JVM
170
+ package/proxy/account/lifecycle suite still runs against QuickJS. A Desktop fixture
171
+ run does not prove Android or Apple behavior; record OS/provider versions and actual
172
+ executed counts. Test UI/account/login flows separately on each shipping target.
173
+
174
+ Do not assert uniform heap/stack quotas or interruption of infinite loops. Test
175
+ cancellable Host work and retirement without running an unending loop in the test
176
+ process. See [runtime semantics](runtime-http.md#async-completion-and-errors).
@@ -0,0 +1,116 @@
1
+ # Troubleshooting and limits
2
+
3
+ First identify the stage: repository/version discovery, archive/manifest
4
+ validation, registration, immediate activation, login, invocation, or UI
5
+ mapping. Record the running plugin version, operation, error code, and a redacted
6
+ input/response. Avoid collecting credentials or private content. `flare` has no
7
+ logging API; do not invent `flare.log` or assume a developer console is available.
8
+
9
+ ## Common symptoms
10
+
11
+ | Symptom / message | Check and repair |
12
+ | --- | --- |
13
+ | No stable version tags | Public repo, stable SemVer tag, matching manifest.version; branches/prereleases are not install choices |
14
+ | Missing required file / nested plugin | Put manifest.json, plugin.js, one supported assets/icon.<extension> at the tagged repository root |
15
+ | Unknown field / unsupported schema | Use API/schema 1 and exact supported fields; strict JSON rejects extra keys |
16
+ | Missing method / incomplete capability | Compare full capability ID, required operation set, registration path, and login negotiation |
17
+ | Host API called during registration / undefined global | Keep top-level work to definitions and synchronous registration; move HTTP/crypto into methods |
18
+ | Platform ID already registered | Choose a distinct stable platform ID; do not impersonate a built-in platform |
19
+ | Installed change has no effect | Changes take effect immediately after successful activation; check validation or activation errors and the running version |
20
+ | Login profile identity mismatch | Verify profile.key.id equals accountId, and profile.key.host matches the selected origin |
21
+ | Form login did not complete | Password/CredentialImport/Form begin must return success or throw a typed error |
22
+ | Cookie login pending indefinitely | Declared probe URL/name/path scope, browser HTTPS, actual server login, and check's profile verification |
23
+ | OAuth state/expiry/package error | Use supplied state/redirectUri, resume with pendingPayload, and restart login after a package change or expiry |
24
+ | HTTP origin is not approved | Use an absolute selected-origin API URL; authOrigins adds login origins only |
25
+ | Request header is not allowed | Use structured authorization/cookies and let the Host manage transport headers |
26
+ | JSON parse error | Inspect status/content type/body; handle 204/empty responses and HTML error pages explicitly |
27
+ | Missing images with working text | mediaNetwork, current running grant, valid HTTPS URLs, and correct media DTOs |
28
+ | Missing tabs/search/controls | Required capabilities, static fields, negotiated account subset, default timeline/profileTabs, and operation completeness |
29
+ | Post click does nothing | detailType and target capability; link requires an absolute HTTPS URL |
30
+ | Infinite/repeated history | Correct cursor namespace, refresh handling, server exhaustion, and explicitly declared directions |
31
+ | Notifications collapse together | Use notification IDs, not the referenced post ID, and preserve actor/kind/timestamp |
32
+ | DM polling repeats or crosses rooms | Newer page cursor is separate from roomEntityToken; advance empty successful pages and isolate rooms |
33
+ | Actions show stale state | Return complete authoritative updatedPost/updatedRelation or appropriate invalidation; preserve action tokens |
34
+ | Runtime retired/paused | Find an unmarked exception, invalid Wire output, or exceeded budget before retrying |
35
+ | JavaScript unavailable on Android | Update Android System WebView; the provider must support message ports, Promise results, and large evaluations |
36
+ | Invocation succeeds despite an async error | Put required work in the method's await/return chain; detached rejections do not automatically fail the call |
37
+
38
+ ## Typed errors
39
+
40
+ Only errors created with `flare.error` cross as recoverable plugin errors:
41
+
42
+ ```js
43
+ throw flare.error({
44
+ code: "RateLimited",
45
+ message: { key: "error.limited", fallback: "Try again in {seconds} seconds", args: { seconds: 30 } },
46
+ retryAfterSeconds: 30,
47
+ });
48
+ ```
49
+
50
+ | Code | Typical use |
51
+ | --- | --- |
52
+ | AuthenticationRequired | Rejected/expired credentials; an account call can trigger relogin |
53
+ | NotFound | The requested entity does not exist |
54
+ | Validation | Invalid user input, with a useful non-sensitive message |
55
+ | RateLimited | Website throttling; optional retryAfterSeconds |
56
+ | Network | A classified network failure |
57
+ | Remote | Other website HTTP/API failure |
58
+ | Unsupported | An operation/parameter combination the adapter cannot handle |
59
+ | InvalidResponse | Malformed or semantically invalid website data |
60
+ | Cancelled | An explicitly represented plugin cancellation |
61
+
62
+ Use a WireText message, not a bare string. Retry delay is 0..604800 seconds;
63
+ remoteCode is optional and at most 512 characters. HTTP Retry-After may be a date
64
+ or seconds: the template handles bounded numeric seconds and otherwise omits
65
+ the hint. The field communicates a delay, not a promise of automatic retries.
66
+ Avoid returning raw server messages containing credentials or private data.
67
+
68
+ Unmarked exceptions and invalid DTOs retire the runtime. Three counted fatal
69
+ failures within 60 seconds pause the plugin for that process. Cancellation can
70
+ retire a generation without counting as a failure; a deadline timeout can count.
71
+ An unavailable Android provider or collateral sandbox death does not count as
72
+ plugin misconduct. Retry after the provider/sandbox recovers; writes are not replayed.
73
+ Fix the underlying issue, then use the Host's retry mechanism where available or
74
+ restart. Global state is disposable, so recovery must not depend on it.
75
+
76
+ ## Limits
77
+
78
+ These are current implementation bounds, not targets. Byte limits apply to UTF-8
79
+ or serialized payload bytes; string limits are field-length checks. Keep normal
80
+ requests/responses well below them.
81
+
82
+ | Resource | Bound |
83
+ | --- | --- |
84
+ | GitHub source archive | 20 MiB compressed, 50 MiB uncompressed, 4096 entries |
85
+ | Canonical plugin package | 20 MiB compressed, 50 MiB uncompressed, 256 files |
86
+ | manifest / plugin.js / icon | 256 KiB / 4 MiB / 2 MiB |
87
+ | Icon dimensions | 1..1024 for each dimension |
88
+ | Locale files | 32 catalogs; 256 KiB each; 2048 keys each; 4096 characters per value |
89
+ | Registration evaluation | 2-second caller deadline |
90
+ | Runtime initialization | 2 seconds |
91
+ | Runtime memory / stack | Engine/system limits; no uniform Host cap or heap-based pool eviction |
92
+ | Invocation deadline | Detector 10 seconds; normal 30 seconds; extended 120 seconds where selected by the Host |
93
+ | Invocation JSON request / response | 1 MiB / 5 MiB |
94
+ | HTTP response body | 4 MiB |
95
+ | HTTP request headers | 32 headers, 8192 characters/value, 64 KiB aggregate name/value length |
96
+ | HTTP text/JSON/form body | 2 MiB; form also max 256 entries |
97
+ | HTTP redirects | 5; GET/HEAD, same origin only |
98
+ | Page request limit / returned items | 1..100 / at most 200 |
99
+ | ID / host | 512 / 253 characters |
100
+ | Cursor / entity token / action token | 4096 / 8192 / 8192 characters |
101
+ | Credential / OAuth pending JSON | 64 KiB each |
102
+ | OAuth pending lifetime | 15 minutes |
103
+ | Rich text | 1000000 characters |
104
+ | Post media / gallery images | At most 100; gallery must have at least 1 image |
105
+ | Nested repost depth | 4 recursive levels beyond the initial post |
106
+ | Crypto randomHex input / sha256 input | 1..1024 random bytes / 1 MiB UTF-8 text |
107
+
108
+ Multipart calls have at most 64 parts; text parts are at most 256 KiB. Assets have
109
+ their own Host/compose constraints, up to 1 GiB per asset at the bridge boundary;
110
+ the website and negotiated composer commonly impose much smaller limits.
111
+ Repeated HTTP requests do not reset an invocation deadline. See
112
+ [runtime/HTTP](runtime-http.md) before choosing a timeout or upload strategy.
113
+ Deadlines retire a runtime and cancel its Host work; they do not guarantee that
114
+ busy JavaScript stops. A JavaScriptCore loop can require an app restart to release
115
+ its worker. Avoid recursion/large allocations and test bounded computation on all
116
+ three engines.
@@ -0,0 +1,163 @@
1
+ # Wire data and pagination
2
+
3
+ All boundary values are JSON. The exact signatures are in
4
+ [flare-plugin.d.ts](../flare-plugin.d.ts). Build DTOs explicitly: returning an
5
+ entire website response introduces unknown fields, and the Host decoder is
6
+ strict. Non-finite numbers, cycles, BigInt, functions, and undefined return values
7
+ are not valid results. Optional nullable fields may be omitted; defaulted
8
+ non-null fields must not be explicitly null. A missing field with no default is
9
+ an error. Preserve string IDs; parsing a large numeric ID into a JavaScript
10
+ Number can already lose precision before mapping.
11
+
12
+ ## Identity and a minimal post
13
+
14
+ `EntityKeyV1` is `{ id, host }`: a stable website identifier and an HTTPS authority
15
+ without a scheme/path, with a non-default port when applicable. Use the selected
16
+ instance/API authority for IDs in its namespace. Do not substitute a display
17
+ name, page position, timestamp, random UUID, or cursor as an entity key.
18
+ Federated content may have a public URL on a different website while its ID is
19
+ still local to the selected API. Profile identity at login must match that origin.
20
+ The Host canonicalizes a profile's host to the selected origin when they differ
21
+ only by case. A notification without a post uses its mapped actor's profile
22
+ target, so tapping its row or avatar uses the same account key. Profile navigation
23
+ remains unavailable when the session does not expose that capability.
24
+
25
+ Example website response (illustrative):
26
+
27
+ ```json
28
+ {
29
+ "id": "post-17",
30
+ "author": { "id": "alice", "username": "alice", "name": "Alice" },
31
+ "created_at": "2026-01-01T00:00:00Z",
32
+ "text": "Hello",
33
+ "url": "https://example.org/posts/post-17"
34
+ }
35
+ ```
36
+
37
+ Explicit mapping, assuming the fields have been validated:
38
+
39
+ ```js
40
+ const host = context.origin.slice("https://".length);
41
+ const post = {
42
+ key: { id: remote.id, host },
43
+ author: {
44
+ key: { id: remote.author.id, host },
45
+ handle: remote.author.username,
46
+ displayName: remote.author.name,
47
+ },
48
+ createdAt: remote.created_at,
49
+ content: { format: "plain", value: remote.text },
50
+ detailType: "link",
51
+ url: remote.url,
52
+ };
53
+ ```
54
+
55
+ Use ISO-8601 instants with a timezone, not epoch numbers or locale-formatted dates.
56
+ `ProfileV1` requires key/handle/displayName; `PostV1` requires key/createdAt/content.
57
+ Post author and visibility default to null and may be omitted or explicitly null.
58
+ When an author is provided, it must be a complete valid ProfileV1. The Host does
59
+ not synthesize an author or infer public visibility. Most collections default
60
+ empty and booleans default false. Counts are nonnegative. Do not include unknown
61
+ optional website fields by spreading objects.
62
+ See [navigation](capabilities.md#entry-points-and-actions) before choosing detailType.
63
+
64
+ A minimal post without author or visibility is:
65
+
66
+ ```json
67
+ {
68
+ "key": { "id": "post-17", "host": "example.org" },
69
+ "createdAt": "2026-01-01T00:00:00Z",
70
+ "content": { "value": "Hello" }
71
+ }
72
+ ```
73
+
74
+ The same defaults apply inside `repost`, notifications, and `updatedPost` results.
75
+ An updated post is a complete snapshot: omitting author or visibility clears that
76
+ value rather than retaining it from the cached post.
77
+
78
+ ## Text and media
79
+
80
+ Rich text is `{ format: "plain" | "html", value }`; omitted format means plain.
81
+ Flare sanitizes HTML. Relative HTML links/resources resolve against the selected
82
+ instance; URLs must resolve to HTTPS without credentials. Scripts and local/
83
+ data/content resources are not a way to embed a custom UI. Normal DTO URL fields
84
+ should be absolute HTTPS. `detailType: "link"` always requires such a URL.
85
+
86
+ Media requires `{ id, type, url }`, with type `image`, `video`, `gif`, or `audio`.
87
+ Optional fields are previewUrl, description (alt text), width, height, and
88
+ durationMillis. Preserve accurate dimensions/duration and accessibility text.
89
+ Remote media needs the manifest's mediaNetwork permission, including profile
90
+ avatars, article covers, and images inside HTML. Returning text successfully does
91
+ not prove media permission is correct.
92
+
93
+ `repost` holds the nested original PostV1 with its own key and detailType; do not
94
+ overwrite it with the wrapper ID. `replyTo` is an EntityKey, not a full post.
95
+ Spoiler text, sensitive flag, visibility, and action state should reflect the
96
+ website. Post nesting is bounded; see [limits](troubleshooting.md#limits).
97
+
98
+ ## Pages
99
+
100
+ Most operations receive `request.page`; some, such as list.page and tabCatalog.page,
101
+ receive PageRequestV1 directly. Consult the signature before destructuring.
102
+
103
+ ```json
104
+ { "direction": "older", "limit": 20, "cursor": "opaque-page-2", "parameters": {} }
105
+ ```
106
+
107
+ Return a PageV1, not the website's pagination envelope:
108
+
109
+ ```json
110
+ { "items": [], "olderCursor": "opaque-page-3", "newerCursor": "latest-marker", "endReached": false }
111
+ ```
112
+
113
+ | Direction | Expected behavior |
114
+ | --- | --- |
115
+ | refresh | Fetch the initial/current page; the Host supplies no old cursor |
116
+ | older | Continue the older/history side using the supplied cursor |
117
+ | newer | Fetch newer items using the newer-side cursor, if declared/supported |
118
+
119
+ The Host maps olderCursor to append and newerCursor to prepend. `endReached`
120
+ suppresses the older cursor; it does not erase newerCursor. Return stable ordering
121
+ consistent with the selected operation and site API. If an API returns newest
122
+ first, keep that convention across pages. Respect the requested limit and map
123
+ website continuation tokens/Link headers/offsets explicitly. A cursor may encode
124
+ an offset or multiple server values, but it is bounded and opaque to the Host.
125
+
126
+ An empty page need not be the end: filtering or long polling may return no items
127
+ and still advance a cursor. Report actual server exhaustion, not merely
128
+ `items.length === 0`. Do not restart at page one when an older cursor is present.
129
+ Only declare directions you implement; an empty manifest direction set currently
130
+ allows all directions. Keep parameter semantics stable across refresh and paging.
131
+
132
+ ## Three different tokens
133
+
134
+ | Value | Purpose | Lifecycle |
135
+ | --- | --- | --- |
136
+ | `olderCursor` / `newerCursor` | Continue a page stream | Returned page → later paging request |
137
+ | `entityToken` | Opaque context for an entity lookup/operation | Returned entity → entity request; room and message tokens have distinct fields |
138
+ | `actionToken` | Opaque website data needed for one semantic mutation | Action descriptor → MutationRequestV1; relation uses actionTokens keyed by action |
139
+
140
+ Never use an entity token as a DM polling cursor. Room `messages` polling uses
141
+ the page's newerCursor and can advance after an empty successful page. Opaque
142
+ tokens are serialized/cacheable operation data, not a secret store. Keep account
143
+ credentials in `flare.credential`, and do not encode URLs that bypass origin checks.
144
+
145
+ ## Other entities
146
+
147
+ | DTO | Required fields / notable semantics |
148
+ | --- | --- |
149
+ | HashtagV1 | name; optional HTTPS url |
150
+ | NotificationV1 | id, createdAt, kind; optional actor/post/message. Identity belongs to the notification, not the referenced post |
151
+ | RelationV1 | profileKey; following/followedBy/blocking/muting default false; actionTokens map semantic actions to opaque strings |
152
+ | SocialListV1 | id, title; optional memberCount/entityToken |
153
+ | DirectMessageRoomV1 | key, title, participants; lastMessage must belong to this room |
154
+ | DirectMessageV1 | key, roomKey, sender, createdAt, content; fromCurrentAccount defaults false |
155
+ | ArticleV1 | key, title, content; optional author/createdAt/url/coverUrl |
156
+ | GalleryV1 | key, title, createdAt, url, nonempty images; all gallery media must be images; orientation defaults vertical |
157
+ | TimelineDescriptorV1 | id, title as WireText, icon; display defaults List; optional string parameters |
158
+ | TimelineSectionV1 | id, title as WireText, timelines as PageV1 of descriptors |
159
+
160
+ Notification kinds are `Mention`, `Reply`, `Favourite`, `Repost`, `Follow`, `Other`.
161
+ Gallery orientations are `horizontal`/`vertical`. These enum spellings intentionally
162
+ differ from manifest notification filter IDs and List/Grid display values.
163
+ See [recipes](recipes.md) for operation-specific result requirements and mutations.