@recursiv/sdk 0.7.46 → 0.7.48
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 +5 -3
- package/dist/client.d.ts +4 -1
- package/dist/client.js +22 -14
- package/dist/errors.d.ts +11 -0
- package/dist/errors.js +27 -0
- package/dist/index.d.ts +2 -2
- package/dist/resources/deployments.d.ts +8 -0
- package/dist/resources/discovery.d.ts +27 -2
- package/dist/resources/discovery.js +73 -8
- package/dist/resources/e2ee.d.ts +20 -1
- package/dist/resources/e2ee.js +12 -1
- package/dist/resources/protocols.d.ts +44 -5
- package/dist/resources/protocols.js +21 -9
- package/llm.md +6 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -75,7 +75,7 @@ for await (const chunk of r.agents.chatStream(agent.id, { message: 'Hello!' }))
|
|
|
75
75
|
| Resource | Key methods |
|
|
76
76
|
|----------|------------|
|
|
77
77
|
| `r.agents` | `create`, `update`, `chat`, `chatStream`, `grantProjectAccess`, `conversations` |
|
|
78
|
-
| `r.e2ee` | `registerDevice`, `devices`, `revokeDevice`, `publishKeyPackages`, `claimKeyPackage`, `appendEvent`, `events`, `acknowledge` (flag-gated E2EE v2 transport) |
|
|
78
|
+
| `r.e2ee` | `registerDevice`, `devices`, `revokeDevice`, `publishKeyPackages`, `keyPackageStatus`, `claimKeyPackage`, `appendEvent`, `events`, `acknowledge` (flag-gated E2EE v2 transport) |
|
|
79
79
|
| `r.brain` | `sendMessage` |
|
|
80
80
|
| `r.commands` | `execute`, `gatePrompt` |
|
|
81
81
|
| `r.projectBrain` | `tasks`, `decisions`, `milestones`, `completeTask`, `usage` |
|
|
@@ -514,8 +514,10 @@ const { data: preview } = await r.protocols.prepareNativeReply({
|
|
|
514
514
|
parent: { protocol: 'bluesky', nativeId: selectedPost.nativeId },
|
|
515
515
|
accountIds: [blueskyAccount.id, activityPubAccount.id],
|
|
516
516
|
linkedPostAccountIds: [activityPubAccount.id],
|
|
517
|
+
// Optional, needs each account's `publish_media` capability:
|
|
518
|
+
media: [{ url: uploadedImageUrl, alt: memberWrittenAltText }],
|
|
517
519
|
})
|
|
518
|
-
// Review each delivery's account, protocol, kind, parent and exact wireText.
|
|
520
|
+
// Review each delivery's account, protocol, kind, parent, media and exact wireText.
|
|
519
521
|
// After the user approves selected delivery IDs:
|
|
520
522
|
await r.protocols.submitNativeReply(preview.id, { approve: true, deliveryIds: approvedDeliveryIds })
|
|
521
523
|
```
|
|
@@ -590,7 +592,7 @@ connections or signing authority. Missing configuration or unavailable safety ch
|
|
|
590
592
|
closed. Signed-in members continue using `getPublicConversation` to apply personal
|
|
591
593
|
blocks/mutes as well. All reply and account endpoints still require authentication.
|
|
592
594
|
|
|
593
|
-
Standalone public
|
|
595
|
+
Standalone public publication uses `protocols.prepareNativePost({ clientMutationId, text, accountIds, media? })`, then explicitly approved `submitNativePost`. Keep the same mutation ID after uncertain preparation; previously attempted deliveries are reconciled without resending. `nativePostStatus` and paginated `nativePostHistory` expose private app/user-owned outcomes. Review current own posts with `prepareNativePostRemoval`, then approve exact removal IDs through `removeNativePost`. Check connected-account `publish_text` capabilities; only explicitly enabled apps may publish. Text is sent unchanged, public only, with each provider’s actual limits. Optional `media` is a `NativeMediaInput[]` of at most four already-uploaded Minds images (`{ url, alt?, width?, height?, mimeType? }`, JPEG/PNG/WebP/GIF, Minds-hosted HTTPS URLs only, a Mastodon GIF must be the only attachment). It additionally requires the account's `publish_media` capability; accounts connected before image permissions existed must reconnect. Pass the member's own `alt` — an omitted one is sent as an empty description, never an invented one — and read `receipt.media` (`{ count, alt_missing }`) after sending. These methods still do not accept a reply parent or caller-supplied authority.
|
|
594
596
|
|
|
595
597
|
For Bluesky accounts advertising `quote`, prepare the same publication with `quote: { protocol: 'bluesky', nativeId: sourceAtUri }`. The server resolves the exact source CID using each connected account, checks quote permission and Minds safety, and rechecks before writing. Review each delivery’s `quote` alongside the account and comment before submitting. Changed or unavailable sources cannot publish; uncertain deliveries reconcile the original record key. ActivityPub/Nostr quotes are not provided by this method. Public post reads include at most one filtered `quote` with `state: 'available'` and `post`, or `state: 'unavailable'` without source content. Available quotes may include `embeddedReferences`: exact standalone source-reference lines in the unchanged outer `text`. Omit them only when the quoted original is actually visible; retain them for unavailable or locally hidden quotes. Nostr references are decoded and matched by the server, without following relay hints.
|
|
596
598
|
|
package/dist/client.d.ts
CHANGED
|
@@ -29,7 +29,10 @@ export declare class HttpClient {
|
|
|
29
29
|
apiKeyAuthHeader(): Record<string, string>;
|
|
30
30
|
private getAuthHeaders;
|
|
31
31
|
private handleResponse;
|
|
32
|
-
|
|
32
|
+
/** `retry: false` answers after one attempt: for a caller with its own deadline, a retried 5xx only adds wait. */
|
|
33
|
+
get<T>(path: string, params?: Record<string, unknown>, options?: {
|
|
34
|
+
retry?: boolean;
|
|
35
|
+
}): Promise<T>;
|
|
33
36
|
post<T>(path: string, body?: unknown, options?: {
|
|
34
37
|
retry?: boolean;
|
|
35
38
|
}): Promise<T>;
|
package/dist/client.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { RecursivError, AuthenticationError, AuthorizationError, NotFoundError, ValidationError, InsufficientCreditsError, RateLimitError, ConflictError, } from './errors.js';
|
|
1
|
+
import { RecursivError, AuthenticationError, AuthorizationError, NotFoundError, ValidationError, InsufficientCreditsError, RateLimitError, ConflictError, errorBodyFromResponse, httpStatusMessage, } from './errors.js';
|
|
2
2
|
const DEFAULT_BASE_URL = 'https://api.recursiv.io/api/v1';
|
|
3
3
|
export class HttpClient {
|
|
4
4
|
#apiKey;
|
|
@@ -118,8 +118,20 @@ export class HttpClient {
|
|
|
118
118
|
code: 'no_response',
|
|
119
119
|
});
|
|
120
120
|
}
|
|
121
|
+
// From here on the server answered. Every error below carries its HTTP
|
|
122
|
+
// status, so callers can tell a refusal from an unreachable server; only a
|
|
123
|
+
// fetch that produced no response (network failure, abort) is statusless.
|
|
121
124
|
if (response.ok) {
|
|
122
|
-
|
|
125
|
+
try {
|
|
126
|
+
return (await response.json());
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
throw new RecursivError(response.status, {
|
|
130
|
+
type: 'api_error',
|
|
131
|
+
message: `${httpStatusMessage(response.status, response.statusText)}: response body is not valid JSON`,
|
|
132
|
+
code: 'invalid_response',
|
|
133
|
+
});
|
|
134
|
+
}
|
|
123
135
|
}
|
|
124
136
|
let body;
|
|
125
137
|
try {
|
|
@@ -128,16 +140,13 @@ export class HttpClient {
|
|
|
128
140
|
catch {
|
|
129
141
|
throw new RecursivError(response.status, {
|
|
130
142
|
type: 'api_error',
|
|
131
|
-
message:
|
|
143
|
+
message: httpStatusMessage(response.status, response.statusText),
|
|
132
144
|
code: 'unknown_error',
|
|
133
145
|
});
|
|
134
146
|
}
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
const
|
|
138
|
-
const err = typeof rawErr === 'string'
|
|
139
|
-
? { type: 'api_error', message: rawErr, code: 'unknown_error' }
|
|
140
|
-
: rawErr;
|
|
147
|
+
// Handles { error: { message, type, code } } (standard), { error: "string" }
|
|
148
|
+
// (legacy dispatcher routes) and any other JSON, which gets a generic body.
|
|
149
|
+
const err = errorBodyFromResponse(response.status, response.statusText, body);
|
|
141
150
|
switch (response.status) {
|
|
142
151
|
case 400:
|
|
143
152
|
throw new ValidationError(err);
|
|
@@ -157,12 +166,11 @@ export class HttpClient {
|
|
|
157
166
|
throw new RecursivError(response.status, err);
|
|
158
167
|
}
|
|
159
168
|
}
|
|
160
|
-
|
|
169
|
+
/** `retry: false` answers after one attempt: for a caller with its own deadline, a retried 5xx only adds wait. */
|
|
170
|
+
async get(path, params, options) {
|
|
161
171
|
const url = this.buildUrl(path, params);
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
headers: this.getAuthHeaders(),
|
|
165
|
-
});
|
|
172
|
+
const request = { method: 'GET', headers: this.getAuthHeaders() };
|
|
173
|
+
const response = await (options?.retry === false ? this.fetchWithTimeout(url, request) : this.fetchWithRetry(url, request));
|
|
166
174
|
return this.handleResponse(response);
|
|
167
175
|
}
|
|
168
176
|
async post(path, body, options) {
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
import type { ApiErrorBody } from './types.js';
|
|
2
|
+
/** `HTTP 403: Forbidden`, or `HTTP 403` when the response has no reason phrase (HTTP/2). */
|
|
3
|
+
export declare function httpStatusMessage(status: number, statusText?: string): string;
|
|
4
|
+
/**
|
|
5
|
+
* Turn whatever JSON an error response carried into a complete error body.
|
|
6
|
+
* The standard shape passes through unchanged; the legacy `{ error: "text" }`
|
|
7
|
+
* shape keeps its text; anything else (no `error`, a null/number/array error,
|
|
8
|
+
* a non-object body) becomes a generic body. A partial error object keeps the
|
|
9
|
+
* fields it has and gains defaults for `type`, `code` and `message`, so the
|
|
10
|
+
* thrown error always has the response status and never a TypeError.
|
|
11
|
+
*/
|
|
12
|
+
export declare function errorBodyFromResponse(status: number, statusText: string | undefined, body: unknown): ApiErrorBody['error'];
|
|
2
13
|
export declare class RecursivError extends Error {
|
|
3
14
|
readonly status: number;
|
|
4
15
|
readonly type: string;
|
package/dist/errors.js
CHANGED
|
@@ -1,3 +1,30 @@
|
|
|
1
|
+
/** `HTTP 403: Forbidden`, or `HTTP 403` when the response has no reason phrase (HTTP/2). */
|
|
2
|
+
export function httpStatusMessage(status, statusText) {
|
|
3
|
+
return statusText ? `HTTP ${status}: ${statusText}` : `HTTP ${status}`;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Turn whatever JSON an error response carried into a complete error body.
|
|
7
|
+
* The standard shape passes through unchanged; the legacy `{ error: "text" }`
|
|
8
|
+
* shape keeps its text; anything else (no `error`, a null/number/array error,
|
|
9
|
+
* a non-object body) becomes a generic body. A partial error object keeps the
|
|
10
|
+
* fields it has and gains defaults for `type`, `code` and `message`, so the
|
|
11
|
+
* thrown error always has the response status and never a TypeError.
|
|
12
|
+
*/
|
|
13
|
+
export function errorBodyFromResponse(status, statusText, body) {
|
|
14
|
+
const fallback = { type: 'api_error', code: 'unknown_error', message: httpStatusMessage(status, statusText) };
|
|
15
|
+
const raw = body && typeof body === 'object' && !Array.isArray(body) ? body.error : undefined;
|
|
16
|
+
if (typeof raw === 'string')
|
|
17
|
+
return { ...fallback, message: raw };
|
|
18
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
19
|
+
return fallback;
|
|
20
|
+
const error = raw;
|
|
21
|
+
return {
|
|
22
|
+
...error,
|
|
23
|
+
type: typeof error.type === 'string' ? error.type : fallback.type,
|
|
24
|
+
code: typeof error.code === 'string' ? error.code : fallback.code,
|
|
25
|
+
message: typeof error.message === 'string' ? error.message : fallback.message,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
1
28
|
function composeMessage(body) {
|
|
2
29
|
// Surface the hint inline so LLM callers see the resolution path without
|
|
3
30
|
// having to inspect a structured field. The base message stays first so
|
package/dist/index.d.ts
CHANGED
|
@@ -144,7 +144,7 @@ export { UsersResource } from './resources/users.js';
|
|
|
144
144
|
export { CommunitiesResource } from './resources/communities.js';
|
|
145
145
|
export { ChatResource } from './resources/chat.js';
|
|
146
146
|
export { E2eeResource } from './resources/e2ee.js';
|
|
147
|
-
export type { E2eeDevice, E2eeEnvelope, E2eeEvent } from './resources/e2ee.js';
|
|
147
|
+
export type { E2eeDevice, E2eeEnvelope, E2eeEvent, E2eeKeyPackageStatus } from './resources/e2ee.js';
|
|
148
148
|
export { ProjectsResource } from './resources/projects.js';
|
|
149
149
|
export { AgentsResource, SUPPORTED_MODELS } from './resources/agents.js';
|
|
150
150
|
export { EvidenceResource } from './resources/evidence.js';
|
|
@@ -198,7 +198,7 @@ export type { UploadMediaInput, UploadUrlInput, UploadUrlResult } from './resour
|
|
|
198
198
|
export { WalletResource } from './resources/wallet.js';
|
|
199
199
|
export { ProtocolsResource } from './resources/protocols.js';
|
|
200
200
|
export { PROTOCOL_ADAPTER_MATURITIES, PROTOCOL_CAPABILITY_OPERATIONS, PROTOCOL_CAPABILITY_STATUSES, } from './resources/protocols.js';
|
|
201
|
-
export type { SocialProfile, SocialSubscription, SocialSubscriptionChange, SocialProfilePage, SocialNetwork, SocialPost, SocialConversation, SocialTrend, SocialTrendProvenance, SocialTrends, SocialTrendFeed, SocialPopularProfiles, SocialAuthorFeed, SocialFollowingFeed, SocialFollowingSource, SearchPublicProfilesParams, SearchPublicPostsParams, SocialPostSearch, HostedProtocolConnections, HostedProtocol, HostedProtocolIdentity, HostedNostrChallenge, HostedNostrIdentityProof, NativeNostrPrepareInput, NativeNostrPrepared, NativeNostrAccount, NativeNostrPostState, NativeNostrReceipt, NativeSocialProtocol, FederationIdentity, FederationIdentityView, NativeSocialAccount, NativeSocialCallbackInput, NativeReplyPrepareInput, NativePostPrepareInput, NativePostBatch, NativePostHistory, NativeFollowSourceInput, NativeFollowPrepareInput, NativeFollowTarget, NativeFollowReceipt, NativeFollowState, NativeFollowReview, NativeLikeSourceInput, NativeRepostSourceInput, NativeLikePrepareInput, NativeRepostPrepareInput, NativeLikeTarget, NativeRepostTarget, NativeLikeReceipt, NativeRepostReceipt, NativeLikeState, NativeRepostState, NativeLikeReview, NativeRepostReview, NativeReplyReceipt, NativeReplyBatch, NativeReplyHistory, NativeDelegatedReplyHistoryItem, NativeNostrReplyResult, NativeNostrReplyHistoryItem, NativeReplyRemovalReceipt, NativeReplyRemovalPreview, SocialSourceRef, SocialSourceControl, SocialSourceControlChange, HostedPostPreview, ConnectHostedIdentityInput, PreviewHostedPostInput, PublishHostedPostInput, ListProtocolCandidatesParams, ProtocolAdapterMaturity, ProtocolAdapterMetadata, ProtocolCandidateListResponse, ProtocolCandidatePageMeta, ProtocolCapabilityOperation, ProtocolCapabilityStatus, ProtocolCollectionStatus, ProtocolIngestionReplayResult, ProtocolIngestionStatus, ProtocolQuarantinedCandidate, ProtocolRefreshResult, ProtocolSettings, ProtocolSettingsUpdateResult, ReplayProtocolIngestionJobInput, UpdateSettingsInput as UpdateProtocolSettingsInput, } from './resources/protocols.js';
|
|
201
|
+
export type { SocialProfile, SocialSubscription, SocialSubscriptionChange, SocialProfilePage, SocialNetwork, SocialPost, SocialConversation, SocialTrend, SocialTrendProvenance, SocialTrends, SocialTrendFeed, SocialPopularProfiles, SocialAuthorFeed, SocialFollowingFeed, SocialFollowingSource, SearchPublicProfilesParams, SearchPublicPostsParams, SocialPostSearch, HostedProtocolConnections, HostedProtocol, HostedProtocolIdentity, HostedNostrChallenge, HostedNostrIdentityProof, NativeNostrPrepareInput, NativeNostrPrepared, NativeNostrAccount, NativeNostrPostState, NativeNostrReceipt, NativeSocialProtocol, FederationIdentity, FederationIdentityView, NativeSocialAccount, NativeSocialCallbackInput, NativeReplyPrepareInput, NativePostPrepareInput, NativeMediaInput, NativeMediaItem, NativePostBatch, NativePostHistory, NativeFollowSourceInput, NativeFollowPrepareInput, NativeFollowTarget, NativeFollowReceipt, NativeFollowState, NativeFollowReview, NativeLikeSourceInput, NativeRepostSourceInput, NativeLikePrepareInput, NativeRepostPrepareInput, NativeLikeTarget, NativeRepostTarget, NativeLikeReceipt, NativeRepostReceipt, NativeLikeState, NativeRepostState, NativeLikeReview, NativeRepostReview, NativeReplyReceipt, NativeReplyBatch, NativeReplyHistory, NativeDelegatedReplyHistoryItem, NativeNostrReplyResult, NativeNostrReplyHistoryItem, NativeReplyRemovalReceipt, NativeReplyRemovalPreview, SocialSourceRef, SocialSourceControl, SocialSourceControlChange, HostedPostPreview, ConnectHostedIdentityInput, PreviewHostedPostInput, PublishHostedPostInput, ListProtocolCandidatesParams, ProtocolAdapterMaturity, ProtocolAdapterMetadata, ProtocolCandidateListResponse, ProtocolCandidatePageMeta, ProtocolCapabilityOperation, ProtocolCapabilityStatus, ProtocolCollectionStatus, ProtocolIngestionReplayResult, ProtocolIngestionStatus, ProtocolQuarantinedCandidate, ProtocolRefreshResult, ProtocolSettings, ProtocolSettingsUpdateResult, ReplayProtocolIngestionJobInput, UpdateSettingsInput as UpdateProtocolSettingsInput, } from './resources/protocols.js';
|
|
202
202
|
export { InboxResource } from './resources/inbox.js';
|
|
203
203
|
export { NetworkResource } from './resources/network.js';
|
|
204
204
|
export { SimulatorResource } from './resources/simulator.js';
|
|
@@ -41,6 +41,12 @@ export interface DeleteDeploymentParams {
|
|
|
41
41
|
export interface PrepareProtocolServersInput {
|
|
42
42
|
project_id: string;
|
|
43
43
|
network_id: string;
|
|
44
|
+
/**
|
|
45
|
+
* `production` reserves the stack in the operator's production environment
|
|
46
|
+
* with the API's HOSTED_* settings (handle domain, SMTP, invites, relay name);
|
|
47
|
+
* hostnames must be allow-listed. Default `staging`.
|
|
48
|
+
*/
|
|
49
|
+
target?: 'staging' | 'production';
|
|
44
50
|
hosts: {
|
|
45
51
|
pds: string;
|
|
46
52
|
activitypub: string;
|
|
@@ -52,7 +58,9 @@ export interface PrepareProtocolServersInput {
|
|
|
52
58
|
activitypub: string;
|
|
53
59
|
nostr: string;
|
|
54
60
|
};
|
|
61
|
+
/** A staging PLC, or https://plc.directory for a production PDS whose operator set HOSTED_PDS_PRODUCTION_PLC=true. */
|
|
55
62
|
test_plc_url: string;
|
|
63
|
+
/** Relay publisher whitelist; may be empty for a public production relay. */
|
|
56
64
|
test_publisher_pubkeys: string[];
|
|
57
65
|
}
|
|
58
66
|
export interface PreparedProtocolServers {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { DiscoveryLayout, Post } from '../types.js';
|
|
2
2
|
import type { DiscoveryLearning } from './discovery-learning.js';
|
|
3
3
|
import type { PostsResource } from './posts.js';
|
|
4
|
-
import type { ProtocolsResource, SocialNetwork, SocialPost, SocialRadar } from './protocols.js';
|
|
4
|
+
import type { ProtocolsResource, SocialNetwork, SocialPost, SocialRadar, SocialRadarPosts } from './protocols.js';
|
|
5
5
|
export type DiscoveryRecommendationKind = 'groups' | 'people' | 'tags';
|
|
6
6
|
export interface DiscoveryGroupCandidate {
|
|
7
7
|
id: string;
|
|
@@ -82,6 +82,21 @@ export type DiscoveryRadarSource = (params: {
|
|
|
82
82
|
}) => Promise<{
|
|
83
83
|
data: SocialRadar;
|
|
84
84
|
}>;
|
|
85
|
+
/**
|
|
86
|
+
* Readers for a story's own member posts. A member found by the radar's search lane is almost never
|
|
87
|
+
* in the page's hot or trend window, so without these a story counts posts it cannot show.
|
|
88
|
+
* Optional, like the radar: an older server answers 404 and the story shows the posts its page carried.
|
|
89
|
+
*/
|
|
90
|
+
export interface DiscoveryRadarMemberSources {
|
|
91
|
+
social?: (params: {
|
|
92
|
+
cluster: string;
|
|
93
|
+
}) => Promise<{
|
|
94
|
+
data: SocialRadarPosts;
|
|
95
|
+
}>;
|
|
96
|
+
minds?: (id: string) => Promise<{
|
|
97
|
+
data: Post;
|
|
98
|
+
}>;
|
|
99
|
+
}
|
|
85
100
|
export type DiscoveryRadarPresentation = 'breaking' | 'story';
|
|
86
101
|
export type DiscoveryPostItem = {
|
|
87
102
|
key: string;
|
|
@@ -153,6 +168,7 @@ export declare class DiscoveryFeedSession {
|
|
|
153
168
|
private category?;
|
|
154
169
|
private learning?;
|
|
155
170
|
private recommend?;
|
|
171
|
+
private members?;
|
|
156
172
|
private streams;
|
|
157
173
|
private pending;
|
|
158
174
|
private seen;
|
|
@@ -162,13 +178,22 @@ export declare class DiscoveryFeedSession {
|
|
|
162
178
|
private clusters;
|
|
163
179
|
private radarUsed;
|
|
164
180
|
private radar?;
|
|
165
|
-
|
|
181
|
+
/** Page 0's radar read, started before its stream reads rather than after the slowest of them. */
|
|
182
|
+
private radarLoading;
|
|
183
|
+
/** Member reads per story, started as soon as the story is known and shared by whichever page shows it. */
|
|
184
|
+
private memberReads;
|
|
185
|
+
constructor(posts: Pick<PostsResource, 'list'>, protocols: Pick<ProtocolsResource, 'getPublicTrends' | 'getPublicTrendFeed'>, networks: SocialNetwork[], category?: string | undefined, source?: string, learning?: DiscoveryLearning | undefined, recommend?: DiscoveryRecommendationSources | undefined, radar?: DiscoveryRadarSource, members?: DiscoveryRadarMemberSources | undefined);
|
|
166
186
|
retryFailed(): void;
|
|
167
187
|
next(): Promise<DiscoveryFeedPage>;
|
|
168
188
|
private read;
|
|
169
189
|
private readPage;
|
|
170
190
|
/** Page 0: the top breaking cluster, else the top cluster. Every second later page: the next unused story or viral cluster. */
|
|
171
191
|
private radarCluster;
|
|
192
|
+
/** Page 0: the top breaking cluster, else the top cluster. Later pages: the next unused story or viral cluster. */
|
|
193
|
+
private pickCluster;
|
|
194
|
+
/** The story's members as feed items, read once per story and bounded to three seconds; a failed read is simply none. */
|
|
195
|
+
private memberPosts;
|
|
196
|
+
private readMembers;
|
|
172
197
|
private loadRadar;
|
|
173
198
|
private radarModule;
|
|
174
199
|
private recommendation;
|
|
@@ -4,6 +4,10 @@ export const RECOMMENDATION_MAX_ITEMS = 6;
|
|
|
4
4
|
/** Clusters read from the radar per session; the server never holds more. */
|
|
5
5
|
export const RADAR_MAX_CLUSTERS = 40;
|
|
6
6
|
const RADAR_MAX_MEMBERS = 12;
|
|
7
|
+
/** Minds members read one by one, so a story reads at most this many. */
|
|
8
|
+
const RADAR_MAX_MINDS_READS = 4;
|
|
9
|
+
/** A discovery page holds its own deadline: a retried 5xx from one provider only makes the whole page wait. */
|
|
10
|
+
const NO_RETRY = { retry: false };
|
|
7
11
|
const RECOMMENDATION_KINDS = ['groups', 'people', 'tags'];
|
|
8
12
|
const nonEmpty = (value) => typeof value === 'string' && value.trim().length > 0 && value.length <= 512;
|
|
9
13
|
function recommendationItem(kind, candidate) {
|
|
@@ -119,6 +123,7 @@ export class DiscoveryFeedSession {
|
|
|
119
123
|
category;
|
|
120
124
|
learning;
|
|
121
125
|
recommend;
|
|
126
|
+
members;
|
|
122
127
|
streams;
|
|
123
128
|
pending = null;
|
|
124
129
|
seen = new Set();
|
|
@@ -128,12 +133,17 @@ export class DiscoveryFeedSession {
|
|
|
128
133
|
clusters = null;
|
|
129
134
|
radarUsed = new Set();
|
|
130
135
|
radar;
|
|
131
|
-
|
|
136
|
+
/** Page 0's radar read, started before its stream reads rather than after the slowest of them. */
|
|
137
|
+
radarLoading = null;
|
|
138
|
+
/** Member reads per story, started as soon as the story is known and shared by whichever page shows it. */
|
|
139
|
+
memberReads = new Map();
|
|
140
|
+
constructor(posts, protocols, networks, category, source, learning, recommend, radar, members) {
|
|
132
141
|
this.posts = posts;
|
|
133
142
|
this.protocols = protocols;
|
|
134
143
|
this.category = category;
|
|
135
144
|
this.learning = learning;
|
|
136
145
|
this.recommend = recommend;
|
|
146
|
+
this.members = members;
|
|
137
147
|
// The radar clusters across every source, so a feed narrowed to one network carries none.
|
|
138
148
|
if (!source || source === 'all')
|
|
139
149
|
this.radar = radar;
|
|
@@ -174,7 +184,7 @@ export class DiscoveryFeedSession {
|
|
|
174
184
|
stream.done = true;
|
|
175
185
|
return { items: [], layout: ordinary, partial: false };
|
|
176
186
|
}
|
|
177
|
-
const { data } = await this.protocols.getPublicTrendFeed({ ...params, protocol: stream.protocol, topic: topic.topic, cursor: topic.cursor });
|
|
187
|
+
const { data } = await this.protocols.getPublicTrendFeed({ ...params, protocol: stream.protocol, topic: topic.topic, cursor: topic.cursor }, NO_RETRY);
|
|
178
188
|
validatePosts(data);
|
|
179
189
|
if (data.trend.protocol !== stream.protocol || data.trend.nativeId !== topic.topic)
|
|
180
190
|
throw new Error('Invalid discovery topic');
|
|
@@ -185,7 +195,7 @@ export class DiscoveryFeedSession {
|
|
|
185
195
|
stream.done = !stream.topics.length;
|
|
186
196
|
return { items: data.items.map(post => ({ key: key(post.protocol, post.nativeId), kind: 'social', post })), layout: data.discovery ?? ordinary, partial: data.categoryFilter?.status === 'partial' };
|
|
187
197
|
}
|
|
188
|
-
const { data } = await this.protocols.getPublicTrends({ ...params, protocol: stream.protocol, cursor: stream.cursor, includePreviews: true });
|
|
198
|
+
const { data } = await this.protocols.getPublicTrends({ ...params, protocol: stream.protocol, cursor: stream.cursor, includePreviews: true }, NO_RETRY);
|
|
189
199
|
if (data.coverage !== 'provider_ranked_window' || data.items.length > 25 || data.items.some(item => item.protocol !== stream.protocol))
|
|
190
200
|
throw new Error('Invalid discovery source response');
|
|
191
201
|
if (data.kind === 'topics') {
|
|
@@ -204,6 +214,13 @@ export class DiscoveryFeedSession {
|
|
|
204
214
|
return { items: data.items.map(post => ({ key: key(post.protocol, post.nativeId), kind: 'social', post })), layout: data.discovery ?? ordinary, partial: data.categoryFilter?.status === 'partial' };
|
|
205
215
|
}
|
|
206
216
|
async readPage() {
|
|
217
|
+
// The radar is one small snapshot read: start it with the page, not after the slowest provider,
|
|
218
|
+
// and start the lead story's member reads the moment the snapshot names it.
|
|
219
|
+
if (this.page === 0 && this.radar && !this.radarLoading) {
|
|
220
|
+
this.radarLoading = this.loadRadar();
|
|
221
|
+
void this.radarLoading.then(clusters => { const lead = this.pickCluster(clusters, 0); if (lead)
|
|
222
|
+
void this.memberPosts(lead); });
|
|
223
|
+
}
|
|
207
224
|
const active = this.streams.filter(stream => !stream.done && !stream.failed);
|
|
208
225
|
const native = active.find(stream => stream.protocol === 'minds');
|
|
209
226
|
const remote = active.filter(stream => stream.protocol !== 'minds');
|
|
@@ -255,8 +272,12 @@ export class DiscoveryFeedSession {
|
|
|
255
272
|
let clusterItems = [];
|
|
256
273
|
if (cluster) {
|
|
257
274
|
const members = new Set(cluster.memberKeys);
|
|
275
|
+
const onPage = remaining.filter(row => members.has(row.item.key)).map(row => row.item);
|
|
276
|
+
const carried = new Set(onPage.map(item => item.key));
|
|
277
|
+
// Members this page did not carry come from the story's own reads; one an earlier page showed stays shown there.
|
|
278
|
+
const read = (await this.memberPosts(cluster)).filter(item => !carried.has(item.key) && !this.seen.has(item.key));
|
|
258
279
|
// Under the story, posts keep the server's member order: the lead media member first, then by member score.
|
|
259
|
-
clusterItems =
|
|
280
|
+
clusterItems = [...onPage, ...read].sort((a, b) => cluster.memberKeys.indexOf(a.key) - cluster.memberKeys.indexOf(b.key));
|
|
260
281
|
remaining = remaining.filter(row => !members.has(row.item.key));
|
|
261
282
|
for (const memberKey of cluster.memberKeys)
|
|
262
283
|
this.seen.add(memberKey);
|
|
@@ -310,11 +331,10 @@ export class DiscoveryFeedSession {
|
|
|
310
331
|
if (!this.radar)
|
|
311
332
|
return null;
|
|
312
333
|
if (page === 0)
|
|
313
|
-
this.clusters = await this.loadRadar();
|
|
334
|
+
this.clusters = await (this.radarLoading ?? this.loadRadar());
|
|
314
335
|
if (!this.clusters?.length || (page !== 0 && page % 2 !== 0))
|
|
315
336
|
return null;
|
|
316
|
-
const
|
|
317
|
-
const cluster = page === 0 ? unused.find(candidate => candidate.kind === 'breaking') ?? unused[0] : unused.find(candidate => candidate.kind !== 'breaking');
|
|
337
|
+
const cluster = this.pickCluster(this.clusters, page);
|
|
318
338
|
if (!cluster)
|
|
319
339
|
return null;
|
|
320
340
|
this.radarUsed.add(cluster.id);
|
|
@@ -323,6 +343,45 @@ export class DiscoveryFeedSession {
|
|
|
323
343
|
return null;
|
|
324
344
|
return cluster;
|
|
325
345
|
}
|
|
346
|
+
/** Page 0: the top breaking cluster, else the top cluster. Later pages: the next unused story or viral cluster. */
|
|
347
|
+
pickCluster(clusters, page) {
|
|
348
|
+
const unused = clusters.filter(candidate => !this.radarUsed.has(candidate.id));
|
|
349
|
+
return page === 0 ? unused.find(candidate => candidate.kind === 'breaking') ?? unused[0] : unused.find(candidate => candidate.kind !== 'breaking');
|
|
350
|
+
}
|
|
351
|
+
/** The story's members as feed items, read once per story and bounded to three seconds; a failed read is simply none. */
|
|
352
|
+
memberPosts(cluster) {
|
|
353
|
+
let pending = this.memberReads.get(cluster.id);
|
|
354
|
+
if (!pending) {
|
|
355
|
+
pending = this.readMembers(cluster);
|
|
356
|
+
this.memberReads.set(cluster.id, pending);
|
|
357
|
+
}
|
|
358
|
+
return pending;
|
|
359
|
+
}
|
|
360
|
+
async readMembers(cluster) {
|
|
361
|
+
const wanted = new Set(cluster.memberKeys);
|
|
362
|
+
const mindsIds = cluster.memberKeys.flatMap(memberKey => { try {
|
|
363
|
+
const [kind, id] = JSON.parse(memberKey);
|
|
364
|
+
return kind === 'minds' ? [id] : [];
|
|
365
|
+
}
|
|
366
|
+
catch {
|
|
367
|
+
return [];
|
|
368
|
+
} })
|
|
369
|
+
.slice(0, RADAR_MAX_MINDS_READS);
|
|
370
|
+
const social = this.members?.social && mindsIds.length < cluster.memberKeys.length
|
|
371
|
+
? withDeadline(this.members.social({ cluster: cluster.id }).then(({ data }) => {
|
|
372
|
+
if (!data || data.cluster !== cluster.id || !Array.isArray(data.items))
|
|
373
|
+
return [];
|
|
374
|
+
return data.items.filter(post => post && typeof post.nativeId === 'string' && post.author?.protocol === post.protocol && wanted.has(key(post.protocol, post.nativeId)))
|
|
375
|
+
.map(post => ({ key: key(post.protocol, post.nativeId), kind: 'social', post }));
|
|
376
|
+
}), [])
|
|
377
|
+
: Promise.resolve([]);
|
|
378
|
+
const minds = this.members?.minds
|
|
379
|
+
? Promise.all(mindsIds.map(id => withDeadline(this.members.minds(id).then(({ data }) => data && data.id === id ? [{ key: key('minds', id), kind: 'minds', post: data }] : []), []))).then(rows => rows.flat())
|
|
380
|
+
: Promise.resolve([]);
|
|
381
|
+
const items = [...await social, ...await minds];
|
|
382
|
+
const unique = new Map(items.map(item => [item.key, item]));
|
|
383
|
+
return [...unique.values()];
|
|
384
|
+
}
|
|
326
385
|
async loadRadar() {
|
|
327
386
|
let response;
|
|
328
387
|
let cancel = () => { };
|
|
@@ -409,6 +468,12 @@ export class DiscoveryResource {
|
|
|
409
468
|
}
|
|
410
469
|
createFeed(options) {
|
|
411
470
|
// The radar rides along by default; an older server answers 404 and the session simply shows no story module.
|
|
412
|
-
return new DiscoveryFeedSession(this.posts, this.protocols, options.networks, options.category, options.source, options.learning, options.recommend, params => this.protocols.getPublicRadar(params));
|
|
471
|
+
return new DiscoveryFeedSession(this.posts, this.protocols, options.networks, options.category, options.source, options.learning, options.recommend, params => this.protocols.getPublicRadar(params, NO_RETRY), { social: params => this.protocols.getPublicRadarPosts(params, NO_RETRY), minds: id => this.posts.get(id) });
|
|
413
472
|
}
|
|
414
473
|
}
|
|
474
|
+
/** A read bounded to three seconds; a late or failed one resolves to `fallback`, never an error. */
|
|
475
|
+
function withDeadline(read, fallback, ms = 3000) {
|
|
476
|
+
let timer;
|
|
477
|
+
return Promise.race([read.catch(() => fallback), new Promise(resolve => { timer = setTimeout(() => resolve(fallback), ms); })])
|
|
478
|
+
.finally(() => clearTimeout(timer));
|
|
479
|
+
}
|
package/dist/resources/e2ee.d.ts
CHANGED
|
@@ -30,14 +30,23 @@ export interface E2eeEvent {
|
|
|
30
30
|
welcome: string | null;
|
|
31
31
|
createdAt: string;
|
|
32
32
|
}
|
|
33
|
+
/** One-time key packages a device still has to hand out. */
|
|
34
|
+
export interface E2eeKeyPackageStatus {
|
|
35
|
+
/** Unclaimed packages that have not expired. Once this reaches 0 nobody can add the device to a new conversation. */
|
|
36
|
+
available: number;
|
|
37
|
+
/** Of `available`, how many expire within 7 days. */
|
|
38
|
+
expiringSoon: number;
|
|
39
|
+
}
|
|
33
40
|
/**
|
|
34
41
|
* E2EE v2 ciphertext transport. The server relays envelopes and public
|
|
35
42
|
* credentials only; encryption happens in the shared core on each device.
|
|
36
|
-
* Requires the `e2eeV2` network feature flag
|
|
43
|
+
* Requires the `e2eeV2` network feature flag and a key acting as the account
|
|
44
|
+
* owner (agent-scoped keys are refused). The realm is the network id.
|
|
37
45
|
*/
|
|
38
46
|
export declare class E2eeResource {
|
|
39
47
|
private client;
|
|
40
48
|
constructor(client: HttpClient);
|
|
49
|
+
/** Register this device's public identity and its signed approval chain. */
|
|
41
50
|
registerDevice(input: {
|
|
42
51
|
device_name: string;
|
|
43
52
|
realm: string;
|
|
@@ -50,12 +59,22 @@ export declare class E2eeResource {
|
|
|
50
59
|
data: E2eeDevice[];
|
|
51
60
|
}>;
|
|
52
61
|
revokeDevice(deviceId: string): Promise<SingleResponse<E2eeDevice>>;
|
|
62
|
+
/** Publish one-time key packages for one of your devices; `available` is how many it now has. */
|
|
53
63
|
publishKeyPackages(input: {
|
|
54
64
|
device_id: string;
|
|
55
65
|
packages: string[];
|
|
56
66
|
}): Promise<SingleResponse<{
|
|
57
67
|
published: number;
|
|
68
|
+
available: number;
|
|
58
69
|
}>>;
|
|
70
|
+
/**
|
|
71
|
+
* How many one-time key packages one of your active devices still has to
|
|
72
|
+
* hand out. Publish more when `available` runs low or most of it is
|
|
73
|
+
* `expiringSoon`: at 0, nobody can add the device to a new conversation.
|
|
74
|
+
*/
|
|
75
|
+
keyPackageStatus(params: {
|
|
76
|
+
device_id: string;
|
|
77
|
+
}): Promise<SingleResponse<E2eeKeyPackageStatus>>;
|
|
59
78
|
claimKeyPackage(input: {
|
|
60
79
|
device_id: string;
|
|
61
80
|
target_device_id: string;
|
package/dist/resources/e2ee.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* E2EE v2 ciphertext transport. The server relays envelopes and public
|
|
3
3
|
* credentials only; encryption happens in the shared core on each device.
|
|
4
|
-
* Requires the `e2eeV2` network feature flag
|
|
4
|
+
* Requires the `e2eeV2` network feature flag and a key acting as the account
|
|
5
|
+
* owner (agent-scoped keys are refused). The realm is the network id.
|
|
5
6
|
*/
|
|
6
7
|
export class E2eeResource {
|
|
7
8
|
client;
|
|
8
9
|
constructor(client) {
|
|
9
10
|
this.client = client;
|
|
10
11
|
}
|
|
12
|
+
/** Register this device's public identity and its signed approval chain. */
|
|
11
13
|
registerDevice(input) {
|
|
12
14
|
return this.client.post('/e2ee/devices', input);
|
|
13
15
|
}
|
|
@@ -17,9 +19,18 @@ export class E2eeResource {
|
|
|
17
19
|
revokeDevice(deviceId) {
|
|
18
20
|
return this.client.post(`/e2ee/devices/${encodeURIComponent(deviceId)}/revoke`);
|
|
19
21
|
}
|
|
22
|
+
/** Publish one-time key packages for one of your devices; `available` is how many it now has. */
|
|
20
23
|
publishKeyPackages(input) {
|
|
21
24
|
return this.client.post('/e2ee/key-packages', input);
|
|
22
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* How many one-time key packages one of your active devices still has to
|
|
28
|
+
* hand out. Publish more when `available` runs low or most of it is
|
|
29
|
+
* `expiringSoon`: at 0, nobody can add the device to a new conversation.
|
|
30
|
+
*/
|
|
31
|
+
keyPackageStatus(params) {
|
|
32
|
+
return this.client.get('/e2ee/key-packages/status', params);
|
|
33
|
+
}
|
|
23
34
|
claimKeyPackage(input) {
|
|
24
35
|
return this.client.post('/e2ee/key-packages/claim', input);
|
|
25
36
|
}
|
|
@@ -211,6 +211,8 @@ export interface HostedProtocolConnections {
|
|
|
211
211
|
servers: {
|
|
212
212
|
protocol: 'atproto' | 'activitypub' | 'nostr';
|
|
213
213
|
url: string;
|
|
214
|
+
/** Handle suffixes the PDS issues (`minds.com` -> `alice.minds.com`); absent means the PDS hostname. */
|
|
215
|
+
handleDomains?: string[];
|
|
214
216
|
}[];
|
|
215
217
|
}
|
|
216
218
|
export type HostedProtocol = 'atproto' | 'activitypub' | 'nostr';
|
|
@@ -300,6 +302,18 @@ export interface SocialSourceControl {
|
|
|
300
302
|
export type SocialSourceControlChange = Omit<SocialSourceControl, 'id' | 'createdAt'> & {
|
|
301
303
|
enabled: boolean;
|
|
302
304
|
};
|
|
305
|
+
/** One image already uploaded to Minds storage. Only Minds-hosted HTTPS URLs are accepted; JPEG, PNG, WebP or GIF; at most four per post. */
|
|
306
|
+
export interface NativeMediaInput {
|
|
307
|
+
url: string;
|
|
308
|
+
/** The member's alt text. When omitted an empty description is sent, never an invented one. */
|
|
309
|
+
alt?: string;
|
|
310
|
+
width?: number;
|
|
311
|
+
height?: number;
|
|
312
|
+
mimeType?: 'image/jpeg' | 'image/png' | 'image/webp' | 'image/gif';
|
|
313
|
+
}
|
|
314
|
+
export interface NativeMediaItem extends NativeMediaInput {
|
|
315
|
+
alt: string;
|
|
316
|
+
}
|
|
303
317
|
export interface NativeReplyPrepareInput {
|
|
304
318
|
clientMutationId: string;
|
|
305
319
|
text: string;
|
|
@@ -309,6 +323,8 @@ export interface NativeReplyPrepareInput {
|
|
|
309
323
|
readRef?: string;
|
|
310
324
|
};
|
|
311
325
|
accountIds: string[];
|
|
326
|
+
/** Minds-hosted images attached to every selected destination. Omit for a text-only reply. */
|
|
327
|
+
media?: NativeMediaInput[];
|
|
312
328
|
/** Selected different-network accounts to review as standalone posts with a source link. */
|
|
313
329
|
linkedPostAccountIds?: string[];
|
|
314
330
|
/** Fresh review of selected unsent or definitively rejected deliveries. The original selected reviews are atomically fenced; preserve this mutation ID on retry. */
|
|
@@ -518,6 +534,11 @@ export interface NativeReplyReceipt {
|
|
|
518
534
|
providerId: string | null;
|
|
519
535
|
acknowledged: boolean;
|
|
520
536
|
readBack: 'matched' | 'absent' | 'unavailable';
|
|
537
|
+
/** Present when the delivery carried images: how many, and how many had no alt text. */
|
|
538
|
+
media?: {
|
|
539
|
+
count: number;
|
|
540
|
+
alt_missing: number;
|
|
541
|
+
};
|
|
521
542
|
}
|
|
522
543
|
export interface NativeReplyRemovalReceipt {
|
|
523
544
|
/** Absence is observed on the owning server; this is not global erasure. */
|
|
@@ -581,6 +602,8 @@ export interface NativeReplyBatch {
|
|
|
581
602
|
text: string;
|
|
582
603
|
wireText: string;
|
|
583
604
|
receipt: NativeReplyReceipt | null;
|
|
605
|
+
/** The reviewed images exactly as they will be sent. */
|
|
606
|
+
media?: NativeMediaItem[];
|
|
584
607
|
removal: {
|
|
585
608
|
id: string;
|
|
586
609
|
expiresAt: string;
|
|
@@ -589,11 +612,13 @@ export interface NativeReplyBatch {
|
|
|
589
612
|
} | null;
|
|
590
613
|
}>;
|
|
591
614
|
}
|
|
592
|
-
/** Public text
|
|
615
|
+
/** Public text and Minds-hosted images; accounts are selected explicitly in the shared editor. */
|
|
593
616
|
export interface NativePostPrepareInput {
|
|
594
617
|
clientMutationId: string;
|
|
595
618
|
text: string;
|
|
596
619
|
accountIds: string[];
|
|
620
|
+
/** Minds-hosted images attached to every selected destination. Omit for a text-only post. */
|
|
621
|
+
media?: NativeMediaInput[];
|
|
597
622
|
/** Native quote on Bluesky only. The server reads the source and permissions using each selected account. */
|
|
598
623
|
quote?: {
|
|
599
624
|
protocol: 'bluesky';
|
|
@@ -1026,6 +1051,16 @@ export interface SocialRadarParams {
|
|
|
1026
1051
|
category?: string;
|
|
1027
1052
|
limit?: number;
|
|
1028
1053
|
}
|
|
1054
|
+
/** One radar story's social member posts, in the cluster's member order. Empty once the story leaves the snapshot. */
|
|
1055
|
+
export interface SocialRadarPosts {
|
|
1056
|
+
cluster: string;
|
|
1057
|
+
builtAt: string | null;
|
|
1058
|
+
items: SocialPost[];
|
|
1059
|
+
}
|
|
1060
|
+
/** Read options for callers that hold their own deadline, such as a discovery page. */
|
|
1061
|
+
export interface SocialReadOptions {
|
|
1062
|
+
retry?: boolean;
|
|
1063
|
+
}
|
|
1029
1064
|
/** Provider-ranked accounts. `source` says whether this page is the app's hourly index snapshot or a live read. */
|
|
1030
1065
|
export interface SocialPopularProfiles {
|
|
1031
1066
|
items: SocialProfile[];
|
|
@@ -1147,7 +1182,7 @@ export declare class ProtocolsResource {
|
|
|
1147
1182
|
includePreviews?: boolean;
|
|
1148
1183
|
category?: string;
|
|
1149
1184
|
discovery?: boolean;
|
|
1150
|
-
}): Promise<SingleResponse<SocialTrends>>;
|
|
1185
|
+
}, options?: SocialReadOptions): Promise<SingleResponse<SocialTrends>>;
|
|
1151
1186
|
/** Open a topic currently listed by the provider. Use its nativeId as topic and preserve the opaque cursor. */
|
|
1152
1187
|
getPublicTrendFeed(params: {
|
|
1153
1188
|
protocol: string;
|
|
@@ -1156,9 +1191,13 @@ export declare class ProtocolsResource {
|
|
|
1156
1191
|
cursor?: string;
|
|
1157
1192
|
category?: string;
|
|
1158
1193
|
discovery?: boolean;
|
|
1159
|
-
}): Promise<SingleResponse<SocialTrendFeed>>;
|
|
1194
|
+
}, options?: SocialReadOptions): Promise<SingleResponse<SocialTrendFeed>>;
|
|
1160
1195
|
/** The app's topic radar under its own key: the same snapshot the keyless route serves. Never triggers a build. */
|
|
1161
|
-
getPublicRadar(params?: SocialRadarParams): Promise<SingleResponse<SocialRadar>>;
|
|
1196
|
+
getPublicRadar(params?: SocialRadarParams, options?: SocialReadOptions): Promise<SingleResponse<SocialRadar>>;
|
|
1197
|
+
/** One radar story's social member posts, so its card can show the posts it counts. Minds members are read with `posts.get`. */
|
|
1198
|
+
getPublicRadarPosts(params: {
|
|
1199
|
+
cluster: string;
|
|
1200
|
+
}, options?: SocialReadOptions): Promise<SingleResponse<SocialRadarPosts>>;
|
|
1162
1201
|
/** Provider-ranked public accounts with explicit provenance; served from the app's index when a fresh snapshot exists. */
|
|
1163
1202
|
getPublicPopularProfiles(params: {
|
|
1164
1203
|
protocol: string;
|
|
@@ -1198,7 +1237,7 @@ export declare class ProtocolsResource {
|
|
|
1198
1237
|
}): Promise<SingleResponse<NativeRepostReview>>;
|
|
1199
1238
|
/** Reconcile the saved action without sending another native write. */
|
|
1200
1239
|
nativeRepostStatus(id: string): Promise<SingleResponse<NativeRepostReview>>;
|
|
1201
|
-
/** Prepare exact public text for selected connected Bluesky/ActivityPub accounts. */
|
|
1240
|
+
/** Prepare exact public text and reviewed images for selected connected Bluesky/ActivityPub accounts. */
|
|
1202
1241
|
prepareNativePost(input: NativePostPrepareInput): Promise<SingleResponse<NativePostBatch>>;
|
|
1203
1242
|
/** Explicitly approve selected deliveries; never auto-retry an uncertain write. */
|
|
1204
1243
|
submitNativePost(id: string, input: {
|
|
@@ -35,9 +35,15 @@ function hostedIdentityPath(id) {
|
|
|
35
35
|
return `/protocols/identities/${encodeURIComponent(id)}`;
|
|
36
36
|
}
|
|
37
37
|
/** Only the two radar fields travel; caller scope or destination extras never reach the query string. */
|
|
38
|
+
/** Forwarded only when given, so a default read makes exactly the call it always made. */
|
|
39
|
+
const readOptions = (options) => options ? [options] : [];
|
|
38
40
|
function radarQuery(params) {
|
|
39
41
|
return { ...(params.category !== undefined ? { category: params.category } : {}), ...(params.limit !== undefined ? { limit: params.limit } : {}) };
|
|
40
42
|
}
|
|
43
|
+
/** Forward only the media contract fields; nothing else from the app's own media rows. */
|
|
44
|
+
const nativeMediaInput = (media) => media.map(item => ({ url: item.url,
|
|
45
|
+
...(item.alt !== undefined ? { alt: item.alt } : {}), ...(item.width !== undefined ? { width: item.width } : {}),
|
|
46
|
+
...(item.height !== undefined ? { height: item.height } : {}), ...(item.mimeType !== undefined ? { mimeType: item.mimeType } : {}) }));
|
|
41
47
|
export class ProtocolsResource {
|
|
42
48
|
client;
|
|
43
49
|
subscriptionListeners = new Set();
|
|
@@ -126,21 +132,25 @@ export class ProtocolsResource {
|
|
|
126
132
|
return this.client.get('/protocols/social/conversation', { protocol: params.protocol, post: params.post, readRef: params.readRef });
|
|
127
133
|
}
|
|
128
134
|
/** Current provider-ranked topics or posts, with explicit source and coverage. Does not ingest content. */
|
|
129
|
-
getPublicTrends(params) {
|
|
135
|
+
getPublicTrends(params, options) {
|
|
130
136
|
return this.client.get('/protocols/social/trends', { protocol: params.protocol, limit: params.limit, cursor: params.cursor,
|
|
131
137
|
...(params.includePreviews !== undefined ? { includePreviews: params.includePreviews } : {}),
|
|
132
138
|
...(params.category !== undefined ? { category: params.category } : {}),
|
|
133
|
-
...(params.discovery !== undefined ? { discovery: params.discovery } : {}) });
|
|
139
|
+
...(params.discovery !== undefined ? { discovery: params.discovery } : {}) }, ...readOptions(options));
|
|
134
140
|
}
|
|
135
141
|
/** Open a topic currently listed by the provider. Use its nativeId as topic and preserve the opaque cursor. */
|
|
136
|
-
getPublicTrendFeed(params) {
|
|
142
|
+
getPublicTrendFeed(params, options) {
|
|
137
143
|
return this.client.get('/protocols/social/trend-feed', { protocol: params.protocol, topic: params.topic, limit: params.limit, cursor: params.cursor,
|
|
138
144
|
...(params.category !== undefined ? { category: params.category } : {}),
|
|
139
|
-
...(params.discovery !== undefined ? { discovery: params.discovery } : {}) });
|
|
145
|
+
...(params.discovery !== undefined ? { discovery: params.discovery } : {}) }, ...readOptions(options));
|
|
140
146
|
}
|
|
141
147
|
/** The app's topic radar under its own key: the same snapshot the keyless route serves. Never triggers a build. */
|
|
142
|
-
getPublicRadar(params = {}) {
|
|
143
|
-
return this.client.get('/protocols/social/radar', radarQuery(params));
|
|
148
|
+
getPublicRadar(params = {}, options) {
|
|
149
|
+
return this.client.get('/protocols/social/radar', radarQuery(params), ...readOptions(options));
|
|
150
|
+
}
|
|
151
|
+
/** One radar story's social member posts, so its card can show the posts it counts. Minds members are read with `posts.get`. */
|
|
152
|
+
getPublicRadarPosts(params, options) {
|
|
153
|
+
return this.client.get('/protocols/social/radar/posts', { cluster: params.cluster }, ...readOptions(options));
|
|
144
154
|
}
|
|
145
155
|
/** Provider-ranked public accounts with explicit provenance; served from the app's index when a fresh snapshot exists. */
|
|
146
156
|
getPublicPopularProfiles(params) {
|
|
@@ -212,11 +222,12 @@ export class ProtocolsResource {
|
|
|
212
222
|
hostedIdentityPath(id);
|
|
213
223
|
return this.client.get(`/protocols/social/reposts/${encodeURIComponent(id)}`);
|
|
214
224
|
}
|
|
215
|
-
/** Prepare exact public text for selected connected Bluesky/ActivityPub accounts. */
|
|
225
|
+
/** Prepare exact public text and reviewed images for selected connected Bluesky/ActivityPub accounts. */
|
|
216
226
|
prepareNativePost(input) {
|
|
217
227
|
return this.client.post('/protocols/social/posts/prepare', { clientMutationId: input.clientMutationId,
|
|
218
228
|
text: input.text, accountIds: input.accountIds,
|
|
219
|
-
...(input.quote ? { quote: { protocol: input.quote.protocol, nativeId: input.quote.nativeId } } : {})
|
|
229
|
+
...(input.quote ? { quote: { protocol: input.quote.protocol, nativeId: input.quote.nativeId } } : {}),
|
|
230
|
+
...(input.media !== undefined ? { media: nativeMediaInput(input.media) } : {}) }, { retry: false });
|
|
220
231
|
}
|
|
221
232
|
/** Explicitly approve selected deliveries; never auto-retry an uncertain write. */
|
|
222
233
|
submitNativePost(id, input) {
|
|
@@ -248,7 +259,8 @@ export class ProtocolsResource {
|
|
|
248
259
|
parent: { protocol: input.parent.protocol, nativeId: input.parent.nativeId,
|
|
249
260
|
...(input.parent.readRef !== undefined ? { readRef: input.parent.readRef } : {}) }, accountIds: input.accountIds,
|
|
250
261
|
...(input.linkedPostAccountIds !== undefined ? { linkedPostAccountIds: input.linkedPostAccountIds } : {}),
|
|
251
|
-
...(input.recoverFrom !== undefined ? { recoverFrom: { batchId: input.recoverFrom.batchId, deliveryIds: input.recoverFrom.deliveryIds } } : {})
|
|
262
|
+
...(input.recoverFrom !== undefined ? { recoverFrom: { batchId: input.recoverFrom.batchId, deliveryIds: input.recoverFrom.deliveryIds } } : {}),
|
|
263
|
+
...(input.media !== undefined ? { media: nativeMediaInput(input.media) } : {}) }, { retry: false });
|
|
252
264
|
}
|
|
253
265
|
/** Send only explicitly approved delivery IDs from this preview. Repeated submissions reconcile; they do not replay a write. */
|
|
254
266
|
submitNativeReply(id, input) {
|
package/llm.md
CHANGED
|
@@ -478,11 +478,12 @@ Methods: `conversations`, `communityConversation`, `conversation`, `messages`, `
|
|
|
478
478
|
|
|
479
479
|
### r.e2ee — End-to-end encryption transport (flag-gated)
|
|
480
480
|
|
|
481
|
-
Relays opaque MLS envelopes, public device credentials and one-time key packages for the shared E2EE core; the server never sees keys or plaintext. Requires the `e2eeV2` network feature flag
|
|
481
|
+
Relays opaque MLS envelopes, public device credentials and one-time key packages for the shared E2EE core; the server never sees keys or plaintext. Requires the `e2eeV2` network feature flag (otherwise `feature_disabled`) and a key acting as the account owner (agent-scoped keys get `agent_key_not_allowed`). The realm is the network id. Only a direct conversation between two people with an accepted request can start encryption (`conversation_not_eligible` otherwise); blocks apply (`blocked`); commits must send `expected_commit_seq`.
|
|
482
482
|
|
|
483
483
|
```ts
|
|
484
|
-
const { data: device } = await r.e2ee.registerDevice({ device_name: 'phone-1', realm:
|
|
485
|
-
await r.e2ee.publishKeyPackages({ device_id: device.id, packages: [keyPackageBase64] });
|
|
484
|
+
const { data: device } = await r.e2ee.registerDevice({ device_name: 'phone-1', realm: networkId, signing_key, credential });
|
|
485
|
+
await r.e2ee.publishKeyPackages({ device_id: device.id, packages: [keyPackageBase64] }); // → { published, available }
|
|
486
|
+
const { data: stock } = await r.e2ee.keyPackageStatus({ device_id: device.id }); // { available, expiringSoon }; refill when low
|
|
486
487
|
const { data: claimed } = await r.e2ee.claimKeyPackage({ device_id: device.id, target_device_id: peerDeviceId });
|
|
487
488
|
await r.e2ee.appendEvent(conversationId, { device_id: device.id, envelope }); // idempotent per event_id
|
|
488
489
|
const { data: page } = await r.e2ee.events(conversationId, { device_id: device.id, after_seq: 0 });
|
|
@@ -634,7 +635,7 @@ if (subscription.active) {
|
|
|
634
635
|
- `r.organizationSettings` / `r.organizationSecurity` — Org configuration
|
|
635
636
|
- Native follows: `nativeFollowState({ accountId, protocol, nativeId })`, `prepareNativeFollow({ clientMutationId, accountId, protocol, nativeId, action: "follow" | "unfollow" })`, `submitNativeFollow(id, { approve: true, event? })`, `nativeFollowStatus(id)`. Show the acting identity and native following/requested/unfollowed state separately from the Minds feed subscription. Nostr must sign the complete review event without dropping other contacts; stale lists require a fresh review. Keep mutation IDs and reconcile ambiguous outcomes without replay. Requires current follow scopes and API `NATIVE_SOCIAL_FOLLOW_PROJECTS` plus source controls.
|
|
636
637
|
- Native reposts: `nativeRepostState({ accountId, protocol, nativeId })`, `prepareNativeRepost({ clientMutationId, accountId, protocol, nativeId, action: "repost" | "unrepost" })`, `submitNativeRepost(id, { approve: true, event? })`, `nativeRepostStatus(id)`. Requires `NATIVE_SOCIAL_REPOST_PROJECTS`, account capability and explicit approval; Nostr signs the exact review (kind 6 or own kind 5). Unknown attempts only reconcile. Bluesky connections need the new repost scope; ActivityPub uses write:statuses. Native likes: `nativeLikeState({ accountId, protocol, nativeId })`, `prepareNativeLike({ clientMutationId, accountId, protocol, nativeId, action: "like" | "unlike" })`, `submitNativeLike(id, { approve: true, event? })`, and `nativeLikeStatus(id)`. Display the selected account, source and action before approval. Only Nostr supplies `event`: sign the exact public `signing.event` in the review; never transmit a private key. Preserve mutation IDs on ambiguous responses and refresh unknown results instead of resending. Read capabilities before offering actions; old reply-only connections need reconnecting for likes. Nostr results describe the connected relay, not every public relay. The app-specific `NATIVE_SOCIAL_LIKE_PROJECTS` switch and source controls must be enabled on the API.
|
|
637
|
-
- 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, 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
|
|
638
|
+
- 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.
|
|
638
639
|
- `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. Keyless post links: `const visitor = new Recursiv({ anonymous: true }); 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`; private app subscriptions: `listSubscriptions`, `subscribeToProfile`, `unsubscribeFromProfile`, `onSubscriptionChange`. 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. 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`.
|
|
639
640
|
- 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.
|
|
640
641
|
- Anonymous discovery: `const visitor = new Recursiv({ anonymous: true }); 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.
|
|
@@ -822,7 +823,7 @@ publication; history stores no signature, signing key or intent. Keep the acting
|
|
|
822
823
|
user/app-bound result IDs and discard UI state after an account switch. The last
|
|
823
824
|
read-back observation is separate from retained publication/deletion evidence.
|
|
824
825
|
|
|
825
|
-
Standalone public text posts: `prepareNativePost({ clientMutationId, text, accountIds })` → review → `submitNativePost(id, { approve: true, deliveryIds })`. Use `nativePostStatus(id)` / `nativePostHistory({limit,cursor})` for private durable outcomes and `prepareNativePostRemoval(id,{deliveryIds})` → `removeNativePost(id,{approve:true,removals:[{deliveryId,removalId}]})` for reviewed own-post removal. Check `publish_text
|
|
826
|
+
Standalone public text posts: `prepareNativePost({ clientMutationId, text, accountIds })` → review → `submitNativePost(id, { approve: true, deliveryIds })`. Use `nativePostStatus(id)` / `nativePostHistory({limit,cursor})` for private durable outcomes and `prepareNativePostRemoval(id,{deliveryIds})` → `removeNativePost(id,{approve:true,removals:[{deliveryId,removalId}]})` for reviewed own-post removal. Check `publish_text`; no reply parent or audience override. Attach already-uploaded Minds images with `media: [{ url, alt?, width?, height?, mimeType? }]` (at most four JPEG/PNG/WebP/GIF, Minds-hosted HTTPS URLs only, a Mastodon GIF must be the only attachment) on accounts that also advertise `publish_media` — grants made before image scopes existed keep working for text and must be reconnected first. Always pass the member's own `alt`; an omitted one is sent empty, never invented. The receipt reports `media: { count, alt_missing }`. Preserve mutation IDs, never create a new send to recover uncertainty.
|
|
826
827
|
Bluesky quote: accounts must advertise `quote`; call `prepareNativePost({clientMutationId,text,accountIds,quote:{protocol:'bluesky',nativeId:sourceAtUri}})`, review each returned quote/account/comment, then approve via the same submit method. The server binds the source CID and rechecks permissions before sending. No cross-network quote conversion. Public reads expose one filtered `post.quote` (`available` with `post`, or `unavailable` without source details). Available quotes may include `embeddedReferences`: exact standalone source-reference lines in the unchanged outer `text`. Omit them only when the quoted original is actually visible; retain them for unavailable or locally hidden quotes. Nostr references are decoded and matched by the server, without following relay hints.
|
|
827
828
|
|
|
828
829
|
Public post search (API/SDK 0.7.35): `const { data: page } = await r.protocols.searchPublicPosts({ protocol: 'nostr', query: 'open social', limit: 10 })`. Bluesky/ActivityPub require a selected `accountId` from `nativeSocialAccounts()` with `post_search`; older Bluesky grants must reconnect with consent. Open their results with `getConnectedSocialConversation({ protocol, accountId: page.readerAccountId, post: item.nativeId, readRef: item.readRef })` and pass the same accountId to `reportSource` for post reports. Nostr uses `getPublicConversation`. Pages report provider/observedAt and bounded coverage; no invented exhaustive or global search. Keep cursors paired with the exact owner, query, account, limit and connection revision. No writes, credentials or new ingestion are part of search.
|