@recursiv/sdk 0.7.83 → 0.7.93

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 (40) hide show
  1. package/README.md +86 -2
  2. package/dist/assistant/index.d.ts +11 -3
  3. package/dist/assistant/index.js +11 -3
  4. package/dist/assistant/render.d.ts +25 -2
  5. package/dist/assistant/render.js +197 -16
  6. package/dist/assistant/subjectBundle.js +20 -3
  7. package/dist/assistant/turnCaps.d.ts +3 -0
  8. package/dist/assistant/turnCaps.js +141 -0
  9. package/dist/assistant/types.d.ts +223 -7
  10. package/dist/assistant/types.js +15 -0
  11. package/dist/index.d.ts +12 -6
  12. package/dist/index.js +8 -2
  13. package/dist/privacy/facts.generated.d.ts +79 -0
  14. package/dist/privacy/facts.generated.js +93 -0
  15. package/dist/privacy/index.d.ts +48 -0
  16. package/dist/privacy/index.js +42 -0
  17. package/dist/resources/discovery-merge.d.ts +17 -1
  18. package/dist/resources/discovery-merge.js +84 -4
  19. package/dist/resources/discovery-progressive.d.ts +4 -3
  20. package/dist/resources/discovery-progressive.js +11 -8
  21. package/dist/resources/discovery.d.ts +9 -1
  22. package/dist/resources/discovery.js +62 -15
  23. package/dist/resources/feeds.d.ts +15 -6
  24. package/dist/resources/feeds.js +2 -1
  25. package/dist/resources/integrations.d.ts +3 -0
  26. package/dist/resources/privacy.d.ts +13 -0
  27. package/dist/resources/privacy.js +14 -0
  28. package/dist/resources/protocols.d.ts +99 -11
  29. package/dist/resources/protocols.js +45 -0
  30. package/dist/resources/sealed.d.ts +127 -7
  31. package/dist/resources/sealed.js +33 -6
  32. package/dist/resources/settings.d.ts +3 -1
  33. package/dist/resources/settings.js +3 -1
  34. package/dist/resources/signals.d.ts +129 -6
  35. package/dist/resources/signals.js +58 -2
  36. package/dist/social-protocol-ids.d.ts +28 -0
  37. package/dist/social-protocol-ids.js +30 -0
  38. package/dist/types.d.ts +45 -0
  39. package/llm.md +17 -6
  40. package/package.json +6 -1
@@ -1,4 +1,6 @@
1
+ import { SOCIAL_PROTOCOL_IDS } from '../social-protocol-ids.js';
1
2
  import { readFalloverSteps } from './discovery-fallover.js';
3
+ import { discoveryItemIdentities } from './discovery-merge.js';
2
4
  import { createDiscoveryTopShare, createProgressiveFeed } from './discovery-progressive.js';
3
5
  export * from './discovery-fallover.js';
4
6
  export * from './discovery-first-screen.js';
@@ -142,7 +144,8 @@ function recommendationItem(kind, candidate) {
142
144
  return { key: `rec:tag:${(nonEmpty(value.slug) ? value.slug : value.name).toLowerCase()}`, kind: 'tag', tag: value };
143
145
  }
144
146
  const RADAR_KINDS = new Set(['breaking', 'viral', 'story']);
145
- const RADAR_SOCIAL_KINDS = new Set(['bluesky', 'activitypub', 'nostr']);
147
+ /** A radar member's open network (SOCIAL_PROTOCOL_IDS, Farcaster included before the radar holds any); another kind is skipped. */
148
+ const RADAR_SOCIAL_KINDS = new Set(SOCIAL_PROTOCOL_IDS);
146
149
  const httpUrl = (value) => typeof value === 'string' && value.length <= 4096 && /^https?:\/\//i.test(value);
147
150
  const countOf = (value) => typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : null;
148
151
  const dimension = (value) => typeof value === 'number' && Number.isSafeInteger(value) && value > 0 ? value : undefined;
@@ -271,7 +274,8 @@ export function pickRadarCluster(clusters, page, options = {}) {
271
274
  }
272
275
  return breaking[0] ?? unused[0];
273
276
  }
274
- const TOP_NETWORKS = new Set(['bluesky', 'activitypub', 'nostr']);
277
+ /** A feed top's open-network item or lane (SOCIAL_PROTOCOL_IDS); a session keeps only the networks it reads (`readyTop`). */
278
+ const TOP_NETWORKS = new Set(SOCIAL_PROTOCOL_IDS);
275
279
  const idString = (value) => typeof value === 'string' && value.length > 0 && value.length <= 4096;
276
280
  const counted = (value) => typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : null;
277
281
  /** The refs an app's seen set may hold for a post: its id, or '<protocol>/<nativeId>' (an ActivityPub id also without https://). */
@@ -339,6 +343,19 @@ function topItemSeenRefs(value) {
339
343
  }
340
344
  const ordinary = { version: 'discovery-layout-v1', presentation: 'posts', source: 'fallback' };
