@recursiv/sdk 0.7.152 → 0.7.195
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 +75 -5
- package/dist/index.d.ts +12 -7
- package/dist/index.js +6 -1
- package/dist/resources/alive.d.ts +61 -0
- package/dist/resources/alive.js +16 -0
- package/dist/resources/boosts.d.ts +110 -1
- package/dist/resources/boosts.js +80 -0
- package/dist/resources/deployments.d.ts +5 -1
- package/dist/resources/discovery.d.ts +5 -2
- package/dist/resources/evidence.d.ts +57 -2
- package/dist/resources/evidence.js +19 -0
- package/dist/resources/feeds.d.ts +63 -0
- package/dist/resources/feeds.js +12 -0
- package/dist/resources/protocols.d.ts +336 -12
- package/dist/resources/protocols.js +146 -4
- package/dist/resources/search.d.ts +71 -4
- package/dist/resources/settings.d.ts +3 -2
- package/dist/resources/settings.js +3 -2
- package/dist/types.d.ts +14 -1
- package/llm.md +37 -5
- package/package.json +1 -1
|
@@ -28,7 +28,22 @@ export const PROTOCOL_ADAPTER_MATURITIES = Object.freeze([
|
|
|
28
28
|
'beta',
|
|
29
29
|
'stable',
|
|
30
30
|
]);
|
|
31
|
+
function nostrStepUpBody(stepUp) {
|
|
32
|
+
const password = typeof stepUp?.password === 'string' ? stepUp.password : '';
|
|
33
|
+
const emailCode = typeof stepUp?.emailCode === 'string' ? stepUp.emailCode.trim() : '';
|
|
34
|
+
if (Boolean(password) === Boolean(emailCode)) {
|
|
35
|
+
throw new ValidationError({ type: 'validation_error', code: 'invalid_input', message: 'Send the account password, or the emailed nostr_key code, not both' });
|
|
36
|
+
}
|
|
37
|
+
return password ? { password } : { email_code: emailCode };
|
|
38
|
+
}
|
|
31
39
|
const NATIVE_NETWORKS = ['activitypub', 'atproto', 'nostr'];
|
|
40
|
+
function engageOneBody(input) {
|
|
41
|
+
if (input.identity !== undefined && input.identity !== 'native' && input.identity !== 'linked') {
|
|
42
|
+
throw new ValidationError({ type: 'validation_error', code: 'invalid_input', message: 'identity must be native or linked' });
|
|
43
|
+
}
|
|
44
|
+
return { clientMutationId: input.clientMutationId, protocol: input.protocol, nativeId: input.nativeId, action: input.action,
|
|
45
|
+
...(input.identity !== undefined ? { identity: input.identity } : {}), ...(input.accountId !== undefined ? { accountId: input.accountId } : {}) };
|
|
46
|
+
}
|
|
32
47
|
function nativeNetworkPath(network) {
|
|
33
48
|
if (!NATIVE_NETWORKS.includes(network)) {
|
|
34
49
|
throw new ValidationError({ type: 'validation_error', code: 'invalid_network', message: 'network must be activitypub, atproto or nostr' });
|
|
@@ -201,6 +216,15 @@ export class ProtocolsResource {
|
|
|
201
216
|
throw error;
|
|
202
217
|
}
|
|
203
218
|
}
|
|
219
|
+
/**
|
|
220
|
+
* A category's stories, open-network posts and clips for a signed-out visitor, in one keyless read (SharedCategorySurface).
|
|
221
|
+
* The answer is the same for every visitor of the app, category and Politics choice, and the URL carries nothing else,
|
|
222
|
+
* so a shared cache may keep it for 30 s. Only `category` and `compass` are sent: the server refuses any other
|
|
223
|
+
* parameter. An API that predates the route answers 401 `missing_api_key` (it falls through to the keyed routes) or 404.
|
|
224
|
+
*/
|
|
225
|
+
getSharedCategorySurface(projectId, params, options) {
|
|
226
|
+
return this.client.get(`/protocols/public/${encodeURIComponent(projectId)}/category`, { category: params.category, ...(params.compass !== undefined ? { compass: params.compass } : {}) }, ...readOptions(options));
|
|
227
|
+
}
|
|
204
228
|
publicNetworks() {
|
|
205
229
|
return this.client.get('/protocols/social/networks');
|
|
206
230
|
}
|
|
@@ -555,6 +579,122 @@ export class ProtocolsResource {
|
|
|
555
579
|
const body = input.enabled ? { enabled: true } : { enabled: false, ...(input.removePast !== undefined ? { remove_past: input.removePast } : {}) };
|
|
556
580
|
return this.client.put(nativeNetworkPath(network), body);
|
|
557
581
|
}
|
|
582
|
+
/**
|
|
583
|
+
* Which identity a one-tap action uses on a network when the member also linked an outside account there (0.7.191).
|
|
584
|
+
* `linked` needs a linked account (409 `linked_account_required`); `native` needs the network on, ready or pending
|
|
585
|
+
* (409 `native_network_off`) and a network that can send a tap as the member's identity (503
|
|
586
|
+
* `native_network_unavailable`, reason `adapter_missing`, otherwise), and sets the identity up now when that is what the
|
|
587
|
+
* choice needs. Needs `users:write`.
|
|
588
|
+
*/
|
|
589
|
+
setEngageDefault(network, engageDefault) {
|
|
590
|
+
if (engageDefault !== 'native' && engageDefault !== 'linked') {
|
|
591
|
+
throw new ValidationError({ type: 'validation_error', code: 'invalid_input', message: 'engageDefault must be native or linked' });
|
|
592
|
+
}
|
|
593
|
+
return this.client.put(`${nativeNetworkPath(network)}/engage-default`, { engage_default: engageDefault });
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* "Get my AT Protocol password" (0.7.191): the PDS emails a reset code to the member's verified address, the one their hosted AT
|
|
597
|
+
* Protocol account uses; Minds never holds or shows that password (it keeps only a revocable app password, which the
|
|
598
|
+
* member can revoke from any AT Protocol app). Needs a step-up (403 `step_up_required` / `step_up_failed`). Other
|
|
599
|
+
* refusals: 404 `no_identity`, 409 `needs_verified_email` or `email_changed`, 429 `rate_limited`. Needs `users:write`.
|
|
600
|
+
* Never auto-retried.
|
|
601
|
+
*/
|
|
602
|
+
claimAtprotoPassword(proof) {
|
|
603
|
+
const body = 'password' in proof ? { password: proof.password } : { email_code: proof.emailCode };
|
|
604
|
+
return this.client.post(`${nativeNetworkPath('atproto')}/claim-password`, body, { retry: false });
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* "Let Minds act as your AT Protocol account" (0.7.191): the way back when AT Protocol reads `needs_action` with reason
|
|
608
|
+
* `needs_atproto_grant` (an account made before Minds ran it natively) or `error` with `app_password_revoked`. The member
|
|
609
|
+
* enters their AT Protocol account's password once (the main one, which `claimAtprotoPassword` lets them set, or a
|
|
610
|
+
* non-privileged app password they made); Minds keeps only a new revocable app password and drops the password given.
|
|
611
|
+
* Refusals: 403 `password_failed`, 400 `privileged_app_password` (one that can read direct messages), 409
|
|
612
|
+
* `identity_mismatch`, 404 `no_identity`, 429 `rate_limited`. Needs `users:write`. Never auto-retried.
|
|
613
|
+
*/
|
|
614
|
+
grantAtproto(input) {
|
|
615
|
+
return this.client.post(`${nativeNetworkPath('atproto')}/grant`, { password: input.password }, { retry: false });
|
|
616
|
+
}
|
|
617
|
+
/** The member's one-tap actions waiting for their native identity to be set up, oldest first (0.7.191). Needs `users:read`. */
|
|
618
|
+
pendingInteractions() {
|
|
619
|
+
return this.client.get('/protocols/interactions/pending');
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* One-tap like or unlike (0.7.191). A native answer is sent (`NativeEngageSent`) or queued (`NativeEngageQueued`, 202)
|
|
623
|
+
* while the identity is set up; a linked answer is the review to submit with `submitNativeLike`, as before. Refusals:
|
|
624
|
+
* 409 `native_network_off` (with `reason`), 409 `linked_account_required`, 429 `too_many_pending`, and 503
|
|
625
|
+
* `native_network_unavailable` with reason `adapter_missing` while the network cannot send a tap as the member's identity
|
|
626
|
+
* (a linked account acts instead when there is one; nothing is set up or queued). Keep
|
|
627
|
+
* `clientMutationId` across retries: a retried tap is never sent twice, and once it expired unsent the retry answers
|
|
628
|
+
* `delivery: { state: 'failed', code: 'interaction_expired' }` (a new tap takes a new id). The same id on a different
|
|
629
|
+
* action, target or words is 409 `client_mutation_reused`. A tap whose identity went off before it was sent answers
|
|
630
|
+
* `delivery: { state: 'failed', code: 'native_network_off' }`. Never auto-retried.
|
|
631
|
+
*/
|
|
632
|
+
engageLike(input) {
|
|
633
|
+
return this.client.post('/protocols/social/likes/prepare', engageOneBody(input), { retry: false });
|
|
634
|
+
}
|
|
635
|
+
/** One-tap repost or unrepost (0.7.191); answers as `engageLike` does (linked: submit with `submitNativeRepost`). */
|
|
636
|
+
engageRepost(input) {
|
|
637
|
+
return this.client.post('/protocols/social/reposts/prepare', engageOneBody(input), { retry: false });
|
|
638
|
+
}
|
|
639
|
+
/** One-tap follow or unfollow (0.7.191); answers as `engageLike` does (linked: submit with `submitNativeFollow`). */
|
|
640
|
+
engageFollow(input) {
|
|
641
|
+
return this.client.post('/protocols/social/follows/prepare', engageOneBody(input), { retry: false });
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* A reply as the member's own identity on the parent's network, or (identity `linked`, or `accountIds`) the shared
|
|
645
|
+
* reply review as `prepareNativeReply` (0.7.191). Answers as `engageLike` does. `media` takes only the member's own
|
|
646
|
+
* Minds uploads, native or linked (400 `invalid_reply` otherwise; 422 `media_unsupported` for a GIF with other images
|
|
647
|
+
* on ActivityPub).
|
|
648
|
+
*/
|
|
649
|
+
engageReply(input) {
|
|
650
|
+
return this.client.post('/protocols/social/replies/prepare', { clientMutationId: input.clientMutationId, text: input.text,
|
|
651
|
+
parent: { protocol: input.parent.protocol, nativeId: input.parent.nativeId, ...(input.parent.readRef !== undefined ? { readRef: input.parent.readRef } : {}) },
|
|
652
|
+
...(input.media !== undefined ? { media: nativeMediaInput(input.media) } : {}),
|
|
653
|
+
...(input.identity !== undefined ? { identity: input.identity } : {}),
|
|
654
|
+
...(input.accountIds !== undefined ? { accountIds: input.accountIds } : {}) }, { retry: false });
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* A quote of a post as the member's own identity on its network, or (identity `linked`, or `accountIds`) the post
|
|
658
|
+
* review as `prepareNativePost` (0.7.191). A post with no quote is never native: the member's posts in the app reach
|
|
659
|
+
* their networks on their own (409 `native_post_from_app`). Answers as `engageLike` does.
|
|
660
|
+
*/
|
|
661
|
+
engageQuote(input) {
|
|
662
|
+
return this.client.post('/protocols/social/posts/prepare', { clientMutationId: input.clientMutationId, text: input.text,
|
|
663
|
+
quote: { protocol: input.quote.protocol, nativeId: input.quote.nativeId },
|
|
664
|
+
...(input.media !== undefined ? { media: nativeMediaInput(input.media) } : {}),
|
|
665
|
+
...(input.identity !== undefined ? { identity: input.identity } : {}),
|
|
666
|
+
...(input.accountIds !== undefined ? { accountIds: input.accountIds } : {}) }, { retry: false });
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* Takes the member's managed Nostr key home, step 1: the key as an `nsec`, after a step-up (403 `step_up_required`
|
|
670
|
+
* or `step_up_failed`, 429 `rate_limited`, 404 `no_managed_key`). Minds keeps its copy. Never retried. Needs
|
|
671
|
+
* `users:write`.
|
|
672
|
+
*/
|
|
673
|
+
exportNostrKey(stepUp) {
|
|
674
|
+
return this.client.post('/protocols/networks/nostr/export', nostrStepUpBody(stepUp), { retry: false });
|
|
675
|
+
}
|
|
676
|
+
/** Step 2a: a single-use challenge for the member's signer to sign with the same key (NIP-07, never published). */
|
|
677
|
+
nostrRebindChallenge() {
|
|
678
|
+
return this.client.post('/protocols/networks/nostr/rebind-challenge', {});
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* Step 2b: the signed challenge. A signature from the identity's own key sets the mode to `attached` (409
|
|
682
|
+
* `rebind_mismatch` for another key; 400 `invalid_proof`, `expired_proof` or `used_proof`). Minds still holds its copy.
|
|
683
|
+
*/
|
|
684
|
+
rebindNostrKey(event) {
|
|
685
|
+
return this.client.post('/protocols/networks/nostr/rebind', { event }, { retry: false });
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* Step 3: after a step-up, and only once rebound (409 `rebind_required`), Minds deletes its encrypted copy of the
|
|
689
|
+
* key. First it signs, with the copy, a takedown for everything it published with the key, kept so a post deletion,
|
|
690
|
+
* "Remove what I already shared" or the account's deletion still takes Minds' notes down (409 `too_many_published`
|
|
691
|
+
* when there is too much: the copy is kept). The NIP-05 address keeps the same key; every later Nostr action is a
|
|
692
|
+
* signing request for the member's device. Not undoable by Minds: make sure their signer holds the key first. Never
|
|
693
|
+
* retried.
|
|
694
|
+
*/
|
|
695
|
+
deleteManagedNostrCopy(stepUp) {
|
|
696
|
+
return this.client.post('/protocols/networks/nostr/delete-managed-copy', nostrStepUpBody(stepUp), { retry: false });
|
|
697
|
+
}
|
|
558
698
|
/** Current Minds-hosted ActivityPub state for this app user; never provisions. */
|
|
559
699
|
federationIdentity() {
|
|
560
700
|
return this.client.get('/protocols/social/federation/identity');
|
|
@@ -582,7 +722,7 @@ export class ProtocolsResource {
|
|
|
582
722
|
source: { protocol: input.source.protocol, nativeId: input.source.nativeId,
|
|
583
723
|
...(input.source.readRef !== undefined ? { readRef: input.source.readRef } : {}) } }
|
|
584
724
|
: { action: input.action, postId: input.postId };
|
|
585
|
-
return this.client.post(`/protocols/social/nostr/${encodeURIComponent(id)}/prepare`, body, { retry: false });
|
|
725
|
+
return this.client.post(`/protocols/social/nostr/${encodeURIComponent(id)}/prepare`, input.send === true ? { ...body, send: true } : body, { retry: false });
|
|
586
726
|
}
|
|
587
727
|
/** Pass only the public signed event. After a timeout, read the prepared postId (delete) or eventId (publish/reply/linked_post).
|
|
588
728
|
* Any user-selected retry keeps the same intent/event. */
|
|
@@ -649,9 +789,11 @@ export class ProtocolsResource {
|
|
|
649
789
|
*
|
|
650
790
|
* Failures to expect: `handle_unavailable` (409, taken or reserved; choose
|
|
651
791
|
* another), `identity_conflict` (409, the member already has one),
|
|
652
|
-
* `creation_in_progress` (409),
|
|
653
|
-
*
|
|
654
|
-
*
|
|
792
|
+
* `creation_in_progress` (409), `outbound_refused` (403, the server's one
|
|
793
|
+
* outbound guard said no before the PDS was asked anything; nothing was
|
|
794
|
+
* created), and the operator-side 503s `handle_dns_unavailable`,
|
|
795
|
+
* `handle_unresolvable`, `handle_unverified` and `hosted_server_unavailable`,
|
|
796
|
+
* which succeed unchanged once the operator is done.
|
|
655
797
|
*/
|
|
656
798
|
createHostedIdentity(input) {
|
|
657
799
|
const { protocol, handle, email } = input;
|
|
@@ -35,9 +35,35 @@ export interface SearchLadder {
|
|
|
35
35
|
pending: number;
|
|
36
36
|
unavailable: string[];
|
|
37
37
|
}
|
|
38
|
-
/**
|
|
38
|
+
/**
|
|
39
|
+
* What a search post's picture is (server 0.7.162, picture-facts-v1: Jev's background read of pictures every floor
|
|
40
|
+
* cleared, cached 30 days). Layout hints only, never a safety signal: absent until the picture is judged, when the
|
|
41
|
+
* server's switch is off, or on servers before 0.7.162, and a client keeps its own layout then.
|
|
42
|
+
*/
|
|
43
|
+
export interface SearchPicture {
|
|
44
|
+
/** `text_graphic`: a screenshot, headline card, table, chart, poster or infographic. `other`: a meme or anything else. */
|
|
45
|
+
kind: 'photo' | 'illustration' | 'text_graphic' | 'other';
|
|
46
|
+
/** Most of what it says is written text: show it whole (contain), not cropped, and do not lead with it. */
|
|
47
|
+
textHeavy: boolean;
|
|
48
|
+
/** How good a big tile it makes, 0 (unusable) to 3 (striking). */
|
|
49
|
+
tileQuality: number;
|
|
50
|
+
/** Where its main subject is, as fractions of the width (`x`) and height (`y`) from the top left: a crop's object position. */
|
|
51
|
+
focal?: {
|
|
52
|
+
x: number;
|
|
53
|
+
y: number;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Whether the picture itself shows what was searched (0.7.190, search-picture-subject-v1, cached 7 days per query and
|
|
57
|
+
* picture): on a visitor's `deep` stage only, for the page's top pictures. Absent unless the operator turned on
|
|
58
|
+
* `SEARCH_PICTURE_SUBJECT` (with picture facts), and until that query's picture is judged. A layout hint (make a big
|
|
59
|
+
* tile of it when `true`), never a reason to hide a result and never a safety signal.
|
|
60
|
+
*/
|
|
61
|
+
showsQuery?: boolean;
|
|
62
|
+
}
|
|
63
|
+
/** A post as a search lists it: `via` says how the ladder found it (absent from servers before 0.7.79); `picture`: SearchPicture. */
|
|
39
64
|
export type SearchPost = Post & {
|
|
40
65
|
via?: SearchVia;
|
|
66
|
+
picture?: SearchPicture;
|
|
41
67
|
};
|
|
42
68
|
/**
|
|
43
69
|
* A visitor's search tabs (tRPC `publicDiscovery.search`, read keyless through the app's public client). `media` (server
|
|
@@ -69,6 +95,30 @@ export interface PublicSearchPage<T = Record<string, unknown>> {
|
|
|
69
95
|
related_queries?: Array<{
|
|
70
96
|
query: string;
|
|
71
97
|
}>;
|
|
98
|
+
/**
|
|
99
|
+
* The page's clips in lead order (server 0.7.173): its posts' clips, the open networks' and the `media` tab's videos,
|
|
100
|
+
* the most about the words first (`SearchVideoLeadEntry`). The `videos` tab's `items` already come in this order.
|
|
101
|
+
* Absent with no clip, from older servers, or when the operator turned the lead order off.
|
|
102
|
+
*/
|
|
103
|
+
video_lead?: SearchVideoLeadEntry[];
|
|
104
|
+
}
|
|
105
|
+
/** Where a clip in a search's `video_lead` came from: a Minds member's post, an open-network post, PeerTube, or the web. */
|
|
106
|
+
export type SearchVideoLeadSource = 'minds' | 'open_network' | 'peertube' | 'web';
|
|
107
|
+
/**
|
|
108
|
+
* One clip of a search answer in lead order (server 0.7.173). The order: `on_subject` (`on`: about the words, `loose`: it
|
|
109
|
+
* mentions them, `off`: the popular tail), then freshness (within a day, a week, 30 days, a year, older, undated last),
|
|
110
|
+
* then a Minds member's clip before any other, then the newer. Play the first entry as the lead. `section` and `index`
|
|
111
|
+
* point at the item in the answer (`items` on a visitor's page); `ref` is the Minds post id, the open-network post's
|
|
112
|
+
* `sourceUrl` or the media item's `url`. A client merging two answers' entries sorts by the same rules.
|
|
113
|
+
*/
|
|
114
|
+
export interface SearchVideoLeadEntry {
|
|
115
|
+
section: 'top_posts' | 'latest_posts' | 'open_network' | 'media' | 'items';
|
|
116
|
+
index: number;
|
|
117
|
+
ref: string;
|
|
118
|
+
source: SearchVideoLeadSource;
|
|
119
|
+
on_subject: 'on' | 'loose' | 'off';
|
|
120
|
+
/** ISO 8601, when the clip is dated. */
|
|
121
|
+
published_at: string | null;
|
|
72
122
|
}
|
|
73
123
|
/** The kinds of a search's media items, in the order the server lists them. */
|
|
74
124
|
export declare const SEARCH_MEDIA_KINDS: readonly ["about", "video", "article", "image", "audio"];
|
|
@@ -77,8 +127,11 @@ export type SearchMediaKind = (typeof SEARCH_MEDIA_KINDS)[number];
|
|
|
77
127
|
* Where a media item was found. `eventregistry` (0.7.133): a news article from the thousands the server's news lanes
|
|
78
128
|
* read each day (Event Registry and outlet feeds), matched by its headline: show its headline, `description` (a short
|
|
79
129
|
* excerpt), `publisher` (the outlet), `published_at`, `thumbnail` and link to `url`, never more of the article.
|
|
130
|
+
* `youtube` (0.7.181): a video the YouTube Data API found for the words; it always has YouTube's player as `embed`: play
|
|
131
|
+
* it in place by YouTube's player rules (no autoplay with sound, nothing drawn over it, YouTube named as the source)
|
|
132
|
+
* rather than opening `url`.
|
|
80
133
|
*/
|
|
81
|
-
export type SearchMediaSource = 'brave' | 'wikipedia' | 'peertube' | 'openverse' | 'eventregistry';
|
|
134
|
+
export type SearchMediaSource = 'brave' | 'wikipedia' | 'peertube' | 'openverse' | 'eventregistry' | 'youtube';
|
|
82
135
|
/**
|
|
83
136
|
* How a video may play in place: YouTube's own player for `id` (use youtube-nocookie.com and follow its player rules:
|
|
84
137
|
* at least 200x200, nothing drawn over it, YouTube shown as the source), or a PeerTube instance's own embed page.
|
|
@@ -117,12 +170,20 @@ export interface SearchMediaItem {
|
|
|
117
170
|
* Visitors and members on safe defaults are never served such an item.
|
|
118
171
|
*/
|
|
119
172
|
sensitive?: true;
|
|
173
|
+
/**
|
|
174
|
+
* The same PeerTube video as a native Fediverse post (0.7.155, `SocialPost.software: 'peertube'`): show the post, which
|
|
175
|
+
* plays in place from its channel, and never open `url`. Present only on a PeerTube video the server could read off its
|
|
176
|
+
* own instance; the item's other fields stay for an app that does not read it.
|
|
177
|
+
*/
|
|
178
|
+
post?: SocialPost;
|
|
120
179
|
}
|
|
121
180
|
/**
|
|
122
181
|
* The reads behind the media section. `news_index` (0.7.133): today's articles about the words from those the server's
|
|
123
|
-
* news lanes already read; `eventregistry`: a live per-query Event Registry read, where the server has one
|
|
182
|
+
* news lanes already read; `eventregistry`: a live per-query Event Registry read, where the server has one; `youtube`
|
|
183
|
+
* (0.7.181): YouTube's videos for the words, asked only when the web's own videos about them are thin or the words name
|
|
184
|
+
* sport or music (`skipped` otherwise), within the server's daily YouTube quota (`budget` past it).
|
|
124
185
|
*/
|
|
125
|
-
export type SearchMediaProvider = 'brave_web' | 'brave_images' | 'wikipedia' | 'peertube' | 'openverse_images' | 'openverse_audio' | 'news_index' | 'eventregistry';
|
|
186
|
+
export type SearchMediaProvider = 'brave_web' | 'brave_images' | 'wikipedia' | 'peertube' | 'openverse_images' | 'openverse_audio' | 'news_index' | 'eventregistry' | 'youtube';
|
|
126
187
|
/**
|
|
127
188
|
* What each provider did for this answer; `timeout`: it is still reading, ask again shortly for more; `budget`: a
|
|
128
189
|
* spending limit held it back (the day's Brave calls, or this caller's own calls a day or fresh answers an hour, past
|
|
@@ -308,6 +369,12 @@ export interface SearchAnswer {
|
|
|
308
369
|
few_results: boolean;
|
|
309
370
|
/** The search ladder's account of the answer (server 0.7.79): present when the server ran the ladder. */
|
|
310
371
|
ladder?: SearchLadder;
|
|
372
|
+
/**
|
|
373
|
+
* Every clip the answer holds (Top's and Latest's Minds clips, the open networks', the media section's videos) in the
|
|
374
|
+
* order to lead and play them (server 0.7.173, `SearchVideoLeadEntry`): the most about the words first, then the
|
|
375
|
+
* freshest, a Minds member's clip winning a tie. Absent with no clip, from older servers, or when turned off.
|
|
376
|
+
*/
|
|
377
|
+
video_lead?: SearchVideoLeadEntry[];
|
|
311
378
|
}
|
|
312
379
|
export interface SearchResponse {
|
|
313
380
|
data: SearchAnswer;
|
|
@@ -59,8 +59,9 @@ export declare class SettingsResource {
|
|
|
59
59
|
* Email a six-digit confirmation code to the account's current address.
|
|
60
60
|
* Only for accounts with no password (`profiles.me().has_password === false`);
|
|
61
61
|
* a password account answers `password_required`. Send the code as
|
|
62
|
-
* `email_code` to `requestEmailChange`,
|
|
63
|
-
* `auth.*TwoFactor` methods
|
|
62
|
+
* `email_code` to `requestEmailChange`, as `emailCode` to the
|
|
63
|
+
* `auth.*TwoFactor` methods, or (purpose `nostr_key`) as `emailCode` to
|
|
64
|
+
* `protocols.exportNostrKey` and `protocols.deleteManagedNostrCopy`.
|
|
64
65
|
*/
|
|
65
66
|
requestConfirmationCode(purpose: ConfirmationCodePurpose): Promise<SingleResponse<ConfirmationCodeSent>>;
|
|
66
67
|
/**
|
|
@@ -99,8 +99,9 @@ export class SettingsResource {
|
|
|
99
99
|
* Email a six-digit confirmation code to the account's current address.
|
|
100
100
|
* Only for accounts with no password (`profiles.me().has_password === false`);
|
|
101
101
|
* a password account answers `password_required`. Send the code as
|
|
102
|
-
* `email_code` to `requestEmailChange`,
|
|
103
|
-
* `auth.*TwoFactor` methods
|
|
102
|
+
* `email_code` to `requestEmailChange`, as `emailCode` to the
|
|
103
|
+
* `auth.*TwoFactor` methods, or (purpose `nostr_key`) as `emailCode` to
|
|
104
|
+
* `protocols.exportNostrKey` and `protocols.deleteManagedNostrCopy`.
|
|
104
105
|
*/
|
|
105
106
|
requestConfirmationCode(purpose) {
|
|
106
107
|
return this.client.post('/settings/confirmation-code', { purpose });
|
package/dist/types.d.ts
CHANGED
|
@@ -184,6 +184,19 @@ export interface UserSummary {
|
|
|
184
184
|
* servers older than this field and from reads that do not label accounts.
|
|
185
185
|
*/
|
|
186
186
|
is_nsfw?: boolean;
|
|
187
|
+
/**
|
|
188
|
+
* An account the operator vouches for: draw the verified badge. Carried by a
|
|
189
|
+
* post's author. An app that draws one mark next to a name draws verified
|
|
190
|
+
* before `founder` before the plan mark. Absent from servers older than this
|
|
191
|
+
* field and from reads that do not mark accounts.
|
|
192
|
+
*/
|
|
193
|
+
verified?: boolean;
|
|
194
|
+
/** The account's Plus plan is live. Absent from reads that do not mark accounts. */
|
|
195
|
+
plus?: boolean;
|
|
196
|
+
/** The account's Pro plan is live. Absent from reads that do not mark accounts. */
|
|
197
|
+
pro?: boolean;
|
|
198
|
+
/** A founding member. Absent from reads that do not mark accounts. */
|
|
199
|
+
founder?: boolean;
|
|
187
200
|
}
|
|
188
201
|
export interface User extends UserSummary {
|
|
189
202
|
bio: string | null;
|
|
@@ -1772,7 +1785,7 @@ export interface RequestEmailChangeInput {
|
|
|
1772
1785
|
email_code?: string;
|
|
1773
1786
|
}
|
|
1774
1787
|
/** What a confirmation code is for. A code for one purpose never confirms another. */
|
|
1775
|
-
export type ConfirmationCodePurpose = 'email_change' | 'two_factor';
|
|
1788
|
+
export type ConfirmationCodePurpose = 'email_change' | 'two_factor' | 'nostr_key';
|
|
1776
1789
|
export interface ConfirmationCodeSent {
|
|
1777
1790
|
success: true;
|
|
1778
1791
|
purpose: ConfirmationCodePurpose;
|
package/llm.md
CHANGED
|
@@ -100,7 +100,7 @@ const { data: logs } = await r.projects.deploymentLogs(project.id, deployment.id
|
|
|
100
100
|
|
|
101
101
|
Methods: `list`, `get`, `create`, `delete`, `deploy`, `executeCode`, `sandbox`, `createSandbox`, `stopSandbox`, `deployment`, `deploymentLogs`, `deployments`
|
|
102
102
|
|
|
103
|
-
Operator API/frontend coordination: `r.deployments.reserveRelease(input)`, `getRelease(releaseId, projectId)`, `advanceRelease(releaseId, input)`, `startRelease(releaseId, input)`, `finishRelease(releaseId, input)`, `recoverRelease(originalReleaseId, input)`. Example read: `const { data: release } = await r.deployments.getRelease(releaseId, projectId)`. Use a project-bound `projects:read`/`projects:write` operator key; follow [shared release phases, live proof and recovery](../../docs/deploy/shared-releases.md). Keep capabilities out of logs; uncertain operations retain ownership.
|
|
103
|
+
Operator API/frontend coordination: `r.deployments.reserveRelease(input)`, `getRelease(releaseId, projectId)`, `advanceRelease(releaseId, input)`, `startRelease(releaseId, input)`, `finishRelease(releaseId, input)`, `recoverRelease(originalReleaseId, input)`. Example read: `const { data: release } = await r.deployments.getRelease(releaseId, projectId)`. Use a project-bound `projects:read`/`projects:write` operator key; follow [shared release phases, live proof and recovery](../../docs/deploy/shared-releases.md). Initial reservation also accepts optional `coordinator_identity_token` after the compatible API is verified live; invalid supplied proof never falls back to tokenless. Keep capabilities and tokens out of logs; uncertain operations retain ownership.
|
|
104
104
|
|
|
105
105
|
### r.storage — Bucket-based file storage
|
|
106
106
|
|
|
@@ -266,6 +266,15 @@ const first = events[0]
|
|
|
266
266
|
const { data: step } = await r.evidence.recordDetail({ type: 'swarm', id: swarmId }, first.id, first.kind)
|
|
267
267
|
// step: model_call → { systemPrompt, promptMessages, responseText, reasoningText, toolCalls }
|
|
268
268
|
// tool_call → { toolName, args, result, success }
|
|
269
|
+
|
|
270
|
+
// Save a set once, re-run it after every change so runs compare like with like
|
|
271
|
+
const { data: saved } = await r.evidence.createEvalSet({
|
|
272
|
+
name: 'Escalation policy',
|
|
273
|
+
cases: [{ input: 'New account, $9.6k order', expected: { answer: 'ESCALATE' }, trap: true }],
|
|
274
|
+
})
|
|
275
|
+
const { data: rerun } = await r.evidence.simulate({ target: { type: 'swarm', id: swarmId }, eval_set_id: saved.eval_set_id })
|
|
276
|
+
const { data: sets } = await r.evidence.listEvalSets() // the key's org, or your own sets
|
|
277
|
+
const { data: file } = await r.evidence.exportRun(rerun.run_id, 'csv') // { filename, mimeType, content }
|
|
269
278
|
```
|
|
270
279
|
|
|
271
280
|
### Task checks — how the org knows a task's work is done
|
|
@@ -734,11 +743,27 @@ const top = await r.feeds.top({ ref: 'builtin:all', limit: 10, session }) // m
|
|
|
734
743
|
// top.lead: the story to open with; every target is in the app (post id, /social/post/<protocol>/<id>, story id), never a URL out
|
|
735
744
|
const { data: next } = await r.feeds.stories({ after: storyId }) // the story tail, same ranker as the lead
|
|
736
745
|
const page = await r.feeds.publicTop({ projectId, ref: 'all' }) // a visitor's top: safe items only, keyless, edge-cacheable
|
|
746
|
+
const live = await r.feeds.firehose({ session, limit: 20, network: 'all' }) // 0.7.195: the member's live stream, never repeated in a session
|
|
747
|
+
// live.items: FeedTopItem + { network, state: 'breaking' | 'viral' | null, publishedAt, chosenAt, card? }; live.retract: refs to remove
|
|
748
|
+
// reveal one at a time; read again no sooner than live.meta.refillAfterMs (404 while off; 429 and 503 carry Retry-After)
|
|
737
749
|
const feed = r.discovery.createFeed({ networks, top: { ref: 'builtin:all', session }, seen: seenRefs })
|
|
738
750
|
```
|
|
739
751
|
|
|
740
752
|
Each answers 404 while the app's operator has it off; `feeds.top` is private to the member and 503 `feed_top_unavailable` carries Retry-After. With a visible view of a top item, send `block: 'top'` and its position. In a feed session the top's read starts with page 0's reads and the top holds the page at most 1.5 s from its start, the by-id reads of its posts included; off, failed or late, the session is unchanged, and the radar's story reads its posts as soon as the radar answers either way. Page 0 then opens with the top's lead story and items (each item carries `top: { ref, position, block }`), its lanes replace the networks' first reads, and no hot page repeats a top item. `seen` (Minds ids and `<protocol>/<nativeId>` refs) leaves seen posts out of the hot pages, walking on past a page left under five posts (up to three more). `seenClusters` (`Map<id, { n, clicked }>`) and a visitor's `bucket` rotate the story when the top names none (`pickRadarCluster`).
|
|
741
753
|
|
|
754
|
+
### r.alive — The alive tick (member, 0.7.193)
|
|
755
|
+
|
|
756
|
+
```typescript
|
|
757
|
+
// One request per tick carries every open screen's interests; each part is answered only when asked for.
|
|
758
|
+
const tick = await r.alive.tick({ followingSince: newestRow.created_at, posts: idsOnScreen, thread: { id: openPostId, since: newestReplyAt } })
|
|
759
|
+
if (tick.following?.fresh) await feed.loadNew() // the rows wait behind the new-posts pill; nothing moves
|
|
760
|
+
for (const [id, n] of Object.entries(tick.counts ?? {})) updateCounts(id, n) // score, upvote_count, reply_count, reposts_count
|
|
761
|
+
if (tick.thread?.newReplies) showNewReplies(tick.thread.newReplies) // "3 new replies", revealed on a tap
|
|
762
|
+
setTimeout(next, tick.nextAfterMs) // the server's pace (30 s by default, 120 s under load)
|
|
763
|
+
```
|
|
764
|
+
|
|
765
|
+
The member's own key of the app, `users:read`. Off until the operator sets `ALIVE_TICK_ENABLED`: the tick throws `NotFoundError` (code `alive_disabled`) and the app stops ticking. `following.fresh` is true only for a top-level Minds post after `followingSince` by someone the member follows that they may be served (blocks either way, mutes, tier 1, moderation, safety setting); `counts` holds only posts the member may be served (a repost, quote or reply in view is left out: keep the number you have); `thread.newReplies` never counts the member's own replies, and is at most 100. At most 30 `posts` (400 `too_many_posts`), 4 ticks a minute per member (`RateLimitError`). Asked once (`retry: false`): the next tick is the retry. Tick only while the page is visible.
|
|
766
|
+
|
|
742
767
|
### r.search
|
|
743
768
|
|
|
744
769
|
```typescript
|
|
@@ -750,9 +775,11 @@ for (const section of answer.order) render(section, answer.sections[section])
|
|
|
750
775
|
const { data: web } = await r.search.query({ q: 'hockey', sections: ['media'], own_images: shownPictures })
|
|
751
776
|
```
|
|
752
777
|
|
|
753
|
-
`media` (0.7.123): videos (YouTube ids as `embed`, PeerTube embed pages), news articles (0.7.133: also today's articles about the words from the server's news lanes, `source: 'eventregistry'`, `providers.news_index`; show the headline, `description` excerpt, `publisher`, date, `thumbnail` and link only), pictures, sound and one `about` card (Wikipedia), from Brave Search, Wikipedia, PeerTube and Openverse with strict safe search. It answers only when `sections` names it (the default sections leave it out: each answer may spend a paid web search). Every picture passes the same vision check a post's picture does: a visitor sees only pictures judged safe, and a member who turned Show NSFW on gets a suggestive one marked `sensitive` (blur it). Every `thumbnail` is on the API's signed media proxy; play a YouTube embed through youtube-nocookie.com by its player's rules and show each `attribution`. `own_images` (how many pictures the page already shows) lets the server ask for the web's pictures only when fewer than six. Only the networks the server offers it to get it (others: `omitted` `unavailable`); `providers` says what each source did (`timeout`: ask again shortly). Visitors read it as `publicDiscovery.search` `tab: 'media'` (`PublicSearchPage.media`).
|
|
778
|
+
`media` (0.7.123): videos (YouTube ids as `embed`, PeerTube embed pages), news articles (0.7.133: also today's articles about the words from the server's news lanes, `source: 'eventregistry'`, `providers.news_index`; show the headline, `description` excerpt, `publisher`, date, `thumbnail` and link only), pictures, sound and one `about` card (Wikipedia), from Brave Search, Wikipedia, PeerTube and Openverse with strict safe search. It answers only when `sections` names it (the default sections leave it out: each answer may spend a paid web search). Every picture passes the same vision check a post's picture does: a visitor sees only pictures judged safe, and a member who turned Show NSFW on gets a suggestive one marked `sensitive` (blur it). Every `thumbnail` is on the API's signed media proxy; play a YouTube embed through youtube-nocookie.com by its player's rules and show each `attribution`. `own_images` (how many pictures the page already shows) lets the server ask for the web's pictures only when fewer than six. Only the networks the server offers it to get it (others: `omitted` `unavailable`); `providers` says what each source did (`timeout`: ask again shortly). Visitors read it as `publicDiscovery.search` `tab: 'media'` (`PublicSearchPage.media`). A PeerTube item the server could read off its own instance carries the same video as `post` (0.7.155), a native Fediverse `SocialPost` (`software: 'peertube'`) that plays in place from its channel: show it instead of opening `url`. A `source: 'youtube'` video (0.7.181) is one the YouTube Data API found for the words, asked only when the web's own videos about them are thin or the words name sport or music, within the server's daily YouTube quota (`providers.youtube`: `skipped`, `budget` past the quota, `unconfigured` without a key): it always carries YouTube's player as `embed`, so play it in place (no autoplay with sound, nothing drawn over the player, YouTube named as the source) rather than opening `url`.
|
|
779
|
+
|
|
780
|
+
`video_lead` (0.7.173): every clip the answer holds (Top's and Latest's Minds clips, the open networks', the media section's videos) in the order to lead and play it, each `{ section, index, ref, source, on_subject, published_at }`: the most about the words first (`on_subject` `on`, `loose`, `off`), then the freshest, a Minds member's clip winning a tie; the `off` tail (the popular clips, about none of the words) keeps the answer's own order. Lead with `video_lead[0]`; a visitor's page (`publicDiscovery.search`) carries it too, and its `videos` tab's `items` already come in that order. Absent with no clip, from older servers, or when the operator turned it off.
|
|
754
781
|
|
|
755
|
-
`trending` joins when the answer is thin or old, so a search never dead-ends. A thin answer steps down the search ladder (0.7.79): the word a typo meant, the known words with the rest dropped ("wiggly memes" searches "memes"), what is popular in the category the words are about, then on the app. Every item says how in `via` (`SearchVia`) and `answer.ladder` (`SearchLadder`) says what happened: label it honestly ("No exact matches for X", "Did you mean Y?", "Popular in Comedy right now"); `ladder.unavailable` names sections the key cannot read. Visitors read the same ladder keyless through tRPC `publicDiscovery.search` (`PublicSearchPage`). `top_posts` is floored as `r.posts.search`'s relevance page is; `people` rows carry `followers_count`, `posts_count`, `empty` and `verified` (see `r.profiles.search`); `groups` rows carry `description` (one line) and `last_active_at`, to tell groups of one name apart. Page further with `r.posts.search`. Needs `posts:read`; `people` needs `users:read`, `groups` `communities:read`; `open_network` and `stories` need `users:read` plus the caller's own app key on its network, `trending` that app key. What the key cannot have is in `omitted` (`scope` / `unavailable`).
|
|
782
|
+
`trending` joins when the answer is thin or old, so a search never dead-ends. A thin answer steps down the search ladder (0.7.79): the word a typo meant, the known words with the rest dropped ("wiggly memes" searches "memes"), what is popular in the category the words are about, then on the app. Every item says how in `via` (`SearchVia`) and `answer.ladder` (`SearchLadder`) says what happened: label it honestly ("No exact matches for X", "Did you mean Y?", "Popular in Comedy right now"); `ladder.unavailable` names sections the key cannot read. Visitors read the same ladder keyless through tRPC `publicDiscovery.search` (`PublicSearchPage`). A visitor's posts tabs may carry `picture` (`SearchPicture`, 0.7.162): `kind`, `textHeavy`, `tileQuality` (0-3) and an optional `focal` point, layout hints from a background picture read, absent until judged: show a `textHeavy` picture whole, crop others at `focal`, never treat it as a safety signal. On the `deep` stage `picture.showsQuery` (0.7.190) says whether the picture itself shows the query (make a big tile where it is `true`); it is absent unless the operator turned on `SEARCH_PICTURE_SUBJECT`, and until judged. `top_posts` is floored as `r.posts.search`'s relevance page is; `people` rows carry `followers_count`, `posts_count`, `empty` and `verified` (see `r.profiles.search`); `groups` rows carry `description` (one line) and `last_active_at`, to tell groups of one name apart. Page further with `r.posts.search`. Needs `posts:read`; `people` needs `users:read`, `groups` `communities:read`; `open_network` and `stories` need `users:read` plus the caller's own app key on its network, `trending` that app key. What the key cannot have is in `omitted` (`scope` / `unavailable`).
|
|
756
783
|
|
|
757
784
|
### r.curator — World Today and its story pages
|
|
758
785
|
|
|
@@ -875,7 +902,7 @@ const frame = mountSealedFrame({ container, frameOrigin: 'https://sealed.minds.c
|
|
|
875
902
|
```
|
|
876
903
|
- `r.billing` — Usage tracking, checkout sessions, portal sessions, metered billing
|
|
877
904
|
- `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)
|
|
878
|
-
- `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`
|
|
905
|
+
- `r.boosts` — Promoted posts (micro-USD): `create`, `mine`, `balance`, `pause`, `resume`, `cancel`, `recordEvent`, `stats(campaignId)` (views, clicks, click rate, cost per 1,000, daily series); a feed page's boosts to splice in with `slots({ surface: 'all', limit: page.length, exclude: ids })` (visitors: `publicSlots({ projectId, ... })`), then `spliceBoostSlots(page, res.data)`; buy credit with `topup({ amount_cents, return_url })` then `confirmTopup(sessionId)` (credits once); pay with tokens via `topupWithTokens({ tokens, request_id })` when `balance().meta.token_topup.available`; admin `listPendingReview`, `review`, `grant`
|
|
879
906
|
- `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.
|
|
880
907
|
- `r.uploads` — Legacy media upload URLs (prefer `r.storage` for new projects)
|
|
881
908
|
- `r.github` — GitHub integration (getRepository, getTree, getFileContent, getCommits, createFromTemplate)
|
|
@@ -909,14 +936,19 @@ await r.appSubscriptions.changeTier({ tier: 'pro', proration_date: quote.prorati
|
|
|
909
936
|
- Delegated social accounts: `r.protocols.nativeSocialAccounts()`, `startNativeSocialConnection({ protocol, identifier })`, `finishNativeSocialConnection({ protocol, state, code?, iss?, error? })`, `disconnectNativeSocialAccount(id)`. Example: `const { data } = await r.protocols.startNativeSocialConnection({ protocol: "bluesky", identifier: "alice.bsky.social" })`; open `data.authorizationUrl`, then complete with the same signed-in app user. Never log callback codes, send passwords, or auto-retry OAuth callbacks. Shared replies (off until schema/API deployment and live verification): `prepareNativeReply({ clientMutationId, text, parent: { protocol, nativeId, readRef? }, accountIds, media?, linkedPostAccountIds?, recoverFrom?: { batchId, deliveryIds } })`, `submitNativeReply(id, { approve: true, deliveryIds })`, `nativeReplyStatus(id)`, `nativeReplyHistory({ limit?, cursor? })`, `prepareNativeReplyRemoval(id, { deliveryIds })`, `removeNativeReply(id, { approve: true, removals })`. Review account, destination, `kind`, source, exact `wireText` and every reviewed `media` item with its alt text; connection is not publication approval. `media` takes the same `NativeMediaInput[]` as `prepareNativePost` and needs the account's `publish_media` capability. `linkedPostAccountIds` explicitly selects different-network standalone posts with a source link, never matching threaded replies or a same-network denial fallback. Preserve mutation IDs, show per-destination results and reconcile uncertain attempts without reposting. To review selected unsent or definitively rejected deliveries again, use recoverFrom with a new mutation ID, the same source/accounts and freshly reviewed text; preserve that ID on uncertain preparation. Only saved recoverable destinations qualify, current gates still apply, and replacedBy disables old sends atomically. Never restart unknown, accepted or published deliveries. The new preview still needs explicit approval. Saved delegated drafts/results: `const { data: history } = await r.protocols.nativeReplyHistory({ limit: 20 })`; use its opaque cursor for the next page and `nativeReplyStatus(item.id)` for details. This is a private read, not a write or provider refresh, and includes Nostr results marked type=nostr. Use nativeNostrReplyResult(item.id) to check that recorded event on its relay without signing or resending. Discard page state on owner/app changes. See README for examples and the callback, revocation and removal contracts.
|
|
910
937
|
- `r.protocols` — Private app safety: `sourceControls` returns current `enabled` plus paged saved choices; `setSourceControl`, `onSourceControlChange` and `reportSource` share app scope. Example: `const choices = new Map(); const stop = r.protocols.onSourceControlChange(change => choices.set(JSON.stringify([change.source, change.control]), change.enabled));` then `await r.protocols.setSourceControl({ source: { protocol: "bluesky", kind: "actor", nativeId: "did:plc:aaaaaaaaaaaaaaaaaaaaaaaa" }, control: "mute", enabled: true, scope: "minds" });` and call `stop(); choices.clear()` on account change/unmount. Notifications follow an exact server confirmation, never an uncertain response. These preferences and reports apply in Minds, not on the remote network. Open network ids (0.7.87, readers first): `import { SOCIAL_PROTOCOL_IDS, isSocialProtocolId } from '@recursiv/sdk'` lists every network a post, account or `SocialSourceRef` may name: bluesky, activitypub, nostr and farcaster (a Farcaster account by its FID, "3"; a cast by `<fid>:0x<40 lowercase hex>`). Listing an id serves nothing: read only what `publicNetworks()` lists, and skip an id this SDK does not know. Writes stay narrower: a native reply, follow, like or repost takes only `NATIVE_WRITE_PROTOCOLS` (bluesky, activitypub, nostr; `isNativeWriteProtocol(post.protocol)` before offering one), and a feed top's `networks` always names those three while a Farcaster lane may be absent (`networks.farcaster ?? []`). Keyless post links: `const visitor = new Recursiv({ anonymous: true, inheritEnvironmentApiKey: false }); const { data: thread } = await visitor.protocols.getSharedConversation(projectId, { protocol: 'nostr', post: publicEventId })`; `sharedNetworks(projectId)` lists public conversation capabilities. Only configured apps; app moderation and rate limits apply, no member state or write authority. Member public reading: `publicNetworks`, `searchPublicProfiles`, `getPublicProfile`, `getPublicAuthorFeed`, `getFollowingFeed`, `getPublicConversation`, `getPublicTrends`, `getPublicTrendFeed`; a Fediverse community a groups read lists under `open_network` (0.7.109): `const { data: page } = await r.protocols.getOpenGroupThreads({ group: entry.actor, limit: 20 })` reads its newest threads read-only (`page.group`, `page.items`); `limit` is 1 to 25, `compass: 'hide'` leaves out the threads in politics, and the next page passes `cursor: page.cursor` while it is set (404 while the app does not offer groups from other networks); private app subscriptions: `listSubscriptions`, `subscribeToProfile`, `unsubscribeFromProfile`, `onSubscriptionChange`. `getFollowingFeed` sources: a slow account is `late` (status `late`, or `ok` with `late: true` while its held first page is shown; older APIs say `unavailable` with `late: true`), never a failure: read the next page for its posts (`lateNetworks` names its network). Example after selecting a returned source post: `const { data: thread } = await r.protocols.getPublicConversation({ protocol: post.protocol, post: post.nativeId, readRef: post.readRef })`. Render `focus`, oldest-first `ancestors` and direct `replies`; `public_provider_window` never promises every reply, and `limited` marks known omissions. The author's own thread (0.7.81): `thread` holds the focus author's continuation below the post, oldest first (a "1/" post's 2/, 3/, 4/ ..., at most 20 parts, each held to the same moderation and member controls as a reply); its first part is not repeated in `replies`, so render `focus`, `thread`, then `replies` (`thread ?? []`: older APIs omit it, and a `replies: 'none'` read answers none). A card (a post shared into a chat, a link preview) asks for the post alone, 0.7.78: `getPublicConversation({ protocol, post, replies: 'none' })` (also `getSharedConversation` and `getConnectedSocialConversation`) answers `focus` with no `ancestors` or `replies` and `repliesStatus: 'not_requested'`; a Nostr note answers from one relay read instead of a whole thread's. An older API answers 400 `invalid_input`: read without it. Requires both `users:read` and `posts:read` on the current human app key. Protocol discovery/settings: `const { data: adapters } = await r.protocols.list()` returns bounded `{ id, name, icon, color, maturity, capabilities, limitations }` metadata; `supported` means implemented, not configured, healthy, authorized, or enabled. Methods: `list`, `status`, `listCandidates`, `getCandidate`, `search`, `getSettings`, `refresh`, `updateSettings`.
|
|
911
938
|
- Native Nostr: `const { data: preview } = await r.protocols.prepareNativeNostr(identityId, { action: "publish", content: draft })`; review and sign the exact public event, then `submitNativeNostr(identityId, { intent: preview.intent, event, approve: true })`. Preparation/submission never auto-retry. After uncertainty, use `getNativeNostrPost(identityId, preview.postId ?? preview.eventId)` before a user-selected retry of the identical intent/event: deletion checks the original note's `postId`, publication checks `eventId`. Check `nativeNostrAccount()` enablement first; keys stay with the signer. For replies, after `nativeNostrAccount()` advertises `reply_text_note`, use `prepareNativeNostr(identityId, { action: "reply", content: draft, parentId: post.nativeId })`. Review returned parent/root/participants/relay and exact event, sign with the connected client signer, then `submitNativeNostr(identityId, { intent, event, approve: true })`. Requires users:read, posts:read and posts:write. Kind1 public notes only; reply enablement and shared UI/live proof remain pending. Do not inherit arbitrary notification tags or relay hints. Signing mutations never auto-retry; check `getNativeNostrPost(identityId, eventId)` after uncertainty before a user-selected identical-event retry.
|
|
912
|
-
-
|
|
939
|
+
- Nostr signing (0.7.192): `nativeNostrAccount()` answers `signing: 'client'` (the member's own key: prepare, sign the exact event, submit) or `signing: 'server'` with `mode: 'managed'` (a key Minds holds: `prepareNativeNostr` is a side-effect-free preview; after the member approves, `prepareNativeNostr(id, { ...input, send: true })` signs, sends and returns `receipt`; a like/repost/follow review submits `{ approve: true }` with no `event`; `engage*` with `identity: 'native'` acts as it). A managed key's `enabled` follows the member's Nostr presence, not the app's Nostr write switches; for either key, like/repost/follow (and their undos) are listed only while the app's review route for them is on, so offer exactly the listed capabilities.
|
|
940
|
+
- Native networks (0.7.89): every eligible member is on ActivityPub, AT Protocol and Nostr by rule. `const { data } = await r.protocols.networks()` lists `{ network, state, reason, mode, handle, engageDefault }` (read-only; `ready` = set up on their first post or interaction, show "Will be set up when you first post or interact"). `activateNetwork(network)` = "Set up now" (`pending` + `retryAt` over budget; 409 `network_off` after the member's off switch). `setNetwork(network, { enabled: false, removePast: true })` turns it off and removes what was shared; `{ enabled: true }` turns it back on (403 `network_locked`, 409 `not_rolled_out`/`age_band_required`). Needs users:read (+ users:write to change). A linked outside account is a Settings choice, never a step in a main flow. Taking the managed Nostr key home: `exportNostrKey({ password } | { emailCode })` (code from `settings.requestConfirmationCode('nostr_key')`) returns the `nsec` (show once, never log); `nostrRebindChallenge()` then `rebindNostrKey(signedEvent)` proves the device holds the same key (mode `attached`); `deleteManagedNostrCopy(stepUp)` deletes Minds' copy (409 `rebind_required` before the rebind; 409 `too_many_published` keeps it), after which Nostr actions are signed on the device and Minds' own notes still come down with takedowns it signed first. Never auto-retried.
|
|
913
941
|
- Famous accounts (0.7.151): `const { data } = await r.protocols.getPublicPopularProfiles({ protocol: 'all', limit: 24 })` reads the reviewed famous accounts of AT Protocol, the Fediverse and Nostr in one ranking (most famous and recently active first: followers plus readers of the person's Wikipedia article, categories interleaved, politics at most one in four and rotated across sides), then the providers' own suggestions. Page with `cursor` for infinite scroll (it holds its list across an hourly refresh for a few hours; on 400 `invalid_input` start again); `null` is the end. `category` (one of `data.categories`) narrows the list; `compass: 'hide'` (the member's Hide politics) leaves political accounts out of any list. Items carry `category` on `protocol: 'all'`. A list the catalog leads says so: `provenance.ranking` is `famous_accounts` (provider named in plain words, `fame and recent activity`, with no company name, so an app can show it as is), and on a network's own list `provenance.sources` keeps the provider's own label and read time for the accounts after the catalog's. A `protocol: 'all'` cursor is also refused after a deploy changed the catalog. Needs users:read and posts:read; 503 until the app's index has built the list.
|
|
914
942
|
- Hosted Bluesky account on the app's own PDS: `const { data } = await r.protocols.createHostedIdentity({ protocol: 'atproto', handle: 'alice', email })`. Only the label is sent; `data.handle` carries the domain and `data.password` is returned this once, never stored. Never auto-retried. Either the account is finished or it is removed again. 409 `handle_unavailable` = choose another label; 503 `handle_dns_unavailable`/`handle_unresolvable`/`handle_unverified` = operator setup, retry later unchanged.
|
|
943
|
+
- One-tap engage (0.7.191): `engageLike|engageRepost|engageFollow({ protocol, nativeId, clientMutationId, action, identity?, accountId? })`, `engageReply({ clientMutationId, text, parent, media?, identity?, accountIds? })`, `engageQuote({ clientMutationId, text, quote, ... })`. Native: `{ identity: 'native', queued: false, delivery, actedAs }` or 202 `{ queued: true, interactionId, expiresAt, retryAt, actedAs }` (sent once the identity is set up; `pendingInteractions()` lists them). Linked: the review to submit as before, plus `identity: 'linked'`, `actedAs` and once `notice: 'engage_default_linked'`. `setEngageDefault(network, 'native' | 'linked')`. Refusals: 409 `native_network_off` (with `reason`), `linked_account_required`, `native_post_from_app` (a plain post is never native), 429 `too_many_pending`, 503 `native_network_unavailable` (reason `adapter_missing`: that network cannot send a tap natively yet; a linked account acts instead when there is one). Keep `clientMutationId` across retries; never auto-retried.
|
|
944
|
+
- AT Protocol password claim (0.7.191): the member's AT Protocol account is created with their verified email and Minds keeps only a revocable app password. `claimAtprotoPassword({ password } | { emailCode })` (emailCode from `POST /settings/confirmation-code` purpose `atproto_password`) has the PDS email a reset code; answers `{ handle, sentTo }` (masked). 403 `step_up_required`/`step_up_failed`, 404 `no_identity`, 409 `needs_verified_email`/`email_changed`, 429 `rate_limited`. Never auto-retried.
|
|
945
|
+
- AT Protocol grant (0.7.191): presence `needs_action` reason `needs_atproto_grant` (an account made before Minds ran it natively) or `error` reason `app_password_revoked` -> `grantAtproto({ password })` with the AT account's password (main, or a non-privileged app password); Minds keeps only a new revocable app password. Answers `{ handle, presence }`. 403 `password_failed`, 400 `privileged_app_password`, 409 `identity_mismatch`, 404 `no_identity`, 429 `rate_limited`. Never auto-retried.
|
|
915
946
|
- Anonymous discovery: `const visitor = new Recursiv({ anonymous: true, inheritEnvironmentApiKey: false }); const { data } = await visitor.protocols.getSharedTrends(projectId, { protocol: 'nostr', limit: 20, discovery: true })`. Use `sharedNetworks(projectId)` capabilities; open a returned topic with `getSharedTrendFeed(projectId, { protocol: 'bluesky', topic: trend.nativeId, cursor })`. Configured apps only; no account authority or writes. App moderation and per-IP/app rate limits apply. Categories and Jev layouts reuse cached judgments without initiating paid inference; handle partial categories and finite provider windows. Category depth: `r.posts.list({ category: 'science', sort: 'hot', discovery: true })` pages the hot window by offset and then by keyset (`meta.next` is `{ offset }`, `{ cursor }` or `null`; pass a cursor back as `cursor`); when a stage ends, `meta.fallover` lists the next feeds in order (`category_older`, `category_network`, `category` with `via`, `for_you`, `all`; `feed` with `ref` is declared in 0.7.60 but not sent yet, so skip kinds you cannot open). One call for a category's modules: `const { data } = await r.discovery.category({ category: 'science' })` returns `stories`, `network`, `clips`, `people`, `groups`, each `{ items, ms, reason? }`. Watch's Minds videos: `const { data, meta } = await r.discovery.videos({ limit: 12 })` returns public video posts in hot order from a list shared per app (rebuilt about once a minute), each re-checked per call for the viewer; page on `meta.next_cursor`. With no category about one row in four is an open-network clip (0.7.78): `isWatchNetworkClip(row)` tells `{ kind: 'social', id, post: SocialPost }` from a `Post`; open and react to it as the network's post (its `id` is never a Minds id).
|
|
916
947
|
- Progressive Discover feed (0.7.71): `const feed = r.discovery.createProgressiveFeed({ networks, category, recommend, top: { ref: 'builtin:all' }, seen, seenClusters }); const round = await feed.next(part => draw(part))` opens one `createFeed` session per source (Minds with the radar and suggestions, one per network with `public_trends`) and hands each source's page on as a `DiscoveryPart` (`{ source, round, modules, composed?, mixed? }`) the moment it lands, instead of a page that waits for its slowest network (up to 15 s); a round waits `DISCOVERY_STRAGGLER_MS` past its first answer and rejects only when every source failed. The first load is held until Minds and one other network answered (or `FIRST_SCREEN_MS`, 2.5 s, after the first answer; `firstScreenMs: VISITOR_FIRST_SCREEN_MS` for a visitor) and arrives as one `composed: true` part from `composeFirstScreen`: the top's first post when it is a playable video, else a picture lead from the best 8 by engagement (never a story, rail, video strip, column, `isSensitivePost` post or `linksAdultSite` porn link; 0.7.80: never a post with no picture or clip and no engagement while the screen holds one with either, which then leads: media and engagement, else media, else engagement, as `mergeDiscoveryArrival` leads a feed's top and the feed after a line), then every network's posts by engagement (`DiscoveryEngagementMerge`) with row 2 from another network than the lead's when one is left (`FIRST_SCREEN_NETWORK_ROWS`), a story as row 3 and a rail as row 5, nothing dropped. Merge later `mixed` parts with `mergeDiscoveryArrival(rows, arriving, reachedIds)`. `top`/`seen`/`seenClusters`/`bucket` reach every session; the top is read once per open. Also `retryFailed()`, `onStatus(listener)`, `setNetworks(catalog)`; settings `perSource`, `firstScreen`, `onLanded`. For you: `createProgressiveFeed({ createFeed: o => r.discovery.createPersonalFeed(o) }, options, { perSource: false })`. A session's `seen` now also leaves seen network posts out of trend pages, previews and topic pages; in a category a topic page with nothing new ends its topic and after `DISCOVERY_EMPTY_TOPIC_PAGES` (3) in a row the network's other topics are skipped. 0.7.84: a featured module carries `theme` only when its lead window was themed so and most of its own posts were judged into that category (a category feed's posts, by the read's category filter); a feed with no category titles every module by its kind alone ("Videos", not "Videos in Technology" over an unjudged Nostr clip). A post is shown once by every identity (`discoveryItemIdentities(item)`: its key, an open-network post's network id, and each media file by address and by content hash, so a second note carrying the same clip is the same post) across a session's pages, a progressive feed's sources (`withoutSeenItems` takes identities or keys), `mergeDiscoveryArrival` and gallery pooling; `withoutRepeatedDiscoveryItems(rows, shown)` does the same for an app's own rows.
|
|
917
948
|
- Viral read (0.7.59; off until the server enables it for the app): `const { data } = await r.protocols.getPublicViral({ media: 'video', category: 'science', limit: 20 })` (member key, users:read + posts:read, never cached) or keyless `visitor.protocols.getSharedViral(projectId, { media: 'video', compass: 'hide' })` (visitors send compass `all` or `hide`, and a side or `mix` only where the catalog lists `compass_sides`). Params, all optional: `network` (all, minds, bluesky, activitypub, nostr), `category`, `media: 'video'`, `limit` 5/10/20/40, `cursor` (the last page's; a cursor whose 30-minute build expired ends the list), `compass`. Each item: `key` (stable; drop ones already shown), `protocol`, `post` (checking `protocol` narrows it: `ViralMindsCard` for minds, open at /post/:id; otherwise a `SocialPost`), `label` viral/rising/hot, `score`, `rank`, `category`, `velocity` (show counts with `countsReportedBy` as reported, never verified), `spread`. A visitor's read serves media only where the server cleared it, so even with `media: 'video'` some items arrive with the video withheld (empty `media`): `import { viralItemHasVideo } from '@recursiv/sdk'` and keep `data.items.filter(viralItemHasVideo)` before a vertical video player. `status` ok/building/screening/off: off resolves with no items, and `getSharedViral` also resolves an older API's 401 `missing_api_key` as off; `getPublicViral` throws every 401/403 (never off), so catch it and skip that step.
|
|
918
949
|
- Public trends: require `public_trends` from `publicNetworks()`, then `getPublicTrends({ protocol, limit?, cursor?, compass?, media? })` (`compass: 'hide'` leaves out politics, 0.7.62; `media: 'video'`, 0.7.73, Bluesky: the network's own window of video posts, a `kind: posts` page paged by its cursor, no category or layout, 404 `unsupported_protocol` for a network without one, 400 on an API that predates it; keyless `getSharedTrends(projectId, { ..., media: 'video' })` holds only clips a visitor may see and counts the rest in `mediaFilter.withheld`); 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.
|
|
919
950
|
- 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.
|
|
951
|
+
- Keyless category surface (0.7.165): `const { data } = await visitor.protocols.getSharedCategorySurface(projectId, { category: 'science', compass: 'hide' })` gives a signed-out category page its `stories`, `network` (open-network posts, with `by_protocol`) and `clips` in one read, the sections a member's `r.discovery.category()` reads from the same server cache, never `people` or `groups`. Every visitor floor runs again on each read (tier-1, held and blocked-instance items, safety and term floors, NSFW, media only where cleared for visitors: a post's pictures keep their `width` and `height`). `compass`: `all` or `hide` (Hide politics on all three, counted in `data.meta.compass_filter`); a side or Mix only where the app's catalog lists `compass_sides` for visitors (else a 400), answered with Everything's sections marked `unavailable`, since no section carries a placement. A short or failed section says why in `reason`. Send only `category` and `compass`: the answer is one per app, category and choice, so the edge may keep it 30 s.
|
|
920
952
|
- 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.
|
|
921
953
|
- 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`, and a side or `mix` only where the catalog lists `compass_sides`, 0.7.116; 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; `compass_sides` (0.7.116) says the viewer may choose sides and Mix too (a visitor only where the operator serves visitors the whole filter).
|
|
922
954
|
- 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.
|