@recursiv/sdk 0.7.53 → 0.7.55

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.
@@ -1,5 +1,5 @@
1
1
  import type { HttpClient } from '../client.js';
2
- import type { DiscoveryLayout, Post } from '../types.js';
2
+ import type { DiscoveryLayout, FalloverStep, Post } from '../types.js';
3
3
  import type { CuratorResource } from './curator.js';
4
4
  import type { DiscoveryLearning } from './discovery-learning.js';
5
5
  import type { PostsResource } from './posts.js';
@@ -66,6 +66,61 @@ export interface EntityImpressionsInput {
66
66
  /** 'shown' when a suggestion was on screen; 'engaged' on a tap, follow or join. Three showings without a tap rest a suggestion for fourteen days; engaging clears it. */
67
67
  event: 'shown' | 'engaged';
68
68
  }
69
+ /** One section of a category surface: bounded, read under its own deadline; empty with a `reason` when it timed out, failed, is off, or the recommender said why. */
70
+ export interface CategorySurfaceSection<T> {
71
+ items: T[];
72
+ ms: number;
73
+ reason?: string;
74
+ }
75
+ /** A radar cluster themed into the category, with up to six members (headline or excerpt and link only). */
76
+ export type CategoryStory = Pick<SocialRadar['clusters'][number], 'id' | 'label' | 'kind' | 'theme' | 'sources' | 'corroboration' | 'laneMix' | 'velocity' | 'media' | 'freshness' | 'score' | 'members'>;
77
+ export interface CategoryClip {
78
+ id: string;
79
+ title: string | null;
80
+ excerpt: string;
81
+ created_at: string;
82
+ author: {
83
+ id: string;
84
+ username: string | null;
85
+ name: string;
86
+ image: string | null;
87
+ };
88
+ media: Array<{
89
+ id: string;
90
+ url: string;
91
+ type: string;
92
+ thumbnail_url: string | null;
93
+ unplayable?: boolean;
94
+ }>;
95
+ engagement: {
96
+ score: number;
97
+ like_count: number;
98
+ reply_count: number;
99
+ repost_count: number;
100
+ };
101
+ }
102
+ export interface CategorySurface {
103
+ category: string;
104
+ /** When the oldest cached section served (stories, network, clips) was built. */
105
+ as_of: string;
106
+ stories: CategorySurfaceSection<CategoryStory>;
107
+ network: CategorySurfaceSection<SocialPost> & {
108
+ by_protocol: Record<string, number>;
109
+ };
110
+ clips: CategorySurfaceSection<CategoryClip>;
111
+ people: CategorySurfaceSection<EntityRecommendation>;
112
+ groups: CategorySurfaceSection<EntityRecommendation>;
113
+ }
114
+ export interface CategorySurfaceResponse {
115
+ /** Null when the key has no app (meta.reason `no_app`). */
116
+ data: CategorySurface | null;
117
+ meta: {
118
+ category: string;
119
+ as_of?: string;
120
+ reason?: 'no_app';
121
+ personalized: boolean;
122
+ };
123
+ }
69
124
  export interface DiscoveryGroupCandidate {
70
125
  id: string;
71
126
  name: string;
@@ -219,6 +274,12 @@ export interface DiscoveryFeedPage {
219
274
  hasMore: boolean;
220
275
  failedSources: string[];
221
276
  partialCategories: boolean;
277
+ /**
278
+ * The Minds feed's fall-through, once its category feed has ended a stage (the server's
279
+ * `meta.fallover`): where to read next when this session has no more. Absent until then, and
280
+ * on servers that predate it.
281
+ */
282
+ fallover?: FalloverStep[];
222
283
  }
223
284
  /** A gallery strip holds up to twelve image posts gathered from the whole page. */
224
285
  export declare const GALLERY_MAX_ITEMS = 12;
@@ -292,6 +353,22 @@ export declare class DiscoveryResource {
292
353
  reason?: 'disabled' | 'other_app' | 'unavailable' | 'no_app';
293
354
  };
294
355
  }>;