341
345
  const key = (protocol, id) => JSON.stringify([protocol, id]);
346
+ /** The judgment of a read made in `category`: its category filter's selection, when that is the category asked. */
347
+ const judgedInto = (category, filter) => (category && filter?.selected === category ? { judged: category } : {});
348
+ /** The judgment of a read's item at `index`. */
349
+ const judgmentAt = (read, index) => (read.judgments ? read.judgments[index] : read.judged);
350
+ /**
351
+ * A featured module's theme: its lead window's, only when most of the module's own posts were judged into it (see
352
+ * DiscoveryModule.theme). Never `mixed`.
353
+ */
354
+ function judgedTheme(theme, judged) {
355
+ if (!theme || theme === 'mixed' || !judged.length)
356
+ return undefined;
357
+ return judged.filter(category => category === theme).length * 2 > judged.length ? theme : undefined;
358
+ }
342
359
  const presentations = new Set(['posts', 'visual', 'conversation', 'video', 'thread', 'longread']);
343
360
  /** A gallery strip holds up to twelve image posts gathered from the whole page. */
344
361
  export const GALLERY_MAX_ITEMS = 12;
@@ -464,7 +481,7 @@ export class DiscoveryFeedSession {
464
481
  ? { key: key(row.post.protocol, row.post.nativeId), kind: 'social', post: row.post }
465
482
  : { key: key('minds', row.id), kind: 'minds', post: row }));
466
483
  const read = { layout: response.meta.discovery ?? ordinary, partial: response.meta.category_filter?.status === 'partial',
467
- ...(response.meta.compass_filter ? { compass: response.meta.compass_filter } : {}) };
484
+ ...(response.meta.compass_filter ? { compass: response.meta.compass_filter } : {}), ...judgedInto(this.category, response.meta.category_filter) };
468
485
  if (!this.filtering)
469
486
  return { items, ...read };
470
487
  head ??= read;
@@ -518,7 +535,7 @@ export class DiscoveryFeedSession {
518
535
  const kept = data.items.flatMap((post, index) => this.metNetworkPost(post) ? [] : [{ post, index }]);
519
536
  const layout = data.discovery ?? ordinary;
520
537
  return { items: kept.map(({ post }) => ({ key: key(post.protocol, post.nativeId), kind: 'social', post })), layout, partial: data.categoryFilter?.status === 'partial',
521
- ...(kept.length !== data.items.length ? { layouts: kept.map(({ index }) => windowLayout(layout, index)) } : {}) };
538
+ ...(kept.length !== data.items.length ? { layouts: kept.map(({ index }) => windowLayout(layout, index)) } : {}), ...judgedInto(this.category, data.categoryFilter) };
522
539
  }
523
540
  /** An open-network post in the member's seen set (options.seen: '<protocol>/<nativeId>', an ActivityPub id also without https://). */
