@recursiv/sdk 0.7.71 → 0.7.72
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.
- package/README.md +9 -2
- package/dist/resources/curator.d.ts +2 -1
- package/dist/resources/notifications.d.ts +7 -2
- package/dist/resources/protocols.d.ts +8 -2
- package/dist/resources/protocols.js +8 -3
- package/dist/types.d.ts +37 -0
- package/llm.md +3 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -108,12 +108,12 @@ for await (const chunk of r.agents.chatStream(agent.id, { message: 'Hello!' }))
|
|
|
108
108
|
### Platform
|
|
109
109
|
| Resource | Purpose |
|
|
110
110
|
|----------|---------|
|
|
111
|
-
| `r.settings` | User preferences (including app-scoped content choices), sessions (`revokeOtherSessions` keeps this device), password management, email change (`requestConfirmationCode` for accounts with no password), deactivation (`disableAccount({ password })`, 0.7.71; 404 on an API that does not serve it) |
|
|
111
|
+
| `r.settings` | User preferences (including app-scoped content choices; per-kind notification settings in `notifications.channels`, set one kind with `updateNotifications({ channels: { post_reaction: { push: false } } })`, 0.7.72), sessions (`revokeOtherSessions` keeps this device), password management, email change (`requestConfirmationCode` for accounts with no password), deactivation (`disableAccount({ password })`, 0.7.71; 404 on an API that does not serve it) |
|
|
112
112
|
| `r.billing` | Usage tracking, subscriptions, checkout |
|
|
113
113
|
| `r.appSubscriptions` | Consumer app plans: `checkout`, `status`, `createPortalSession`, `previewTierChange`, `changeTier` |
|
|
114
114
|
| `r.boosts` | Promoted posts: `create`, `mine`, `balance`, `pause`, `resume`, `cancel`, `recordEvent`, `topup`, `confirmTopup`; admin `listPendingReview`, `review`, `grant` |
|
|
115
115
|
| `r.wallet` | Wallet, MINDS tokens (`getTokenBalance`, `getTokenLedger`), USD payouts (`getPayouts`, `startPayoutOnboarding`, `payoutLoginLink`) |
|
|
116
|
-
| `r.notifications` | Notification stream (`list`, `unreadCount`, `markAsRead`, `archive`, `markAllAsRead`) and push tokens |
|
|
116
|
+
| `r.notifications` | Notification stream (`list`, `unreadCount`, `markAsRead`, `archive`, `markAllAsRead`; `category: 'mentions'` for mentions and replies only, 0.7.72) and push tokens |
|
|
117
117
|
| `r.github` | GitHub integration |
|
|
118
118
|
| `r.integrations` | External service connections |
|
|
119
119
|
| `r.admin` | User management, stats, content reports (`listReports`, `resolveReport`, `dismissReport`) |
|
|
@@ -809,6 +809,13 @@ a legislature, or a party or politician. Hide politics leaves those out whatever
|
|
|
809
809
|
`feeds.top()` carry `political` too (0.7.67), and those reads and `curator.forYouWorld()` leave political stories out
|
|
810
810
|
under Hide politics.
|
|
811
811
|
|
|
812
|
+
From 0.7.72 Hide politics on those reads that need no compass judgment (the radar, `curator.today()`,
|
|
813
|
+
`getPublicTrends` and keyless `getSharedTrends({ ..., compass: 'hide' })`, the viral read, `sdk.discovery.videos` and a
|
|
814
|
+
visitor's Discover posts) applies for every caller, whether or not the operator serves them the full filter: where it
|
|
815
|
+
does not, Hide is the floor (the server's own `political` flags and browse categories, no judgment read) and
|
|
816
|
+
`meta.compass_filter` says `status: 'applied'` with `basis: 'flags'`. Choose sides, Mix sides and the catalog's
|
|
817
|
+
`compass_filter` capability still follow the operator's audience.
|
|
818
|
+
|
|
812
819
|
Every story names one outlet count (0.7.71): radar stories, World Today stories, category strip stories, feed-top and search stories carry `outlets: { total, mainstream, independent, other }`, the distinct outlets of the articles the story lists (buckets sum to `total`). Show `outlets.total`; `sources.outlet` counts articles, not outlets.
|
|
813
820
|
|
|
814
821
|
A World Today story's discussion across networks (0.7.69; servers with World Story Volume on): `sdk.curator.today()`
|
|
@@ -409,7 +409,8 @@ export interface CuratorTodayResponse {
|
|
|
409
409
|
data: CuratorTodayEdition | null;
|
|
410
410
|
/**
|
|
411
411
|
* `compass_filter`: present when the read named a filtering `compass`: what Hide politics did to the stories (counts
|
|
412
|
-
* only
|
|
412
|
+
* only; from 0.7.72 for every reader, `basis: 'flags'` where the full filter is not served to them), or status
|
|
413
|
+
* 'unavailable' when it was not applied (a side or Mix).
|
|
413
414
|
*/
|
|
414
415
|
meta: {
|
|
415
416
|
generating: boolean;
|
|
@@ -91,7 +91,11 @@ export interface Notification {
|
|
|
91
91
|
actor?: NotificationActor | null;
|
|
92
92
|
post_preview?: NotificationPostPreview | null;
|
|
93
93
|
}
|
|
94
|
-
|
|
94
|
+
/**
|
|
95
|
+
* `mentions` (SDK 0.7.72) is a Mentions tab: mentions and replies only, a
|
|
96
|
+
* subset of `social`. An older server answers it 400.
|
|
97
|
+
*/
|
|
98
|
+
export type NotificationCategory = 'all' | 'social' | 'system' | 'mentions';
|
|
95
99
|
export type NotificationScopeParams = {
|
|
96
100
|
/** Which kind of notification to count. Defaults to `all`. Same meaning as in `list`. */
|
|
97
101
|
category?: NotificationCategory;
|
|
@@ -114,7 +118,8 @@ export interface ListNotificationsParams extends PaginationParams {
|
|
|
114
118
|
* reposts, mentions, community invites, joins and join requests, moderation
|
|
115
119
|
* decisions). `system` is everything operational (agent, task, deploy,
|
|
116
120
|
* billing, security). Direct messages are in neither: they live in the chat
|
|
117
|
-
* inbox. `
|
|
121
|
+
* inbox. `mentions` is the posts that name you or answer you (mentions and
|
|
122
|
+
* replies). `all` (the default) lists every row that has not been archived.
|
|
118
123
|
*
|
|
119
124
|
* Ask for it here rather than filtering the page you get back: the server
|
|
120
125
|
* applies the limit AFTER the category, so a client that filters afterwards
|
|
@@ -1768,7 +1768,11 @@ export declare class ProtocolsResource {
|
|
|
1768
1768
|
}): Promise<SingleResponse<SocialAuthorFeed>>;
|
|
1769
1769
|
/**
|
|
1770
1770
|
* Public, app-moderated discovery without account authority. Enrichment uses cached judgments only.
|
|
1771
|
-
* Every page, previews included, gets the safety floor; `discovery` only asks for a layout.
|
|
1771
|
+
* Every page, previews included, gets the safety floor; `discovery` only asks for a layout. `compass` (0.7.72): a
|
|
1772
|
+
* visitor's Politics choice, `all` or `hide` only (a side or Mix is refused, 400 `invalid_input`). Hide politics leaves
|
|
1773
|
+
* out the topics and posts whose cached browse category is politics, for every visitor, and the page says what it did
|
|
1774
|
+
* in `meta.compass_filter` (`basis: 'flags'` until the operator serves visitors the full filter). An API older than
|
|
1775
|
+
* 0.7.72 refuses `compass` here with a 400.
|
|
1772
1776
|
*/
|
|
1773
1777
|
getSharedTrends(projectId: string, params: {
|
|
1774
1778
|
protocol: string;
|
|
@@ -1777,6 +1781,7 @@ export declare class ProtocolsResource {
|
|
|
1777
1781
|
includePreviews?: boolean;
|
|
1778
1782
|
category?: string;
|
|
1779
1783
|
discovery?: boolean;
|
|
1784
|
+
compass?: string;
|
|
1780
1785
|
}): Promise<SingleResponse<SocialTrends>>;
|
|
1781
1786
|
/** Open a provider-listed topic through the same public-only app boundary, with the same safety floor. */
|
|
1782
1787
|
getSharedTrendFeed(projectId: string, params: {
|
|
@@ -1843,7 +1848,8 @@ export declare class ProtocolsResource {
|
|
|
1843
1848
|
* Current provider-ranked topics or posts, with explicit source and coverage. Does not ingest content.
|
|
1844
1849
|
* Every page, previews included, gets the safety floor; `discovery` only asks for a layout. `compass` (a Politics
|
|
1845
1850
|
* choice, spelled as `posts.list` takes it): Hide politics (`hide`) leaves out the topics and posts whose cached
|
|
1846
|
-
* browse category is politics,
|
|
1851
|
+
* browse category is politics, for every member (from 0.7.72; before, only where the filter was served to them), with
|
|
1852
|
+
* `meta.compass_filter.basis` 'flags' where the full filter is not served; a side or Mix is answered unfiltered. The
|
|
1847
1853
|
* page then says what it did in `meta.compass_filter`.
|
|
1848
1854
|
*/
|
|
1849
1855
|
getPublicTrends(params: {
|
|
@@ -123,11 +123,15 @@ export class ProtocolsResource {
|
|
|
123
123
|
}
|
|
124
124
|
/**
|
|
125
125
|
* Public, app-moderated discovery without account authority. Enrichment uses cached judgments only.
|
|
126
|
-
* Every page, previews included, gets the safety floor; `discovery` only asks for a layout.
|
|
126
|
+
* Every page, previews included, gets the safety floor; `discovery` only asks for a layout. `compass` (0.7.72): a
|
|
127
|
+
* visitor's Politics choice, `all` or `hide` only (a side or Mix is refused, 400 `invalid_input`). Hide politics leaves
|
|
128
|
+
* out the topics and posts whose cached browse category is politics, for every visitor, and the page says what it did
|
|
129
|
+
* in `meta.compass_filter` (`basis: 'flags'` until the operator serves visitors the full filter). An API older than
|
|
130
|
+
* 0.7.72 refuses `compass` here with a 400.
|
|
127
131
|
*/
|
|
128
132
|
getSharedTrends(projectId, params) {
|
|
129
133
|
return this.client.get(`/protocols/public/${encodeURIComponent(projectId)}/trends`, { protocol: params.protocol, limit: params.limit, cursor: params.cursor,
|
|
130
|
-
includePreviews: params.includePreviews, category: params.category, discovery: params.discovery });
|
|
134
|
+
includePreviews: params.includePreviews, category: params.category, discovery: params.discovery, compass: params.compass });
|
|
131
135
|
}
|
|
132
136
|
/** Open a provider-listed topic through the same public-only app boundary, with the same safety floor. */
|
|
133
137
|
getSharedTrendFeed(projectId, params) {
|
|
@@ -207,7 +211,8 @@ export class ProtocolsResource {
|
|
|
207
211
|
* Current provider-ranked topics or posts, with explicit source and coverage. Does not ingest content.
|
|
208
212
|
* Every page, previews included, gets the safety floor; `discovery` only asks for a layout. `compass` (a Politics
|
|
209
213
|
* choice, spelled as `posts.list` takes it): Hide politics (`hide`) leaves out the topics and posts whose cached
|
|
210
|
-
* browse category is politics,
|
|
214
|
+
* browse category is politics, for every member (from 0.7.72; before, only where the filter was served to them), with
|
|
215
|
+
* `meta.compass_filter.basis` 'flags' where the full filter is not served; a side or Mix is answered unfiltered. The
|
|
211
216
|
* page then says what it did in `meta.compass_filter`.
|
|
212
217
|
*/
|
|
213
218
|
getPublicTrends(params, options) {
|
package/dist/types.d.ts
CHANGED
|
@@ -104,6 +104,13 @@ export interface CompassFilterMeta {
|
|
|
104
104
|
o: number;
|
|
105
105
|
};
|
|
106
106
|
};
|
|
107
|
+
/**
|
|
108
|
+
* `flags` (0.7.72): Hide politics was applied by the floor every caller gets, with no compass judgment: a story's
|
|
109
|
+
* `political` flag and theme (the radar, World Today), a network post's cached browse category (trends, the viral
|
|
110
|
+
* read), a Minds post's browse category (Watch's videos, a visitor's Discover posts). Sent where the operator does not
|
|
111
|
+
* serve this caller the full filter yet. Absent: the full filter as served. Choose sides and Mix sides never take it.
|
|
112
|
+
*/
|
|
113
|
+
basis?: 'flags';
|
|
107
114
|
}
|
|
108
115
|
export interface PaginationMeta {
|
|
109
116
|
discovery?: DiscoveryLayout;
|
|
@@ -1453,11 +1460,32 @@ export interface RegisterTokenInput {
|
|
|
1453
1460
|
*/
|
|
1454
1461
|
device_key?: string;
|
|
1455
1462
|
}
|
|
1463
|
+
/** The three ways one kind of notification can reach someone. */
|
|
1464
|
+
export interface NotificationChannelToggles {
|
|
1465
|
+
inApp: boolean;
|
|
1466
|
+
push: boolean;
|
|
1467
|
+
email: boolean;
|
|
1468
|
+
}
|
|
1469
|
+
/**
|
|
1470
|
+
* The kinds of notification a person can set one by one. Social: mentions,
|
|
1471
|
+
* replies, reposts, reactions, follows and messages. Operator: agent work,
|
|
1472
|
+
* deploys, billing, invites and the daily email. A server may send more keys
|
|
1473
|
+
* than these.
|
|
1474
|
+
*/
|
|
1475
|
+
export type NotificationChannelKind = 'post_mention' | 'post_reply' | 'post_repost' | 'post_reaction' | 'follow' | 'message' | 'agent_activity' | 'billing' | 'community_invite' | 'project_invite' | 'deployment' | 'daily_digest';
|
|
1476
|
+
/** Every kind with all three channels spelled out, as the delivery gates read them. */
|
|
1477
|
+
export type NotificationChannelSettings = Partial<Record<NotificationChannelKind, NotificationChannelToggles>> & Record<string, NotificationChannelToggles>;
|
|
1456
1478
|
export interface UserPreferences {
|
|
1457
1479
|
notifications: {
|
|
1480
|
+
/** Master switches: on when any kind has the channel on. */
|
|
1458
1481
|
in_app: boolean;
|
|
1459
1482
|
push: boolean;
|
|
1460
1483
|
email: boolean;
|
|
1484
|
+
/**
|
|
1485
|
+
* Each kind on its own (`post_reaction: { inApp: true, push: false, email: false }`).
|
|
1486
|
+
* Absent on servers older than SDK 0.7.72.
|
|
1487
|
+
*/
|
|
1488
|
+
channels?: NotificationChannelSettings;
|
|
1461
1489
|
};
|
|
1462
1490
|
privacy: {
|
|
1463
1491
|
profile_visibility: 'public' | 'network' | 'private';
|
|
@@ -1479,9 +1507,18 @@ export interface ContentPreferences {
|
|
|
1479
1507
|
default_feed: 'foryou' | 'following';
|
|
1480
1508
|
}
|
|
1481
1509
|
export interface UpdateNotificationChannelsInput {
|
|
1510
|
+
/** Master switch: sets in-app on every kind. */
|
|
1482
1511
|
in_app?: boolean;
|
|
1512
|
+
/** Master switch: sets push on every kind. */
|
|
1483
1513
|
push?: boolean;
|
|
1514
|
+
/** Master switch: sets email on every kind. */
|
|
1484
1515
|
email?: boolean;
|
|
1516
|
+
/**
|
|
1517
|
+
* Set single kinds and leave every other kind as it is:
|
|
1518
|
+
* `{ post_reaction: { push: false } }` stops reaction pushes and nothing else.
|
|
1519
|
+
* Applied after the master switches when both are sent.
|
|
1520
|
+
*/
|
|
1521
|
+
channels?: Partial<Record<NotificationChannelKind, Partial<NotificationChannelToggles>>> & Record<string, Partial<NotificationChannelToggles>>;
|
|
1485
1522
|
}
|
|
1486
1523
|
export interface UpdatePrivacyInput {
|
|
1487
1524
|
profile_visibility?: 'public' | 'network' | 'private';
|
package/llm.md
CHANGED
|
@@ -788,7 +788,7 @@ Methods: `list`, `get`, `create`, `delete`
|
|
|
788
788
|
|
|
789
789
|
## Platform Resources
|
|
790
790
|
|
|
791
|
-
- `r.settings` — User preferences, sessions, password changes, account deletion. `revokeOtherSessions({ keep_session_token? })` signs out every other device and keeps this key (and the proven session); `revokeAllSessions()` also revokes this key. Accounts with no password confirm `requestEmailChange({ new_email, email_code })` with a code from `requestConfirmationCode('email_change')`. With a project-bound key, `getPreferences()` includes app-scoped `content`; persist changes with `updateContent({ show_nsfw?, autoplay_video?, ai_enabled?, default_feed? })`. `disableAccount({ password })` (0.7.71) deactivates the account (hidden, nothing deleted; signing back in restores it), asked once; an API that does not serve deactivation answers 404 `NotFoundError`, so keep the control hidden until yours does.
|
|
791
|
+
- `r.settings` — User preferences, sessions, password changes, account deletion. `revokeOtherSessions({ keep_session_token? })` signs out every other device and keeps this key (and the proven session); `revokeAllSessions()` also revokes this key. Accounts with no password confirm `requestEmailChange({ new_email, email_code })` with a code from `requestConfirmationCode('email_change')`. With a project-bound key, `getPreferences()` includes app-scoped `content`; persist changes with `updateContent({ show_nsfw?, autoplay_video?, ai_enabled?, default_feed? })`. Notifications (0.7.72): `getPreferences()` returns `notifications.channels`, each kind (`post_mention`, `post_reply`, `post_repost`, `post_reaction`, `follow`, `message`, and the operator kinds) with `{ inApp, push, email }`; `updateNotifications({ channels: { post_reaction: { push: false } } })` changes that kind only, while `in_app`/`push`/`email` stay master switches over every kind. `disableAccount({ password })` (0.7.71) deactivates the account (hidden, nothing deleted; signing back in restores it), asked once; an API that does not serve deactivation answers 404 `NotFoundError`, so keep the control hidden until yours does.
|
|
792
792
|
- `r.signals` (0.7.71) — The member's engagement signals for their own ranking (For You), under their app-bound key (`agents:write`): `batch(events, { keepalive?, retry? })` sends up to `SIGNAL_BATCH_MAX` (100) `{ event_type, post_id?, dwell_ms?, metadata? }` to POST /signals/batch; `record(event)` sends one to POST /signals/post (for an API before the batch route, which answers that one 404). `event_type`: `view`, `click`, `dwell`, `react_up`, `react_down`, `save`, `hide`, `share`, `mute_source`, `open_chat`, `ask_agent`, `more_like`, `unhide`. Best-effort: a signal the server cannot store answers 200 with `recorded` 0 (or false) and a `reason` (`no_project_scope`, `project_db_unavailable`). `keepalive: true` is for a flush as the page is hidden or unloads: the request is on the wire before the call returns and is asked once.
|
|
793
793
|
|
|
794
794
|
```typescript
|
|
@@ -798,7 +798,7 @@ addEventListener('pagehide', () => { void r.signals.batch(pending.splice(0, SIGN
|
|
|
798
798
|
- `r.billing` — Usage tracking, checkout sessions, portal sessions, metered billing
|
|
799
799
|
- `r.appSubscriptions` — Project-scoped consumer plans: `checkout` (409 `subscription_exists` when the member already pays), `status`, `createPortalSession({ return_url, flow?: 'cancel' })` (`flow: 'cancel'` opens Stripe's cancel confirmation; 409 `no_subscription` when there is no plan to cancel), `previewTierChange({ tier })`, `changeTier({ tier, proration_date? })` (in-place, prorated, never a second subscription)
|
|
800
800
|
- `r.boosts` — Promoted posts (micro-USD): `create`, `mine`, `balance`, `pause`, `resume`, `cancel`, `recordEvent`; buy credit with `topup({ amount_cents, return_url })` then `confirmTopup(sessionId)` (credits once); admin `listPendingReview`, `review`, `grant`
|
|
801
|
-
- `r.notifications` — The notification stream: `list({ category: 'social' })` (rows carry `actor`, `reaction_type` and a `post_preview` with the viewer's `user_reaction`/`reposted_by_viewer_id` and counts; an `agent_needs_input` row carries `answered`, true once you have written in the agent's thread since it asked, when its title reads "You answered <agent>"), `unreadCount({ category })` for badges (`const { data } = await r.notifications.unreadCount({ category: 'social' })`), `markAsRead(id)`, `archive(id)` to dismiss, `markAllAsRead(category)` or `markAllAsRead({ category, organization_id })`, plus push token `registerToken`/`unregisterToken`. A push's `data.notificationId` is the row to mark read on tap. DMs are not in the stream.
|
|
801
|
+
- `r.notifications` — The notification stream: `list({ category: 'social' })` (rows carry `actor`, `reaction_type` and a `post_preview` with the viewer's `user_reaction`/`reposted_by_viewer_id` and counts; an `agent_needs_input` row carries `answered`, true once you have written in the agent's thread since it asked, when its title reads "You answered <agent>"), `category: 'mentions'` (0.7.72) for a Mentions tab (mentions and replies only; `unreadCount` and `markAllAsRead` take it too), `unreadCount({ category })` for badges (`const { data } = await r.notifications.unreadCount({ category: 'social' })`), `markAsRead(id)`, `archive(id)` to dismiss, `markAllAsRead(category)` or `markAllAsRead({ category, organization_id })`, plus push token `registerToken`/`unregisterToken`. A push's `data.notificationId` is the row to mark read on tap. DMs are not in the stream.
|
|
802
802
|
- `r.uploads` — Legacy media upload URLs (prefer `r.storage` for new projects)
|
|
803
803
|
- `r.github` — GitHub integration (getRepository, getTree, getFileContent, getCommits, createFromTemplate)
|
|
804
804
|
- `r.deployments` — Deployment management (prefer `r.projects.deploy()` for new projects)
|
|
@@ -835,6 +835,7 @@ await r.appSubscriptions.changeTier({ tier: 'pro', proration_date: quote.prorati
|
|
|
835
835
|
- Public trends: require `public_trends` from `publicNetworks()`, then `getPublicTrends({ protocol, limit?, cursor?, compass? })` (`compass: 'hide'` leaves out politics, 0.7.62); render `kind: topics` as source topics and `kind: posts` as source posts. With `trend_feed`, open only a returned current topic using `getPublicTrendFeed({ protocol, topic: trend.nativeId, limit?, cursor? })`. Show provider, observedAt and provider_ranked_window coverage; null rankingWindow is unknown, not a daily/global ranking. Preserve source order, canonical IDs and readRef, and opaque cursors. Topic catalogs do not paginate; Mastodon trends stop at 200 source positions. Both methods require users:read plus posts:read on the current human app key; Nostr trends are unsupported. Propagate errors; trend_unavailable means refresh the topic list. No ingestion or native writes.
|
|
836
836
|
- One outlet count per story (0.7.71): a radar story (list, story read, archive), a World Today story (`outlets`, with `outlet_count` its total and `lane_mix` its lanes), a category strip story, a `feeds.top`/`feeds.stories` story and a search story carry `outlets: { total, mainstream, independent, other }`, the distinct outlets of the articles the story lists (an allow-listed outlet once, an unlisted host in `other`; the buckets sum to `total`). Show `outlets.total`; `sources.outlet` counts articles. `DiscoveryCluster.outletCount` reads it. An older server omits it.
|
|
837
837
|
- Political stories (0.7.67): every radar story (`getPublicRadar`, `getSharedRadar`, a story read) and every World Today story (`r.curator.today()`) carries `political`: true when a quarter of its judged posts are politics, its theme or topic is politics, or its headline or summary names an election, a war or troop deployment, a court or justice, a legislature, or a party or politician. `compass: 'hide'` leaves those out whatever their theme; `r.curator.today({ compass: 'hide' })` does the same to the front page, the next story leads, and `meta.compass_filter` counts what was left out. A story from `r.feeds.stories()` or a `r.feeds.top()` lead carries `political` too, and both reads, and `r.curator.forYouWorld()`, leave political stories out under `compass: 'hide'` whatever their theme. An older server omits `political`: decide by `theme` as before.
|
|
838
|
+
- Hide politics as a floor (0.7.72): the radar, `r.curator.today()`, `getPublicTrends`, keyless `visitor.protocols.getSharedTrends(projectId, { protocol, compass: 'hide' })` (visitors send `all` or `hide` only; an older API answers 400 to `compass` there), the viral read, `r.discovery.videos` and a visitor's Discover posts apply `compass: 'hide'` for every caller. Where the operator does not serve the caller the full filter, the answer's `meta.compass_filter` is `{ status: 'applied', basis: 'flags', ... }`: left out by the server's own `political` flags and browse categories, no judgment read. Treat `applied` with or without `basis` as filtered. Choose sides and Mix sides still answer `unavailable` until the operator serves them, and the catalog's `compass_filter` capability still says when to offer Politics in Filters.
|
|
838
839
|
- Post language: a `SocialPost` may carry `language`, the lower-case primary subtag the post declares (`'de'`, `'en'`; Bluesky `langs[0]`, Mastodon `language`, a Nostr NIP-32 `ISO-639-1` label). It is absent when the post declares none (most Nostr notes) and on posts from the source index; it is never guessed and no post is dropped for it. Order by it (the viewer's language first, undeclared next), never filter on it.
|
|
839
840
|
- `listCandidates({ protocol?, limit?, cursor? })` and `getCandidate(id)` are read-only quarantine review, not published-post search. Both require `admin` scope, a project-bound key, a live admin role, and existing project-admin access. The key supplies app/network scope; neither method accepts another project or network. They never invoke refresh, change settings, or publish.
|
|
840
841
|
- Lists return `{ data: ProtocolQuarantinedCandidate[], meta: { limit, has_more, next_cursor } }`; use the opaque `next_cursor`, not offset pagination. The API defaults `limit` to 20 and permits at most 50. Details return `{ data: ProtocolQuarantinedCandidate }`. Every candidate has `state: 'quarantined'` and `content_format: 'plain'`; render its bounded `content` as text, respect `content_truncated`, and distinguish upstream `published_at` from local `observed_at` / `created_at`.
|