356
+ /**
357
+ * A category's surface in one call: the modules All has, scoped to the category. `stories` (radar
358
+ * clusters themed into it), `network` (open-network posts judged into it, newest first) and `clips`
359
+ * (public video posts among its hot window) are shared per app and category (refreshed after 30 s, kept up to 300 s, so a copy
360
+ * may be that old) and filtered for the viewer on every call: the cached safety verdicts and the promotion term list again, the
361
+ * viewer's blocks and mutes, and for stories and network the member's own hidden sources. `people` and `groups` are the
362
+ * viewer's suggestions in it. A section that timed out, failed or is off is empty
363
+ * with a `reason` and never delays the others. The posts feed itself is `posts.list({ category })`,
364
+ * whose pages carry `meta.next` and `meta.fallover`.
365
+ */
366
+ category(params: {
367
+ category: string;
368
+ session?: string;
369
+ }, options?: {
370
+ retry?: boolean;
371
+ }): Promise<CategorySurfaceResponse>;
295
372
  /**
296
373
  * A For you Discover feed: the member's For You ranking (asked for Discover's surface, with layouts and a
297
374
  * snapshot cursor) in a session's Minds slot. With a category, the session also reads open-network posts
@@ -22,6 +22,16 @@ const isGone = (error) => typeof error === 'object' && error !== null && error.c
22
22
  const TOPIC_WALK = 5;
23
23
  /** The walk starts no topic read after this long: well inside the page deadline, so it commits its progress rather than timing the network out. */
24
24
  const TOPIC_WALK_MS = 8000;
25
+ /**
26
+ * A topic page the server answered from its open-network source index (`sourceIndex`): in a category, a
27
+ * topic that is gone or holds nothing in it is served the network's indexed posts in the category, which
28
+ * are the same for every such topic. While the provider has more of the topic the page carries the
29
+ * provider's own cursor, which the walk keeps like any other; a topic that is gone or ended carries an
30
+ * `idx:tf:` cursor, the one index stream of the network. So a walk starts that stream at most once per
31
+ * session (a second start would re-read what it already showed), and an index page that shows nothing
32
+ * new counts as an empty topic.
33
+ */
34
+ const INDEX_CURSOR = 'idx:tf:';
25
35
  const RECOMMENDATION_KINDS = ['groups', 'people', 'tags'];
26
36
  const nonEmpty = (value) => typeof value === 'string' && value.trim().length > 0 && value.length <= 512;