524
541
  metNetworkPost(post) {
@@ -607,7 +624,8 @@ export class DiscoveryFeedSession {
607
624
  const items = (indexed ? data.items.filter(post => !this.seen.has(key(post.protocol, post.nativeId))) : data.items).filter(post => !this.metNetworkPost(post));
608
625
  if (items.length) {
609
626
  stream.done = !topics.length;
610
- return { items: items.map(post => ({ key: key(post.protocol, post.nativeId), kind: 'social', post })), layout: data.discovery ?? ordinary, partial };
627
+ return { items: items.map(post => ({ key: key(post.protocol, post.nativeId), kind: 'social', post })), layout: data.discovery ?? ordinary, partial,
628
+ ...judgedInto(this.category, data.categoryFilter) };
611
629
  }
612
630
  }
613
631
  while (topics[0] && passedOver() && !topics[0].cursor?.startsWith(INDEX_CURSOR))
@@ -680,17 +698,25 @@ export class DiscoveryFeedSession {
680
698
  // Round-robin before grouping: the largest network cannot occupy every slot.
681
699
  // Each item carries the judgment for its own window of its source response,
682
700
  // so one page can feature several modules instead of one per source batch.
701
+ // A post is shown once by every identity (discoveryItemIdentities): a second note carrying a clip already placed is
702
+ // the same post to a reader, on this page or a later one.
683
703
  const mixed = [];
684
704
  for (let index = 0; index < Math.max(0, ...reads.map(read => read.items.length)); index++)
685
705
  for (const read of reads) {
686
706
  const item = read.items[index];
687
- if (!item || this.seen.has(item.key))
707
+ if (!item)
708
+ continue;
709
+ const identities = discoveryItemIdentities(item);
710
+ if (identities.some(identity => this.seen.has(identity)))
688
711
  continue;
689
- this.seen.add(item.key);
690
- mixed.push({ item, layout: read.layouts?.[index] ?? windowLayout(read.layout, index) });
712
+ for (const identity of identities)
713
+ this.seen.add(identity);
714
+ const judged = judgmentAt(read, index);
715
+ mixed.push({ item, layout: read.layouts?.[index] ?? windowLayout(read.layout, index), ...(judged ? { judged } : {}) });
691
716
  }
692
- // Deduplication memory is bounded independently of the UI's virtual window.
693
- while (this.seen.size > 4096)
717
+ // Deduplication memory is bounded independently of the UI's virtual window: an open-network post holds a few
718
+ // identities (its key, its id and its files), so the bound that held 4096 keys holds four times as many entries.
719
+ while (this.seen.size > 16_384)
694
720
  this.seen.delete(this.seen.values().next().value);
695
721
  const modules = topItems.map((item, index) => this.topModule(this.page, index, item));
696
722
  const page = this.page++;
@@ -707,9 +733,22 @@ export class DiscoveryFeedSession {
707
733
  const onPage = remaining.filter(row => members.has(row.item.key)).map(row => row.item);
708
734
  const carried = new Set(onPage.map(item => item.key));
709
735
  // Members this page did not carry come from the story's own reads; one an earlier page showed stays shown there.
710
- const read = (await this.memberPosts(cluster)).filter(item => !carried.has(item.key) && !this.seen.has(item.key));
736
+ // They are taken one at a time in the story's member order, each adding its identities as it is kept, so two
737
+ // members of one read that carry one clip (a mirror note on another server) show once: the one the story lists first.
738
+ const order = (item) => cluster.memberKeys.indexOf(item.key);
739
+ const read = [];
740
+ for (const item of [...await this.memberPosts(cluster)].sort((a, b) => order(a) - order(b))) {
741
+ if (carried.has(item.key))
742
+ continue;
743
+ const identities = discoveryItemIdentities(item);
744
+ if (identities.some(identity => this.seen.has(identity)))
745
+ continue;
746
+ for (const identity of identities)
747
+ this.seen.add(identity);
748
+ read.push(item);
749
+ }
711
750
  // Under the story, posts keep the server's member order: the lead media member first, then by member score.
712
- clusterItems = [...onPage, ...read].sort((a, b) => cluster.memberKeys.indexOf(a.key) - cluster.memberKeys.indexOf(b.key));
751
+ clusterItems = [...onPage, ...read].sort((a, b) => order(a) - order(b));
713
752
  remaining = remaining.filter(row => !members.has(row.item.key));
714
753
  for (const memberKey of cluster.memberKeys)
715
754
  this.seen.add(memberKey);
@@ -742,7 +781,8 @@ export class DiscoveryFeedSession {
742
781
  const selected = visual ? images : discussion ? conversations : thread ? quotes : [first];
743
782
  const items = selected.map(row => row.item);
744
783
  const type = video ? 'DiscoveryVideo' : visual ? 'DiscoveryGallery' : discussion ? 'DiscoveryConversation' : thread ? 'DiscoveryThread' : longread ? 'DiscoveryLongRead' : 'DiscoveryPosts';
745
- const theme = presentation !== 'posts' && first.layout.theme && first.layout.theme !== 'mixed' ? first.layout.theme : undefined;
784
+ // Titled after its lead window's theme only when most of its own posts were judged into it (DiscoveryModule.theme).
785
+ const theme = presentation !== 'posts' ? judgedTheme(first.layout.theme, selected.map(row => row.judged)) : undefined;
746
786
  modules.push({ id: `discovery-${page}-${index}-${items[0].key}`, presentation, source: presentation === 'posts' ? 'fallback' : 'jev', items, ...(theme ? { theme } : {}),
747
787
  spec: { root: 'module', elements: { module: { type, props: { itemKeys: items.map(item => item.key) }, children: [] } } } });
748
788
  const selectedKeys = new Set(items.map(item => item.key));
@@ -848,7 +888,7 @@ export class DiscoveryFeedSession {
848
888
  }
849
889
  const lanes = new Map();
850
890
  const networks = value.networks && typeof value.networks === 'object' ? value.networks : {};
851
- for (const protocol of TOP_NETWORKS) {
891
+ for (const protocol of SOCIAL_PROTOCOL_IDS) {
852
892
  const entries = networks[protocol];
853
893
  if (!streams.has(protocol) || !Array.isArray(entries))
854
894
  continue;
@@ -917,7 +957,9 @@ export class DiscoveryFeedSession {
917
957
  const read = at >= 0 ? reads[at] : null;
918
958
  if (read) {
919
959
  const kept = read.previews ? [] : read.items;
920
- reads[at] = { ...read, items: [...lane, ...kept], layouts: [...lane.map(() => ordinary), ...kept.map((_item, index) => read.layouts?.[index] ?? windowLayout(read.layout, index))] };
960
+ // The lane's posts lead the read's; they carry no judgment of the read's category.
961
+ reads[at] = { ...read, items: [...lane, ...kept], layouts: [...lane.map(() => ordinary), ...kept.map((_item, index) => read.layouts?.[index] ?? windowLayout(read.layout, index))],
962
+ judgments: [...lane.map(() => undefined), ...kept.map((_item, index) => judgmentAt(read, index))] };
921
963
  }
922
964
  else if (this.streams.some(stream => stream.protocol === protocol))
923
965
  reads.push({ items: lane, layout: ordinary, partial: false });
@@ -930,10 +972,15 @@ export class DiscoveryFeedSession {
930
972
  const found = top.posts.get(row.key) ?? carried.get(row.key);
931
973
  if (!found || this.seen.has(row.key))
932
974
  continue;
975
+ const identities = discoveryItemIdentities(found);
976
+ if (identities.some(identity => this.seen.has(identity)))
977
+ continue;
933
978
  const position = top.positions.get(row.ref) ?? placed.length;
934
979
  placed.push({ ...found, top: { ref: row.ref, position, block: position < 10 ? 'top' : 'below' } });
935
980
  this.seen.add(row.key);
936
981
  this.topKeys.add(row.key);
982
+ for (const identity of identities)
983
+ this.seen.add(identity);
937
984
  }
938
985
  return placed;
939
986
  }
@@ -1,4 +1,5 @@
1
1
  import type { HttpClient } from '../client.js';
2
+ import { type SocialProtocolId } from '../social-protocol-ids.js';
2
3
  import type { CompassFilterMeta, StoryOutlets } from '../types.js';
3
4
  /**
4
5
  * The top of a feed: what the member (or a visitor) is most likely to engage with, rotated by what they have seen.
@@ -8,9 +9,17 @@ import type { CompassFilterMeta, StoryOutlets } from '../types.js';
8
9
  * Every tap target is in the app: a Minds post by id, an open-network post by `/social/post/<protocol>/<id>`, a story
9
10
  * by id. Nothing here is a URL on another network.
10
11
  */
11
- /** The open networks whose lanes a top carries. */
12
- export type FeedTopNetwork = 'bluesky' | 'activitypub' | 'nostr';
12
+ /**
13
+ * The open networks whose lanes a top may carry: every SOCIAL_PROTOCOL_IDS network (0.7.87: Farcaster before any top
14
+ * carries a Farcaster lane or item).
15
+ */
16
+ export type FeedTopNetwork = SocialProtocolId;
13
17
  export declare const FEED_TOP_NETWORKS: readonly FeedTopNetwork[];
18
+ /**
19
+ * A top's lanes, or their states, by network: Bluesky, ActivityPub and Nostr are always named; a network listed later
20
+ * (Farcaster) only once the API carries its lane, so read `networks.farcaster ?? []`.
21
+ */
22
+ export type FeedTopNetworkMap<T> = Record<'bluesky' | 'activitypub' | 'nostr', T> & Partial<Record<FeedTopNetwork, T>>;
14
23
  /** Where a tap on an item goes, always in the app. `path` is the app's own page of a network post, never its URL there. */
15
24
  export type FeedTopTarget = {
16
25
  kind: 'post';
@@ -103,7 +112,7 @@ export interface FeedStory {
103
112
  * are politics. Hide politics (`compass: 'hide'`) leaves such a story out. Absent from a server older than 0.7.67.
104
113
  */
105
114
  political?: boolean;
106
- sources: Partial<Record<'outlet' | 'hackernews' | 'bluesky' | 'activitypub' | 'nostr' | 'minds', number>>;
115
+ sources: Partial<Record<'outlet' | 'hackernews' | SocialProtocolId | 'minds', number>>;
107
116
  /** How many outlets reported it (StoryOutlets): the number every surface names for the story. Absent from a server older than 0.7.71. */
108
117
  outlets?: StoryOutlets;
109
118
  media: {
@@ -141,13 +150,13 @@ export interface FeedTopMeta {
141
150
  /** 'safe' for a member on safe defaults, 'open' for one who turned them off. */
142
151
  safety: 'safe' | 'open';
143
152
  mindsSource: 'member' | 'anonymous' | 'none' | 'following';
144
- networks: Record<FeedTopNetwork, 'ok' | 'unavailable'> | null;
153
+ networks: FeedTopNetworkMap<'ok' | 'unavailable'> | null;
145
154
  compass_filter?: CompassFilterMeta;
146
155
  }
147
156
  /** GET /feeds/top. `networks` is null on a feed with no lanes (Following, a story); `lead` is null when the feed has no story. */
148
157
  export interface FeedTopResponse {
149
158
  items: FeedTopItem[];
150
- networks: Record<FeedTopNetwork, FeedTopNetworkEntry[]> | null;
159
+ networks: FeedTopNetworkMap<FeedTopNetworkEntry[]> | null;
151
160
  lead: FeedStory | null;
152
161
  meta: FeedTopMeta;
153
162
  }
@@ -179,7 +188,7 @@ export interface PublicFeedTopMeta {
179
188
  /** A visitor's top (`feeds.publicTop`): safe items only, the same for every visitor in one 15-minute bucket. */
180
189
  export interface PublicFeedTopResponse {
181
190
  items: FeedTopItem[];
182
- networks: Record<FeedTopNetwork, FeedTopNetworkEntry[]> | null;
191
+ networks: FeedTopNetworkMap<FeedTopNetworkEntry[]> | null;
183
192
  lead: FeedStory | null;
184
193
  meta: PublicFeedTopMeta;
185
194
  }
@@ -1,5 +1,6 @@
1
1
  import { NotFoundError, RecursivError } from '../errors.js';
2
- export const FEED_TOP_NETWORKS = ['bluesky', 'activitypub', 'nostr'];
2
+ import { SOCIAL_PROTOCOL_IDS } from '../social-protocol-ids.js';
3
+ export const FEED_TOP_NETWORKS = SOCIAL_PROTOCOL_IDS;
3
4
  export class FeedsResource {
4
5
  http;
5
6
  constructor(http) {
@@ -90,6 +90,9 @@ export interface UpdateAgentIntegrationInput {
90
90
  allowed_tools?: string[];
91
91
  }
92
92
  export interface PendingToolExecution {
93
+ /** First-party builder identity is supplied by the server, never inferred from tool name. */
94
+ builder_approval?: true;
95
+ details_withheld?: boolean;
93
96
  id: string;
94
97
  conversation_id: string;
95
98
  agent: {
@@ -0,0 +1,13 @@
1
+ import type { HttpClient } from '../client.js';
2
+ import type { PrivacyCapabilities } from '../privacy/index.js';
3
+ import type { SingleResponse } from '../types.js';
4
+ /**
5
+ * What the deployed API enforces. Pair it with `@recursiv/sdk/privacy`, which says what each fact means and whether
6
+ * the installed SDK marks it enforced: render a privacy sentence only when both do (`isFactLive`).
7
+ */
8
+ export declare class PrivacyResource {
9
+ private client;
10
+ constructor(client: HttpClient);
11
+ /** GET /privacy/capabilities: the platform facts this deployment's build enforces, and its commit. Any API key. */
12
+ capabilities(): Promise<SingleResponse<PrivacyCapabilities>>;
13
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * What the deployed API enforces. Pair it with `@recursiv/sdk/privacy`, which says what each fact means and whether
3
+ * the installed SDK marks it enforced: render a privacy sentence only when both do (`isFactLive`).
4
+ */
5
+ export class PrivacyResource {
6
+ client;
7
+ constructor(client) {
8
+ this.client = client;
9
+ }
10
+ /** GET /privacy/capabilities: the platform facts this deployment's build enforces, and its commit. Any API key. */
11
+ capabilities() {
12
+ return this.client.get('/privacy/capabilities');
13
+ }
14
+ }
@@ -1,6 +1,7 @@
1
1
  import type { HttpClient } from '../client.js';
2
2
  import type { CompassFilterMeta, DiscoveryLayout, PaginatedResponse, PaginationParams, SingleResponse, StoryOutlets } from '../types.js';
3
3
  import type { CuratorStoryPostsPage } from './curator.js';
4
+ import type { NativeWriteProtocol, SocialProtocolId } from '../social-protocol-ids.js';
4
5
  /** Canonical protocol operation names returned by the adapter registry. */
5
6
  export declare const PROTOCOL_CAPABILITY_OPERATIONS: readonly ["discover", "stream", "backfill", "search", "resolveIdentity", "fetchProfile", "fetchThread", "publish", "reply", "react", "repost", "follow", "delete", "health", "checkpoint"];
6
7
  export declare const PROTOCOL_CAPABILITY_STATUSES: readonly ["supported", "planned", "unsupported"];
@@ -426,8 +427,55 @@ export interface FederationIdentityView {
426
427
  origin: string | null;
427
428
  identity: FederationIdentity | null;
428
429
  }
430
+ /** A native network every eligible member is on by rule (Farcaster joins later). */
431
+ export type NativeNetwork = 'activitypub' | 'atproto' | 'nostr';
432
+ /**
433
+ * A member's state on one native network. `ready`: on by rule, set up on their first public post, first interaction
434
+ * there, or `activateNetwork` ("Will be set up when you first post or interact"). `pending`: being set up, or waiting
435
+ * for the setup budget (`retryAt`). `off`: the member turned it off (`reason: 'opted_out'`), it is not available to them
436
+ * yet (`not_rolled_out`), or their profile is private (`private_profile`, AT Protocol and Nostr only; `setNetwork`
437
+ * with `enabled: true` turns it on). `needs_action`: the member must act first (`age_band_required`,
438
+ * `needs_verified_email`). `locked`: held off by a rule the member cannot change.
439
+ */
440
+ export type NativeNetworkState = 'ready' | 'on' | 'pending' | 'off' | 'error' | 'locked' | 'needs_action';
441
+ export interface NativeNetworkPresence {
442
+ network: NativeNetwork;
443
+ state: NativeNetworkState;
444
+ /** Why, as a code: `opted_out`, `not_rolled_out`, `private_profile`, `age_band_required`, `needs_verified_email`, `activation_failed`... */
445
+ reason: string | null;
446
+ /** `hosted` (a Minds-run ActivityPub actor or AT Protocol account), `managed` (a Minds-held Nostr key) or `attached`. */
447
+ mode: 'hosted' | 'managed' | 'attached';
448
+ /** The member's address on the network, set up or not, when the server can say it. */
449
+ handle: string | null;
450
+ /** Which identity a one-tap like, repost, reply or follow uses when the member also linked an outside account. */
451
+ engageDefault: 'native' | 'linked';
452
+ activatedAt: string | null;
453
+ optedOutAt: string | null;
454
+ removePast: boolean;
455
+ }
456
+ export interface NativeNetworkActivation {
457
+ outcome: 'activated' | 'already_on' | 'pending' | 'in_flight' | 'needs_action' | 'error' | 'not_eligible' | 'refused' | 'unavailable';
458
+ presence: NativeNetworkPresence;
459
+ /** When the setup budget frees, for `pending`. */
460
+ retryAt: string | null;
461
+ }
462
+ export interface NativeNetworkOff {
463
+ outcome: 'off';
464
+ presence: NativeNetworkPresence;
465
+ /** `failed`: the member is off, but removing what was shared did not start; call `setNetwork` again to retry it. */
466
+ retraction: 'queued' | 'failed' | 'none';
467
+ }
468
+ export interface SetNativeNetworkInput {
469
+ enabled: boolean;
470
+ /** Turning off: also remove what was already shared there (the Settings sheet checks this by default). */
471
+ removePast?: boolean;
472
+ }
429
473
  export interface SocialSourceRef {
430
- protocol: 'bluesky' | 'activitypub' | 'nostr';
474
+ /**
475
+ * 0.7.87: any SOCIAL_PROTOCOL_IDS network, Farcaster (an FID, or a `<fid>:0x<hash>` CastId) before its reader serves a
476
+ * post. A source to show, mute, hide or report; a native write takes only a NativeWriteProtocol network.
477
+ */
478
+ protocol: SocialProtocolId;
431
479
  kind: 'actor' | 'post';
432
480
  nativeId: string;
433
481
  }
@@ -474,7 +522,7 @@ export interface NativeReplyPrepareInput {
474
522
  clientMutationId: string;
475
523
  text: string;
476
524
  parent: {
477
- protocol: SocialSourceRef['protocol'];
525
+ protocol: NativeWriteProtocol;
478
526
  nativeId: string;
479
527
  readRef?: string;
480
528
  };
@@ -491,7 +539,7 @@ export interface NativeReplyPrepareInput {
491
539
  }
492
540
  export interface NativeFollowSourceInput {
493
541
  accountId: string;
494
- protocol: SocialSourceRef['protocol'];
542
+ protocol: NativeWriteProtocol;
495
543
  nativeId: string;
496
544
  }
497
545
  export interface NativeFollowPrepareInput extends NativeFollowSourceInput {
@@ -536,7 +584,7 @@ export interface NativeFollowState extends NativeFollowReceipt {
536
584
  /** One connected account on the source's network, as the account lists describe it. */
537
585
  export interface NativeFollowAccount {
538
586
  id: string;
539
- protocol: SocialSourceRef['protocol'];
587
+ protocol: NativeWriteProtocol;
540
588
  nativeId: string;
541
589
  handle: string;
542
590
  serverUrl: string;
@@ -545,7 +593,7 @@ export interface NativeFollowAccount {
545
593
  }
546
594
  /** Every connected account's relationship with one source, from one read. */
547
595
  export interface NativeFollowStates {
548
- protocol: SocialSourceRef['protocol'];
596
+ protocol: NativeWriteProtocol;
549
597
  nativeId: string;
550
598
  /** False when native follows are off for this app; accounts are still listed, and no state is read. */
551
599
  enabled: boolean;
@@ -578,7 +626,7 @@ export interface NativeFollowReview {
578
626
  }
579
627
  export interface NativeLikeSourceInput {
580
628
  accountId: string;
581
- protocol: SocialSourceRef['protocol'];
629
+ protocol: NativeWriteProtocol;
582
630
  nativeId: string;
583
631
  }
584
632
  export interface NativeLikePrepareInput extends NativeLikeSourceInput {
@@ -644,7 +692,7 @@ export interface NativeLikeReview {
644
692
  }
645
693
  export interface NativeRepostSourceInput {
646
694
  accountId: string;
647
- protocol: SocialSourceRef['protocol'];
695
+ protocol: NativeWriteProtocol;
648
696
  nativeId: string;
649
697
  }
650
698
  export interface NativeRepostPrepareInput extends NativeRepostSourceInput {
@@ -773,7 +821,7 @@ export interface NativeReplyBatch {
773
821
  serverUrl: string;
774
822
  } | null;
775
823
  parent: {
776
- protocol: SocialSourceRef['protocol'];
824
+ protocol: NativeWriteProtocol;
777
825
  nativeId: string;
778
826
  sourceUrl: string;
779
827
  authorId: string;
@@ -910,7 +958,7 @@ export interface NativeNostrReplyResult {
910
958
  text: string;
911
959
  /** `readRef`: a linked ActivityPub source's public reader id, for the conversation read (replies saved before it existed have none). */
912
960
  source: {
913
- protocol: SocialSourceRef['protocol'];
961
+ protocol: NativeWriteProtocol;
914
962
  nativeId: string;
915
963
  sourceUrl: string;
916
964
  readRef?: string;
@@ -1321,7 +1369,8 @@ export interface SocialTrendFeed extends SocialTrendProvenance {
1321
1369
  };
1322
1370
  }
1323
1371
  /** A member of a radar cluster: a headline or excerpt and a link only, never a body. */
1324
- export type SocialRadarSourceKind = 'outlet' | 'hackernews' | 'bluesky' | 'activitypub' | 'nostr' | 'minds';
1372
+ /** 0.7.87: an open-network member's kind is any SOCIAL_PROTOCOL_IDS network, Farcaster before the radar holds any. */
1373
+ export type SocialRadarSourceKind = 'outlet' | 'hackernews' | SocialProtocolId | 'minds';
1325
1374
  export interface SocialRadarMember {
1326
1375
  kind: SocialRadarSourceKind;
1327
1376
  /** outlet: the article url; hackernews: the `hn:<id>` candidate id; social: JSON `[protocol, nativeId]`; minds: the post id. */
@@ -1748,12 +1797,27 @@ export interface SocialConversation {
1748
1797
  hiddenByViewer?: true;
1749
1798
  viewerControls?: Array<Pick<SocialSourceControl, 'source' | 'control'>>;
1750
1799
  }
1800
+ /**
1801
+ * A followed account's status on a Following page, in the server's order (SocialFollowingFeed.ts
1802
+ * FOLLOWING_SOURCE_STATUSES). `late` (added after 0.7.82): the account's read had not answered at the page's deadline;
1803
+ * it is read again on a later page and is never a failure. APIs before it name such an account `unavailable` with
1804
+ * `late: true`.
1805
+ */
1806
+ export declare const SOCIAL_FOLLOWING_SOURCE_STATUSES: readonly ["ok", "late", "unavailable", "rate_limited", "unsupported_protocol"];
1807
+ export type SocialFollowingSourceStatus = typeof SOCIAL_FOLLOWING_SOURCE_STATUSES[number];
1751
1808
  export interface SocialFollowingSource {
1752
1809
  subscriptionId: string;
1753
1810
  protocol: string;
1754
1811
  nativeId: string;
1755
- status: 'ok' | 'unavailable' | 'rate_limited' | 'unsupported_protocol';
1812
+ /**
1813
+ * `late`: the account's read has not answered yet; it is read again on a later page. `ok` with `late: true`: the
1814
+ * account's last-held first page, served while its fresh read goes on. `unavailable` is a read that failed, or one
1815
+ * still late after its reads again.
1816
+ */
1817
+ status: SocialFollowingSourceStatus;
1756
1818
  moreAvailable: boolean | null;
1819
+ /** The account is read again on a later page: never show it as a failure; read the next page for its posts. */
1820
+ late?: true;
1757
1821
  }
1758
1822
  /** A bounded batch of recent author windows. Continue the cursor to read more followed accounts. */
1759
1823
  export interface SocialFollowingFeed {
@@ -1764,6 +1828,10 @@ export interface SocialFollowingFeed {
1764
1828
  totalAccounts: number;
1765
1829
  postsPerAccount: number;
1766
1830
  coverage: 'recent_original_posts';
1831
+ /** The networks with an account named late on this page (absent from older APIs). */
1832
+ lateNetworks?: string[];
1833
+ /** How long this page waited for an account's first read on each network, in ms (absent from older APIs). */
1834
+ deadlineMs?: number;
1767
1835
  }
1768
1836
  export declare class ProtocolsResource {
1769
1837
  private client;
@@ -2134,6 +2202,26 @@ export declare class ProtocolsResource {
2134
2202
  disconnected: true;
2135
2203
  providerRevocation: 'acknowledged' | 'unconfirmed';
2136
2204
  }>>;
2205
+ /**
2206
+ * The signed-in member's native networks in the key's app (ActivityPub, AT Protocol, Nostr): each one's state, reason
2207
+ * and address. Read-only: it never sets anything up. Needs `users:read`.
2208
+ */
2209
+ networks(): Promise<SingleResponse<{
2210
+ networks: NativeNetworkPresence[];
2211
+ }>>;
2212
+ /**
2213
+ * "Set up now": sets up a network that is `ready` (or `pending`, or failed) at once, instead of on the member's first
2214
+ * post. Never undoes their off switch (409 `network_off`) or a private profile's rule (409 `private_profile`): use
2215
+ * `setNetwork` for those. Over the setup budget it answers `pending` with `retryAt`. Needs `users:write`.
2216
+ */
2217
+ activateNetwork(network: NativeNetwork): Promise<SingleResponse<NativeNetworkActivation>>;
2218
+ /**
2219
+ * The member's switch for one network. `{ enabled: false, removePast: true }` turns it off and removes what was
2220
+ * shared there (reversible: turning it back on serves the same identity; remote followers may need to follow again).
2221
+ * `{ enabled: true }` turns it back on or sets it up. Refusals: 403 `network_locked`, 409 `not_rolled_out` or
2222
+ * `age_band_required`. Needs `users:write`.
2223
+ */
2224
+ setNetwork(network: NativeNetwork, input: SetNativeNetworkInput): Promise<SingleResponse<NativeNetworkActivation | NativeNetworkOff>>;
2137
2225
  /** Current Minds-hosted ActivityPub state for this app user; never provisions. */
2138
2226
  federationIdentity(): Promise<SingleResponse<FederationIdentityView>>;
2139
2227
  /** Explicit opt-in; the platform provisions once and never rotates keys or re-enables a disabled identity on retry. */
@@ -28,6 +28,13 @@ export const PROTOCOL_ADAPTER_MATURITIES = Object.freeze([
28
28
  'beta',
29
29
  'stable',
30
30
  ]);
31
+ const NATIVE_NETWORKS = ['activitypub', 'atproto', 'nostr'];
32
+ function nativeNetworkPath(network) {
33
+ if (!NATIVE_NETWORKS.includes(network)) {
34
+ throw new ValidationError({ type: 'validation_error', code: 'invalid_network', message: 'network must be activitypub, atproto or nostr' });
35
+ }
36
+ return `/protocols/networks/${network}`;
37
+ }
31
38
  function hostedIdentityPath(id) {
32
39
  if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id)) {
33
40
  throw new ValidationError({ type: 'validation_error', code: 'invalid_identity_id', message: 'A valid hosted identity connection ID is required' });
@@ -42,6 +49,19 @@ function hostedIdentityPath(id) {
42
49
  export function viralItemHasVideo(item) {
43
50
  return item.post.media.some(media => media.type === 'video');
44
51
  }
52
+ /**
53
+ * A followed account's status on a Following page, in the server's order (SocialFollowingFeed.ts
54
+ * FOLLOWING_SOURCE_STATUSES). `late` (added after 0.7.82): the account's read had not answered at the page's deadline;
55
+ * it is read again on a later page and is never a failure. APIs before it name such an account `unavailable` with
56
+ * `late: true`.
57
+ */
58
+ export const SOCIAL_FOLLOWING_SOURCE_STATUSES = Object.freeze([
59
+ 'ok',
60
+ 'late',
61
+ 'unavailable',
62
+ 'rate_limited',
63
+ 'unsupported_protocol',
64
+ ]);
45
65
  /** Only the radar's own fields travel; caller scope or destination extras never reach the query string. */
46
66
  /** Forwarded only when given, so a default read makes exactly the call it always made. */
47
67
  const readOptions = (options) => options ? [options] : [];
@@ -488,6 +508,31 @@ export class ProtocolsResource {
488
508
  hostedIdentityPath(id);
489
509
  return this.client.delete(`/protocols/social/accounts/${encodeURIComponent(id)}`);
490
510
  }
511
+ /**
512
+ * The signed-in member's native networks in the key's app (ActivityPub, AT Protocol, Nostr): each one's state, reason
513
+ * and address. Read-only: it never sets anything up. Needs `users:read`.
514
+ */
515
+ networks() {
516
+ return this.client.get('/protocols/networks');
517
+ }
518
+ /**
519
+ * "Set up now": sets up a network that is `ready` (or `pending`, or failed) at once, instead of on the member's first
520
+ * post. Never undoes their off switch (409 `network_off`) or a private profile's rule (409 `private_profile`): use
521
+ * `setNetwork` for those. Over the setup budget it answers `pending` with `retryAt`. Needs `users:write`.
522
+ */
523
+ activateNetwork(network) {
524
+ return this.client.post(`${nativeNetworkPath(network)}/activate`, {});
525
+ }
526
+ /**
527
+ * The member's switch for one network. `{ enabled: false, removePast: true }` turns it off and removes what was
528
+ * shared there (reversible: turning it back on serves the same identity; remote followers may need to follow again).
529
+ * `{ enabled: true }` turns it back on or sets it up. Refusals: 403 `network_locked`, 409 `not_rolled_out` or
530
+ * `age_band_required`. Needs `users:write`.
531
+ */
532
+ setNetwork(network, input) {
533
+ const body = input.enabled ? { enabled: true } : { enabled: false, ...(input.removePast !== undefined ? { remove_past: input.removePast } : {}) };
534
+ return this.client.put(nativeNetworkPath(network), body);
535
+ }
491
536
  /** Current Minds-hosted ActivityPub state for this app user; never provisions. */
492
537
  federationIdentity() {
493
538
  return this.client.get('/protocols/social/federation/identity');