27
37
  function recommendationItem(kind, candidate) {
@@ -188,6 +198,9 @@ export class DiscoveryFeedSession {
188
198
  if (stream.cursor)
189
199
  stream.used = [...stream.used, stream.cursor].slice(-32);
190
200
  stream.cursor = next;
201
+ // A category feed that ended a stage says where to go next; kept on the stream and reported on every page from here.
202
+ if (Array.isArray(response.meta.fallover))
203
+ stream.fallover = response.meta.fallover;
191
204
  return { items: response.data.map(post => ({ key: key('minds', post.id), kind: 'minds', post })),
192
205
  layout: response.meta.discovery ?? ordinary, partial: response.meta.category_filter?.status === 'partial' };
193
206
  }
@@ -237,9 +250,20 @@ export class DiscoveryFeedSession {
237
250
  const topic = topics[0];
238
251
  if (!topic)
239
252
  break;
253
+ // A failed read of the network's index stream (the server's index busy, or its safety cache down) ends that
254
+ // continuation, not the network: the stream may start again from a later topic's page. A provider topic
255
+ // read that fails any other way than gone is still the network's failure.
256
+ const indexContinuation = !!topic.cursor?.startsWith(INDEX_CURSOR);
240
257
  const response = await this.protocols.getPublicTrendFeed({ ...params, protocol: stream.protocol, topic: topic.topic, cursor: topic.cursor }, NO_RETRY)
241
- .catch((error) => { if (isGone(error))
242
- return null; throw error; });
258
+ .catch((error) => {
259
+ if (isGone(error))
260
+ return null;
261
+ if (indexContinuation) {
262
+ stream.indexStarted = false;
263
+ return null;
264
+ }
265
+ throw error;
266
+ });
243
267
  topics.shift();
244
268
  if (!response)
245
269
  continue;
@@ -248,12 +272,20 @@ export class DiscoveryFeedSession {
248
272
  if (data.trend.protocol !== stream.protocol || data.trend.nativeId !== topic.topic)
249
273
  throw new Error('Invalid discovery topic');
250
274
  const next = data.cursor;
251
- if (next && next !== topic.cursor && !topic.used.includes(next))
275
+ // A provider cursor is always kept. An index cursor from a provider page starts the network's one index
276
+ // stream: queued the first time only, since every later start would re-read the same stream from its top.
277
+ const indexed = !!data.sourceIndex;
278
+ const startsIndex = !!next?.startsWith(INDEX_CURSOR) && !topic.cursor?.startsWith(INDEX_CURSOR);
279
+ if (next && next !== topic.cursor && !topic.used.includes(next) && !(startsIndex && stream.indexStarted)) {
252
280
  topics.push({ topic: topic.topic, cursor: next, used: [...topic.used, ...(topic.cursor ? [topic.cursor] : [])].slice(-32) });
281
+ if (startsIndex)
282
+ stream.indexStarted = true;
283
+ }
253
284
  partial ||= data.categoryFilter?.status === 'partial';
254
- if (data.items.length) {
285
+ const items = indexed ? data.items.filter(post => !this.seen.has(key(post.protocol, post.nativeId))) : data.items;
286
+ if (items.length) {
255
287
  stream.done = !topics.length;
256
- return { items: data.items.map(post => ({ key: key(post.protocol, post.nativeId), kind: 'social', post })), layout: data.discovery ?? ordinary, partial };
288
+ return { items: items.map(post => ({ key: key(post.protocol, post.nativeId), kind: 'social', post })), layout: data.discovery ?? ordinary, partial };
257
289
  }
258
290
  }
259
291
  stream.done = !topics.length;
@@ -369,8 +401,10 @@ export class DiscoveryFeedSession {
369
401
  modules.splice(Math.min(2, modules.length), 0, recommendation);
370
402
  if (cluster)
371
403
  modules.splice(page === 0 ? 0 : Math.min(2, modules.length), 0, this.radarModule(page, cluster, clusterItems));
404
+ const fallover = this.streams.find(stream => stream.protocol === 'minds')?.fallover;
372
405
  return { modules, hasMore: this.streams.some(stream => !stream.done && !stream.failed),
373
- failedSources: this.streams.filter(stream => stream.failed).map(stream => stream.protocol), partialCategories: reads.some(read => read.partial) };
406
+ failedSources: this.streams.filter(stream => stream.failed).map(stream => stream.protocol), partialCategories: reads.some(read => read.partial),
407
+ ...(fallover ? { fallover } : {}) };
374
408
  }
375
409
  /** Page 0: the top breaking cluster, else the top cluster. Every second later page: the next unused story or viral cluster. */
376
410
  async radarCluster(page) {
@@ -534,6 +568,21 @@ export class DiscoveryResource {
534
568
  throw new Error('recordImpressions needs the HTTP client');
535
569
  return this.http.post('/discover/impressions', input);
536
570
  }
571
+ /**
572
+ * A category's surface in one call: the modules All has, scoped to the category. `stories` (radar
573
+ * clusters themed into it), `network` (open-network posts judged into it, newest first) and `clips`
574
+ * (public video posts among its hot window) are shared per app and category (refreshed after 30 s, kept up to 300 s, so a copy
575
+ * may be that old) and filtered for the viewer on every call: the cached safety verdicts and the promotion term list again, the
576
+ * viewer's blocks and mutes, and for stories and network the member's own hidden sources. `people` and `groups` are the
577
+ * viewer's suggestions in it. A section that timed out, failed or is off is empty
578
+ * with a `reason` and never delays the others. The posts feed itself is `posts.list({ category })`,
579
+ * whose pages carry `meta.next` and `meta.fallover`.
580
+ */
581
+ category(params, options) {
582
+ if (!this.http)
583
+ throw new Error('category needs the HTTP client');
584
+ return this.http.get('/discover/category', params, options);
585
+ }
537
586
  /**
538
587
  * A For you Discover feed: the member's For You ranking (asked for Discover's surface, with layouts and a
539
588
  * snapshot cursor) in a session's Minds slot. With a category, the session also reads open-network posts
@@ -37,12 +37,6 @@ export interface E2eeKeyPackageStatus {
37
37
  /** Of `available`, how many expire within 7 days. */
38
38
  expiringSoon: number;
39
39
  }
40
- /**
41
- * E2EE v2 ciphertext transport. The server relays envelopes and public
42
- * credentials only; encryption happens in the shared core on each device.
43
- * Requires the `e2eeV2` network feature flag and a key acting as the account
44
- * owner (agent-scoped keys are refused). The realm is the network id.
45
- */
46
40
  /** A reserved encrypted attachment object: PUT exactly `length` bytes of ciphertext to `uploadUrl`, then complete it. */
47
41
  export interface E2eeObjectUpload {
48
42
  reference: string;
@@ -56,9 +50,45 @@ export interface E2eeObjectDownload {
56
50
  downloadUrl: string;
57
51
  length: number;
58
52
  }
53
+ /**
54
+ * Why this account cannot use the E2EE relay on its network:
55
+ * - `network_off`: the network has not turned E2EE v2 on.
56
+ * - `not_allowlisted`: it is on, but only for pilot accounts, and this is not one.
57
+ */
58
+ export type E2eeStatusReason = 'network_off' | 'not_allowlisted';
59
+ /** Whether the calling account is admitted to the E2EE relay on its network. */
60
+ export interface E2eeStatus {
61
+ /**
62
+ * True: this account may enroll devices, publish key packages, send, and
63
+ * start encryption in an eligible direct conversation. False: the relay is
64
+ * frozen for it. Conversations that already carry encryption stay readable
65
+ * (fetch, acknowledge, object downloads), its own devices can be listed and
66
+ * revoked, and nothing new can be sent or enrolled. It never falls back to
67
+ * plaintext.
68
+ */
69
+ enabled: boolean;
70
+ /** Present only when `enabled` is false. Treat any unrecognised reason as off. */
71
+ reason?: E2eeStatusReason;
72
+ }
73
+ /**
74
+ * E2EE v2 ciphertext transport. The server relays envelopes and public
75
+ * credentials only; encryption happens in the shared core on each device.
76
+ * Requires the `e2eeV2` network feature flag and a key acting as the account
77
+ * owner (agent-scoped keys are refused). The realm is the network id.
78
+ */
59
79
  export declare class E2eeResource {
60
80
  private client;
61
81
  constructor(client: HttpClient);
82
+ /**
83
+ * Whether this account is admitted to encrypted chat on its network, without
84
+ * attempting a relay call. `{ enabled: true }`, or `{ enabled: false, reason }`
85
+ * with `reason` `'network_off'` or `'not_allowlisted'`. Only the caller's own
86
+ * account is described; the network comes from the key or session. Enrolling
87
+ * a device still needs an accepted two-person DM (or an already-encrypted
88
+ * conversation) in this key's scope. Agent-scoped keys get
89
+ * `agent_key_not_allowed`.
90
+ */
91
+ status(): Promise<SingleResponse<E2eeStatus>>;
62
92
  /** Register this device's public identity and its signed approval chain. */
63
93
  registerDevice(input: {
64
94
  device_name: string;
@@ -96,12 +126,28 @@ export declare class E2eeResource {
96
126
  bytes: string;
97
127
  device: E2eeDevice;
98
128
  }>>;
129
+ /**
130
+ * Append one MLS envelope to an encrypted conversation.
131
+ *
132
+ * `silent: true` marks a message that changes an earlier one (a reaction,
133
+ * an edit, a delete for everyone): the relay sends no push (so no unread
134
+ * badge either) and does not move the conversation up the chat list. The
135
+ * `e2ee:event` doorbell still rings exactly as for any message: on the
136
+ * sockets of every member, the sender's own account included, except
137
+ * across a block. So every listener hears a quiet event, not only an open
138
+ * thread: a chat list that pulls on the doorbell pulls it too, and should
139
+ * leave the row where it is once it has decrypted a reaction, an edit or a
140
+ * delete. A commit ignores the flag. The relay learns only that the event
141
+ * is quiet; the flag is not stored. A relay that predates it drops the
142
+ * key, so the event is simply not quiet there.
143
+ */
99
144
  appendEvent(conversationId: string, input: {
100
145
  device_id: string;
101
146
  envelope: E2eeEnvelope;
102
147
  welcome?: string;
103
148
  welcome_for_device_id?: string;
104
149
  expected_commit_seq?: number;
150
+ silent?: boolean;
105
151
  }): Promise<SingleResponse<{
106
152
  seq: number;
107
153
  duplicate: boolean;
@@ -1,8 +1,26 @@
1
+ /**
2
+ * E2EE v2 ciphertext transport. The server relays envelopes and public
3
+ * credentials only; encryption happens in the shared core on each device.
4
+ * Requires the `e2eeV2` network feature flag and a key acting as the account
5
+ * owner (agent-scoped keys are refused). The realm is the network id.
6
+ */
1
7
  export class E2eeResource {
2
8
  client;
3
9
  constructor(client) {
4
10
  this.client = client;
5
11
  }
12
+ /**
13
+ * Whether this account is admitted to encrypted chat on its network, without
14
+ * attempting a relay call. `{ enabled: true }`, or `{ enabled: false, reason }`
15
+ * with `reason` `'network_off'` or `'not_allowlisted'`. Only the caller's own
16
+ * account is described; the network comes from the key or session. Enrolling
17
+ * a device still needs an accepted two-person DM (or an already-encrypted
18
+ * conversation) in this key's scope. Agent-scoped keys get
19
+ * `agent_key_not_allowed`.
20
+ */
21
+ status() {
22
+ return this.client.get('/e2ee/status');
23
+ }
6
24
  /** Register this device's public identity and its signed approval chain. */
7
25
  registerDevice(input) {
8
26
  return this.client.post('/e2ee/devices', input);
@@ -28,6 +46,21 @@ export class E2eeResource {
28
46
  claimKeyPackage(input) {
29
47
  return this.client.post('/e2ee/key-packages/claim', input);
30
48
  }
49
+ /**
50
+ * Append one MLS envelope to an encrypted conversation.
51
+ *
52
+ * `silent: true` marks a message that changes an earlier one (a reaction,
53
+ * an edit, a delete for everyone): the relay sends no push (so no unread
54
+ * badge either) and does not move the conversation up the chat list. The
55
+ * `e2ee:event` doorbell still rings exactly as for any message: on the
56
+ * sockets of every member, the sender's own account included, except
57
+ * across a block. So every listener hears a quiet event, not only an open
58
+ * thread: a chat list that pulls on the doorbell pulls it too, and should
59
+ * leave the row where it is once it has decrypted a reaction, an edit or a
60
+ * delete. A commit ignores the flag. The relay learns only that the event
61
+ * is quiet; the flag is not stored. A relay that predates it drops the
62
+ * key, so the event is simply not quiet there.
63
+ */
31
64
  appendEvent(conversationId, input) {
32
65
  // Retries are safe: the server is idempotent on (conversation, device, event id).
33
66
  return this.client.post(`/e2ee/conversations/${encodeURIComponent(conversationId)}/events`, input, { retry: true });
@@ -15,6 +15,7 @@ export interface RequestHumanInputInput {
15
15
  export interface HumanInputRequest {
16
16
  conversation_id: string;
17
17
  message_id: string;
18
+ human_ask_id: string | null;
18
19
  /** Where to read the answer. MCP is stateless, so the agent polls. */
19
20
  poll: string;
20
21
  poll_after_seconds: number;
@@ -45,7 +46,9 @@ export declare class HumanInputResource {
45
46
  private client;
46
47
  constructor(client: HttpClient);
47
48
  /** Raise a blocking question and notify the human wherever they are. */
48
- request(input: RequestHumanInputInput): Promise<SingleResponse<HumanInputRequest>>;
49
+ request(input: RequestHumanInputInput, options?: {
50
+ requestId?: string;
51
+ }): Promise<SingleResponse<HumanInputRequest>>;
49
52
  /** Has the human answered yet? */
50
53
  answers(conversationId: string): Promise<SingleResponse<HumanInputAnswers>>;
51
54
  }
@@ -17,8 +17,13 @@ export class HumanInputResource {
17
17
  this.client = client;
18
18
  }
19
19
  /** Raise a blocking question and notify the human wherever they are. */
20
- request(input) {
21
- return this.client.post('/human-input', input);
20
+ request(input, options) {
21
+ // Never automatically replay: chat delivery may have committed before a
22
+ // response was lost, even when a keyed workspace ask was recorded once.
23
+ // Callers can reuse a requestId with the exact same input after an
24
+ // uncertain response. The server claims it against the durable
25
+ // organization-owned ask row; changed fields need a new requestId.
26
+ return this.client.post('/human-input', input, { retry: false, idempotencyKey: options?.requestId });
22
27
  }
23
28
  /** Has the human answered yet? */
24
29
  answers(conversationId) {
@@ -81,7 +81,11 @@ export declare class ModerationResource {
81
81
  * reachable through a valid key while the account is suspended or banned.
82
82
  */
83
83
  mine(): Promise<SingleResponse<OwnModerationLog>>;
84
- /** Appeal a decision affecting the caller or content they own. */
84
+ /**
85
+ * Appeal a decision affecting the caller or content they own. Sent once,
86
+ * never retried automatically: a 429 past the member's appeal budget
87
+ * (`RateLimitError`, `retryAfter`) is for the member to see.
88
+ */
85
89
  appeal(actionId: string, statement: string): Promise<SingleResponse<{
86
90
  success: true;
87
91
  id: string;
@@ -22,9 +22,13 @@ export class ModerationResource {
22
22
  mine() {
23
23
  return this.client.get('/appeals/moderation-actions');
24
24
  }
25
- /** Appeal a decision affecting the caller or content they own. */
25
+ /**
26
+ * Appeal a decision affecting the caller or content they own. Sent once,
27
+ * never retried automatically: a 429 past the member's appeal budget
28
+ * (`RateLimitError`, `retryAfter`) is for the member to see.
29
+ */
26
30
  appeal(actionId, statement) {
27
- return this.client.post(`/appeals/${encodeURIComponent(actionId)}`, { statement });
31
+ return this.client.post(`/appeals/${encodeURIComponent(actionId)}`, { statement }, { retry: false });
28
32
  }
29
33
  /** Admin review: uphold or overturn an appeal. */
30
34
  resolveAppeal(appealId, input) {
@@ -19,6 +19,11 @@ export interface NotificationActor {
19
19
  * state so inline Upvote and Repost can start in the right position. `null`
20
20
  * when the post is gone or no longer visible to the viewer.
21
21
  */
22
+ /** A post's tag as a notification's post preview carries it. */
23
+ export interface NotificationPostTag {
24
+ name: string;
25
+ slug: string;
26
+ }
22
27
  export interface NotificationPostPreview {
23
28
  id: string;
24
29
  /**
@@ -35,6 +40,20 @@ export interface NotificationPostPreview {
35
40
  excerpt_truncated?: boolean;
36
41
  media_url: string | null;
37
42
  media_type: string | null;
43
+ /**
44
+ * The post's own NSFW flag. With `tags`, it says whether the post is
45
+ * sensitive: a client that masks a sensitive post's card masks its excerpt
46
+ * and picture here too, and holds the picture back while this is absent
47
+ * (servers older than this field).
48
+ */
49
+ is_nsfw?: boolean;
50
+ /**
51
+ * The post's tags, each with its name as written and the slug the tag API
52
+ * derived from it ("nsfw", "nude", "porn" and the like mark it sensitive).
53
+ * Match both: a tag created as "NSFW!" is slugged "nsfw". Absent from
54
+ * servers older than this field.
55
+ */
56
+ tags?: NotificationPostTag[];
38
57
  reactions_count?: number;
39
58
  score?: number;
40
59
  upvote_count?: number;
@@ -1,5 +1,5 @@
1
1
  import type { HttpClient } from '../client.js';
2
- import type { PaginatedResponse, SingleResponse, SuccessResponse, Profile, ProfileDetail, ProfileMe, LeaderboardEntry, UpdateProfileInput, SearchProfilesParams, ProfileSearchResult, SearchMentionsParams, MentionCandidate, FollowListParams, FollowListProfile, BlockedProfile, MutedProfile, ModerationListParams, ViewerRelationship } from '../types.js';
2
+ import type { PaginatedResponse, SingleResponse, SuccessResponse, Profile, ProfileDetail, ProfileMe, LeaderboardEntry, UpdateProfileInput, SearchProfilesParams, ProfileSearchResult, SearchMentionsParams, MentionCandidate, FollowListParams, FollowListProfile, BlockedProfile, MutedProfile, ModerationListParams, ProfileRelationship } from '../types.js';
3
3
  export declare class ProfilesResource {
4
4
  private client;
5
5
  constructor(client: HttpClient);
@@ -60,8 +60,12 @@ export declare class ProfilesResource {
60
60
  cursor?: string;
61
61
  limit?: number;
62
62
  }): Promise<PaginatedResponse<string>>;
63
- /** The follow relationship with another user in both directions (mutual when both are true). */
64
- isFollowing(userId: string): Promise<SingleResponse<ViewerRelationship>>;
63
+ /**
64
+ * The caller's relationship with another user: follows in both directions
65
+ * (mutual when both are true), and whether the caller blocks (`is_blocking`)
66
+ * or mutes (`is_muting`) them, left out when the server cannot say.
67
+ */
68
+ isFollowing(userId: string): Promise<SingleResponse<ProfileRelationship>>;
65
69
  /**
66
70
  * Block a user. A block is a two-way wall: it severs follows in both
67
71
  * directions and hides each side's posts from the other.
@@ -76,7 +76,11 @@ export class ProfilesResource {
76
76
  followingIds(params) {
77
77
  return this.client.get('/profiles/me/following/ids', params);
78
78
  }
79
- /** The follow relationship with another user in both directions (mutual when both are true). */
79
+ /**
80
+ * The caller's relationship with another user: follows in both directions
81
+ * (mutual when both are true), and whether the caller blocks (`is_blocking`)
82
+ * or mutes (`is_muting`) them, left out when the server cannot say.
83
+ */
80
84
  isFollowing(userId) {
81
85
  return this.client.get(`/profiles/${userId}/is-following`);
82
86
  }
@@ -928,6 +928,8 @@ export interface SocialProfile {
928
928
  followersCount: number | null;
929
929
  followingCount: number | null;
930
930
  postsCount: number | null;
931
+ /** Nostr only: the NIP-05 name its domain has confirmed for this key ("lyn@primal.net"); `handle` stays the key. */
932
+ nip05?: string;
931
933
  subscription: SocialSubscription | null;
932
934
  }
933
935
  export interface SocialSubscription {
@@ -1073,6 +1075,20 @@ export interface SocialTrendFeed extends SocialTrendProvenance {
1073
1075
  trend: SocialTrend;
1074
1076
  items: SocialPost[];
1075
1077
  cursor: string | null;
1078
+ /**
1079
+ * Present when the page came from the app's open-network source index instead of the provider:
1080
+ * with a category, a topic the provider no longer lists (`gone`) or whose feed held nothing in the
1081
+ * category (`empty`) is served the network's indexed posts in the category. While the provider has
1082
+ * more of the topic, `cursor` is the provider's own continuation (the topic's deeper pages may hold
1083
+ * the category); once it has none, or the topic is gone, it is an `idx:tf:` cursor that continues in
1084
+ * the index (`cursor`), the same stream for every topic of the network. The `trend` of a gone topic
1085
+ * carries its id as its title. Pass the cursor back unchanged like any other. When the index cannot
1086
+ * answer, the page is what the provider answered (the empty page with its cursor, or the gone topic's
1087
+ * 404); only an `idx:tf:` page is 503 `unavailable`, which ends that continuation, not the network.
1088
+ */
1089
+ sourceIndex?: SocialTrendSourceIndex & {
1090
+ reason: 'gone' | 'empty' | 'cursor';
1091
+ };
1076
1092
  }
1077
1093
  /** A member of a radar cluster: a headline or excerpt and a link only, never a body. */
1078
1094
  export type SocialRadarSourceKind = 'outlet' | 'hackernews' | 'bluesky' | 'activitypub' | 'nostr' | 'minds';
@@ -1103,6 +1119,15 @@ export interface SocialRadarMember {
1103
1119
  height?: number;
1104
1120
  };
1105
1121
  providerRank?: number;
1122
+ /**
1123
+ * Present on a post member whose image or video a signed-out visitor may see (an open-network post with a cached
1124
+ * safe verdict, or a Minds post that passes the visitor's floor); absent on every other post member, and on members
1125
+ * of a radar built before the server recorded it. An outlet's media needs no mark. The keyless routes
1126
+ * (`getSharedRadar`, `getSharedRadarPosts`, `getSharedRadarStory`) serve media only from marked members and outlets.
1127
+ * A signed-in read (`getPublicRadar` and friends) keeps every member's media, so use only an outlet's or a marked
1128
+ * member's media on a signed-out page or a share card.
1129
+ */
1130
+ visitorMedia?: true;
1106
1131
  }
1107
1132
  /** One story on the radar. `breaking` is outlet corroboration inside a short window; `viral` is social velocity above its network's norm. */
1108
1133
  export interface SocialRadarCluster {
@@ -1127,6 +1152,7 @@ export interface SocialRadarCluster {
1127
1152
  normalized: number;
1128
1153
  samples: number;
1129
1154
  };
1155
+ /** On a signed-in read the lead may belong to a member without `visitorMedia`; find it by `memberKey` before a share card uses it. */
1130
1156
  media: {
1131
1157
  images: number;
1132
1158
  videos: number;
@@ -1317,7 +1343,10 @@ export declare class ProtocolsResource {
1317
1343
  category?: string;
1318
1344
  discovery?: boolean;
1319
1345
  }): Promise<SingleResponse<SocialTrendFeed>>;
1320
- /** Breaking and viral stories clustered across outlets, open networks and Minds, through the same public-only app boundary. */
1346
+ /**
1347
+ * Breaking and viral stories clustered across outlets, open networks and Minds, through the same public-only app boundary.
1348
+ * Images and videos come only from outlets and from members marked `visitorMedia`; every other member is text.
1349
+ */
1321
1350
  getSharedRadar(projectId: string, params?: SocialRadarParams): Promise<SingleResponse<SocialRadar>>;
1322
1351
  /** One radar story's social member posts without a key, so a signed-out story card can show the posts it counts. */
1323
1352
  getSharedRadarPosts(projectId: string, params: {
@@ -1379,7 +1408,10 @@ export declare class ProtocolsResource {
1379
1408
  category?: string;
1380
1409
  discovery?: boolean;
1381
1410
  }, options?: SocialReadOptions): Promise<SingleResponse<SocialTrendFeed>>;
1382
- /** The app's topic radar under its own key: the same snapshot the keyless route serves. Never triggers a build. */
1411
+ /**
1412
+ * The app's topic radar under its own key: the same snapshot the keyless route serves, for a signed-in reader, so
1413
+ * every member keeps its media (the keyless route serves only outlets' and `visitorMedia` members'). Never triggers a build.
1414
+ */
1383
1415
  getPublicRadar(params?: SocialRadarParams, options?: SocialReadOptions): Promise<SingleResponse<SocialRadar>>;
1384
1416
  /** One radar story's social member posts, so its card can show the posts it counts. Minds members are read with `posts.get`. */
1385
1417
  getPublicRadarPosts(params: {
@@ -102,7 +102,10 @@ export class ProtocolsResource {
102
102
  return this.client.get(`/protocols/public/${encodeURIComponent(projectId)}/trend-feed`, { protocol: params.protocol, topic: params.topic,
103
103
  limit: params.limit, cursor: params.cursor, category: params.category, discovery: params.discovery });
104
104
  }
105
- /** Breaking and viral stories clustered across outlets, open networks and Minds, through the same public-only app boundary. */
105
+ /**
106
+ * Breaking and viral stories clustered across outlets, open networks and Minds, through the same public-only app boundary.
107
+ * Images and videos come only from outlets and from members marked `visitorMedia`; every other member is text.
108
+ */
106
109
  getSharedRadar(projectId, params = {}) {
107
110
  return this.client.get(`/protocols/public/${encodeURIComponent(projectId)}/radar`, radarQuery(params));
108
111
  }
@@ -167,7 +170,10 @@ export class ProtocolsResource {
167
170
  ...(params.category !== undefined ? { category: params.category } : {}),
168
171
  ...(params.discovery !== undefined ? { discovery: params.discovery } : {}) }, ...readOptions(options));
169
172
  }
170
- /** The app's topic radar under its own key: the same snapshot the keyless route serves. Never triggers a build. */
173
+ /**
174
+ * The app's topic radar under its own key: the same snapshot the keyless route serves, for a signed-in reader, so
175
+ * every member keeps its media (the keyless route serves only outlets' and `visitorMedia` members'). Never triggers a build.
176
+ */
171
177
  getPublicRadar(params = {}, options) {
172
178
  return this.client.get('/protocols/social/radar', radarQuery(params), ...readOptions(options));
173
179
  }
@@ -35,6 +35,10 @@ export declare class ReportsResource {
35
35
  * Submit a content report. Targets a post, user, community, or message
36
36
  * by ID, with a short reason and optional free-text details. Reporter
37
37
  * is the authenticated user.
38
+ *
39
+ * Sent once, never retried automatically: past the member's report budget
40
+ * the server answers 429 (`RateLimitError`, `retryAfter` = seconds until
41
+ * the window opens), and that refusal is for the member to see.
38
42
  */
39
43
  create(input: CreateReportInput): Promise<SingleResponse<{
40
44
  success: true;
@@ -7,9 +7,13 @@ export class ReportsResource {
7
7
  * Submit a content report. Targets a post, user, community, or message
8
8
  * by ID, with a short reason and optional free-text details. Reporter
9
9
  * is the authenticated user.
10
+ *
11
+ * Sent once, never retried automatically: past the member's report budget
12
+ * the server answers 429 (`RateLimitError`, `retryAfter` = seconds until
13
+ * the window opens), and that refusal is for the member to see.
10
14
  */
11
15
  create(input) {
12
- return this.client.post('/reports', input);
16
+ return this.client.post('/reports', input, { retry: false });
13
17
  }
14
18
  /** List recent reports in the caller's project/network (admin only). */
15
19
  list() {