@recursiv/sdk 0.7.58 → 0.7.59

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.
@@ -20,7 +20,54 @@ export interface E2eeDevice {
20
20
  * before 0.7.57.
21
21
  */
22
22
  lastSeenAt?: string | null;
23
+ /**
24
+ * Set on a root that a recorded reset of encrypted chat retired
25
+ * (`resetAccount`): when the old main device was revoked. Null on every
26
+ * other device, and on a root that stopped being the root without a
27
+ * recorded reset: treat that change as unexplained (compare security codes),
28
+ * never as a reset. Absent on relays without resets.
29
+ */
30
+ rootRetiredAt?: string | null;
31
+ }
32
+ /** `requestResetCode`'s answer: when the emailed code expires, and the masked address it went to. */
33
+ export interface E2eeResetCodeSent {
34
+ expires_at: string;
35
+ /** Masked, e.g. `b***@minds.com`: show it so the person knows which inbox to open. */
36
+ sent_to: string;
37
+ }
38
+ /** What `resetAccount` did. The account's encrypted chat starts again from `device`. */
39
+ export interface E2eeAccountReset {
40
+ reset: true;
41
+ resetAt: string;
42
+ /** The main device the reset retired; null when the account had none. */
43
+ retiredRoot: {
44
+ deviceId: string;
45
+ signingKey: string;
46
+ } | null;
47
+ /** Every device the reset revoked. */
48
+ revokedDeviceIds: string[];
49
+ /** This device, registered as the account's main device (root). */
50
+ device: E2eeDevice;
51
+ /**
52
+ * How many other sessions and API keys of the account were signed out, and
53
+ * how many AI clients connected over MCP OAuth were disconnected.
54
+ */
55
+ signedOut: {
56
+ sessions: number;
57
+ keys: number;
58
+ connectedApps: number;
59
+ };
23
60
  }
61
+ /**
62
+ * Codes a reset, or a call a reset may be needed for, can be refused with:
63
+ * - `confirmation_failed` (403): the code is wrong, expired, spent or dead after five wrong tries.
64
+ * - `code_limit` (409, never 429): too many codes asked for; `details.retryAfterSeconds`.
65
+ * - `no_email_on_file` (400), `email_delivery_unavailable` (503): no code could be sent.
66
+ * - `reset_required` (409): from `createLink` when the account's main device is gone with
67
+ * nothing left to approve a new one, and from `revokeDevice` on the main device. Only a reset goes on.
68
+ * - `conflict` (409): the device name or key was used before; start the device again with fresh ones.
69
+ */
70
+ export type E2eeResetErrorCode = 'confirmation_failed' | 'code_limit' | 'no_email_on_file' | 'email_delivery_unavailable' | 'reset_required' | 'conflict';
24
71
  /** Opaque `recursiv-mls-v2-draft1` envelope with base64 MLS bytes. */
25
72
  export interface E2eeEnvelope {
26
73
  profile: 'recursiv-mls-v2-draft1';
@@ -111,6 +158,12 @@ export interface E2eeStatus {
111
158
  * makes, never switch to it on their behalf.
112
159
  */
113
160
  links?: boolean;
161
+ /**
162
+ * True when this network offers this account resetting encrypted chat
163
+ * (`requestResetCode`, `resetAccount`); never for an account the reset would
164
+ * refuse, such as a platform owner's. False or absent: do not offer it.
165
+ */
166
+ accountReset?: boolean;
114
167
  }
115
168
  /** A device link's state. An open link past `expiresAt` reads as `cancelled` with `cancelledBy: 'expired'`. */
116
169
  export type E2eeLinkState = 'open' | 'approved' | 'completed' | 'cancelled';
@@ -227,11 +280,41 @@ export declare class E2eeResource {
227
280
  * `details: E2eeHasDependentsDetails`, so the app can say which devices
228
281
  * would be signed out. Call again with `{ cascade: true }` to revoke it and
229
282
  * every device chained through it (relays from 0.7.58; older relays ignore
230
- * the body and revoke only the device).
283
+ * the body and revoke only the device). Where this account is offered
284
+ * resetting encrypted chat (`status().accountReset`), the main device is never
285
+ * revoked this way: 409 `reset_required`; replace it with `resetAccount`.
231
286
  */
232
287
  revokeDevice(deviceId: string, input?: {
233
288
  cascade?: boolean;
234
289
  }): Promise<E2eeRevokeResponse>;
290
+ /**
291
+ * Email this account an eight-digit code to reset encrypted chat, and warn
292
+ * its owner that one was asked for. Only where `status().accountReset` is
293
+ * true. The account's code budget is shared with other confirmation codes:
294
+ * over it, a `ConflictError` with code `code_limit` and
295
+ * `details.retryAfterSeconds` (never a 429). Not retried.
296
+ */
297
+ requestResetCode(): Promise<SingleResponse<E2eeResetCodeSent>>;
298
+ /**
299
+ * Reset encrypted chat with the emailed code, and make this device the
300
+ * account's main device: pass it as `registerDevice` takes it, with a fresh
301
+ * name and key and its own self-signed credential. Every other device of
302
+ * the account is removed from encrypted chat, and the account is signed out
303
+ * of every other session and API key on this network (`keep_session_token`
304
+ * keeps a Better Auth session this device holds). Contacts see that the
305
+ * security code changed. A wrong, expired or spent code: `confirmation_failed`
306
+ * (403), nothing changed. Sending the same request again after it succeeded
307
+ * (from the same sign-in) answers the same result, so a retry after a lost
308
+ * answer is safe.
309
+ */
310
+ resetAccount(input: {
311
+ email_code: string;
312
+ device_name: string;
313
+ realm: string;
314
+ signing_key: string;
315
+ credential: Record<string, unknown>;
316
+ keep_session_token?: string;
317
+ }): Promise<SingleResponse<E2eeAccountReset>>;
235
318
  /** Publish one-time key packages for one of your devices; `available` is how many it now has. */
236
319
  publishKeyPackages(input: {
237
320
  device_id: string;
@@ -328,7 +411,9 @@ export declare class E2eeResource {
328
411
  /**
329
412
  * Start linking this new, not yet registered device: `POST /e2ee/links`.
330
413
  * Answers the open link (180 s on the relay's clock). A new link cancels
331
- * this sign-in's previous open link. `label` is 1-48 letters, numbers,
414
+ * this sign-in's previous open link. `reset_required` (409): the account's
415
+ * main device is gone and nothing is left to approve this one; only
416
+ * `resetAccount` goes on. `label` is 1-48 letters, numbers,
332
417
  * spaces and `. , ' ( ) + -`; reserved words are refused (`invalid_label`).
333
418
  * Not retried: a 5xx is answered at once, and the caller decides.
334
419
  */
@@ -35,11 +35,38 @@ export class E2eeResource {
35
35
  * `details: E2eeHasDependentsDetails`, so the app can say which devices
36
36
  * would be signed out. Call again with `{ cascade: true }` to revoke it and
37
37
  * every device chained through it (relays from 0.7.58; older relays ignore
38
- * the body and revoke only the device).
38
+ * the body and revoke only the device). Where this account is offered
39
+ * resetting encrypted chat (`status().accountReset`), the main device is never
40
+ * revoked this way: 409 `reset_required`; replace it with `resetAccount`.
39
41
  */
40
42
  revokeDevice(deviceId, input) {
41
43
  return this.client.post(`/e2ee/devices/${encodeURIComponent(deviceId)}/revoke`, input ?? {});
42
44
  }
45
+ /**
46
+ * Email this account an eight-digit code to reset encrypted chat, and warn
47
+ * its owner that one was asked for. Only where `status().accountReset` is
48
+ * true. The account's code budget is shared with other confirmation codes:
49
+ * over it, a `ConflictError` with code `code_limit` and
50
+ * `details.retryAfterSeconds` (never a 429). Not retried.
51
+ */
52
+ requestResetCode() {
53
+ return this.client.post('/e2ee/account/reset-code', {}, { retry: false });
54
+ }
55
+ /**
56
+ * Reset encrypted chat with the emailed code, and make this device the
57
+ * account's main device: pass it as `registerDevice` takes it, with a fresh
58
+ * name and key and its own self-signed credential. Every other device of
59
+ * the account is removed from encrypted chat, and the account is signed out
60
+ * of every other session and API key on this network (`keep_session_token`
61
+ * keeps a Better Auth session this device holds). Contacts see that the
62
+ * security code changed. A wrong, expired or spent code: `confirmation_failed`
63
+ * (403), nothing changed. Sending the same request again after it succeeded
64
+ * (from the same sign-in) answers the same result, so a retry after a lost
65
+ * answer is safe.
66
+ */
67
+ resetAccount(input) {
68
+ return this.client.post('/e2ee/account/reset', input, { retry: true });
69
+ }
43
70
  /** Publish one-time key packages for one of your devices; `available` is how many it now has. */
44
71
  publishKeyPackages(input) {
45
72
  return this.client.post('/e2ee/key-packages', input);
@@ -109,7 +136,9 @@ export class E2eeResource {
109
136
  /**
110
137
  * Start linking this new, not yet registered device: `POST /e2ee/links`.
111
138
  * Answers the open link (180 s on the relay's clock). A new link cancels
112
- * this sign-in's previous open link. `label` is 1-48 letters, numbers,
139
+ * this sign-in's previous open link. `reset_required` (409): the account's
140
+ * main device is gone and nothing is left to approve this one; only
141
+ * `resetAccount` goes on. `label` is 1-48 letters, numbers,
113
142
  * spaces and `. , ' ( ) + -`; reserved words are refused (`invalid_label`).
114
143
  * Not retried: a 5xx is answered at once, and the caller decides.
115
144
  */
@@ -112,15 +112,39 @@ export interface SendEmailInput {
112
112
  reply_to?: string;
113
113
  /** File attachments (max 10). Content must be base64-encoded. */
114
114
  attachments?: EmailAttachment[];
115
+ /**
116
+ * Defaults to `'marketing'`, which is held unless the recipient has given
117
+ * marketing consent. Pass `'transactional'` for receipts, invites,
118
+ * confirmations and alerts.
119
+ */
120
+ category?: 'transactional' | 'product' | 'marketing';
121
+ }
122
+ export interface SendEmailMessage {
123
+ id: string | null;
124
+ to: string;
125
+ /** `queued`/`sent`/`retry` are on their way; `skipped`/`suppressed`/`failed`/`duplicate` are not. */
126
+ status: 'queued' | 'sent' | 'retry' | 'failed' | 'suppressed' | 'skipped' | 'duplicate' | (string & {});
127
+ /** Why a held recipient was not sent, e.g. "marketing consent not granted". */
128
+ reason?: string;
115
129
  }
116
130
  export interface SendEmailResult {
117
- id: string;
118
- status: string;
131
+ id: string | null;
132
+ /** `queued`: every recipient; `partial`: some held (see `messages`). A send where all were held throws `EmailNotSentError`. */
133
+ status: 'queued' | 'partial' | 'skipped' | (string & {});
134
+ messages?: SendEmailMessage[];
135
+ /** Present when any recipient was held: says why, in words. */
136
+ hint?: string;
119
137
  }
120
138
  export declare class EmailResource {
121
139
  private client;
122
140
  constructor(client: HttpClient);
123
- /** Send a transactional email (uses org's BYOK Resend key if configured) */
141
+ /**
142
+ * Send an email as the organization (its BYOK Resend key when configured).
143
+ * Set `category: 'transactional'` for receipts, invites and alerts: the
144
+ * default is `'marketing'`, which needs the recipient's consent.
145
+ * Throws `EmailNotSentError` when every recipient was held; check
146
+ * `status === 'partial'` and `messages` for a send that reached only some.
147
+ */
124
148
  send(input: SendEmailInput): Promise<SingleResponse<SendEmailResult>>;
125
149
  /** List all campaigns */
126
150
  listCampaigns(): Promise<SingleResponse<CampaignSummary[]>>;
@@ -1,11 +1,26 @@
1
+ import { EmailNotSentError } from '../errors.js';
1
2
  export class EmailResource {
2
3
  client;
3
4
  constructor(client) {
4
5
  this.client = client;
5
6
  }
6
- /** Send a transactional email (uses org's BYOK Resend key if configured) */
7
- send(input) {
8
- return this.client.post('/email/send', input);
7
+ /**
8
+ * Send an email as the organization (its BYOK Resend key when configured).
9
+ * Set `category: 'transactional'` for receipts, invites and alerts: the
10
+ * default is `'marketing'`, which needs the recipient's consent.
11
+ * Throws `EmailNotSentError` when every recipient was held; check
12
+ * `status === 'partial'` and `messages` for a send that reached only some.
13
+ */
14
+ async send(input) {
15
+ const res = (await this.client.post('/email/send', input));
16
+ if (res?.data?.status === 'skipped') {
17
+ throw new EmailNotSentError({
18
+ message: 'Email not sent: every recipient was held.',
19
+ hint: res.data.hint,
20
+ details: res.data,
21
+ });
22
+ }
23
+ return res;
9
24
  }
10
25
  /** List all campaigns */
11
26
  listCampaigns() {
@@ -37,6 +37,25 @@ export interface DeleteEnvVarParams {
37
37
  /** Omit to remove the variable from every environment. */
38
38
  environment?: EnvVarEnvironment;
39
39
  }
40
+ export interface CreateProjectApiKeyInput {
41
+ project_id: string;
42
+ /** The env var that receives the key. UPPER_SNAKE_CASE, e.g. RECURSIV_API_KEY. */
43
+ key: string;
44
+ /** Defaults to databases:read + databases:write. Never more than the calling key holds; never admin. */
45
+ scopes?: string[];
46
+ /** Defaults to 'both'. */
47
+ environment?: EnvVarEnvironment;
48
+ }
49
+ export interface CreateProjectApiKeyResult {
50
+ key: string;
51
+ environment: EnvVarEnvironment;
52
+ api_key_id: string;
53
+ /** The key's public prefix, for recognizing it in a key list. Not the key. */
54
+ api_key_prefix: string;
55
+ scopes: string[];
56
+ /** How many earlier keys minted into this env var were revoked. */
57
+ revoked_previous: number;
58
+ }
40
59
  export declare class EnvVarsResource {
41
60
  private client;
42
61
  constructor(client: HttpClient);
@@ -47,6 +66,12 @@ export declare class EnvVarsResource {
47
66
  * changes what runs in production.
48
67
  */
49
68
  set(input: SetEnvVarInput): Promise<SingleResponse<Omit<EnvVar, 'value' | 'source' | 'updated_at'>>>;
69
+ /**
70
+ * Mint a project-bound API key straight into an env var. The key value goes
71
+ * to the encrypted variable and is never returned. Running it again rotates:
72
+ * the previous key in that variable is revoked. Requires project admin.
73
+ */
74
+ createProjectApiKey(input: CreateProjectApiKeyInput): Promise<SingleResponse<CreateProjectApiKeyResult>>;
50
75
  delete(params: DeleteEnvVarParams): Promise<SingleResponse<{
51
76
  key: string;
52
77
  deleted: boolean;
@@ -17,6 +17,14 @@ export class EnvVarsResource {
17
17
  async set(input) {
18
18
  return this.client.put('/env-vars', input);
19
19
  }
20
+ /**
21
+ * Mint a project-bound API key straight into an env var. The key value goes
22
+ * to the encrypted variable and is never returned. Running it again rotates:
23
+ * the previous key in that variable is revoked. Requires project admin.
24
+ */
25
+ async createProjectApiKey(input) {
26
+ return this.client.post('/env-vars/api-key', input);
27
+ }
20
28
  async delete(params) {
21
29
  const query = new URLSearchParams({ project_id: params.project_id, key: params.key });
22
30
  if (params.environment)
@@ -63,6 +63,18 @@ export interface ApplyModerationActionInput {
63
63
  actor_type?: 'human' | 'ai' | 'system';
64
64
  source_report_id?: string;
65
65
  }
66
+ /** What a granted agent may do on the report queue: shadow reads it; act also resolves and dismisses. */
67
+ export type AgentModerationMode = 'shadow' | 'act';
68
+ /** A network admin's grant letting one AI agent work the report queue. */
69
+ export interface AgentModerationGrant {
70
+ id: string;
71
+ network_id: string | null;
72
+ agent_id: string;
73
+ mode: AgentModerationMode;
74
+ granted_by_id: string | null;
75
+ created_at: string;
76
+ revoked_at: string | null;
77
+ }
66
78
  export declare class ModerationResource {
67
79
  private client;
68
80
  constructor(client: HttpClient);
@@ -101,4 +113,14 @@ export declare class ModerationResource {
101
113
  action_id: string;
102
114
  outcome: 'upheld' | 'overturned';
103
115
  }>>;
116
+ /** Admin: the network's active agent moderation grants. */
117
+ listAgentGrants(): Promise<SingleResponse<AgentModerationGrant[]>>;
118
+ /**
119
+ * Admin: let an AI agent in the caller's network work the report queue. An
120
+ * active grant is replaced. The agent's own key also needs the
121
+ * `moderation:agent` scope.
122
+ */
123
+ grantAgent(agentId: string, mode: AgentModerationMode): Promise<SingleResponse<AgentModerationGrant>>;
124
+ /** Admin: revoke an agent's active moderation grant. */
125
+ revokeAgent(agentId: string): Promise<SingleResponse<AgentModerationGrant>>;
104
126
  }
@@ -34,4 +34,20 @@ export class ModerationResource {
34
34
  resolveAppeal(appealId, input) {
35
35
  return this.client.post(`/moderation/appeals/${encodeURIComponent(appealId)}/resolve`, input);
36
36
  }
37
+ /** Admin: the network's active agent moderation grants. */
38
+ listAgentGrants() {
39
+ return this.client.get('/moderation/agent-grants');
40
+ }
41
+ /**
42
+ * Admin: let an AI agent in the caller's network work the report queue. An
43
+ * active grant is replaced. The agent's own key also needs the
44
+ * `moderation:agent` scope.
45
+ */
46
+ grantAgent(agentId, mode) {
47
+ return this.client.post('/moderation/agent-grants', { agent_id: agentId, mode });
48
+ }
49
+ /** Admin: revoke an agent's active moderation grant. */
50
+ revokeAgent(agentId) {
51
+ return this.client.delete(`/moderation/agent-grants/${encodeURIComponent(agentId)}`);
52
+ }
37
53
  }
@@ -17,7 +17,7 @@ export interface CategoryOrderMeta {
17
17
  export declare class PostsResource {
18
18
  private client;
19
19
  constructor(client: HttpClient);
20
- /** List posts with optional filters */
20
+ /** List posts with optional filters. `retry: false` asks once and is never sent (see ListPostsParams.retry). */
21
21
  list(params?: ListPostsParams): Promise<PaginatedResponse<Post>>;
22
22
  /**
23
23
  * Broad categories currently enabled for public posts in this app, in the order this member should see
@@ -3,9 +3,13 @@ export class PostsResource {
3
3
  constructor(client) {
4
4
  this.client = client;
5
5
  }
6
- /** List posts with optional filters */
6
+ /** List posts with optional filters. `retry: false` asks once and is never sent (see ListPostsParams.retry). */
7
7
  list(params) {
8
- return this.client.get('/posts', params);
8
+ // Without `retry`, the same request older SDKs make.
9
+ if (!params || !('retry' in params))
10
+ return this.client.get('/posts', params);
11
+ const { retry, ...query } = params;
12
+ return retry === false ? this.client.get('/posts', query, { retry: false }) : this.client.get('/posts', query);
9
13
  }
10
14
  /**
11
15
  * Broad categories currently enabled for public posts in this app, in the order this member should see
@@ -1,5 +1,5 @@
1
1
  import type { HttpClient } from '../client.js';
2
- import type { DiscoveryLayout, PaginatedResponse, PaginationParams, SingleResponse } from '../types.js';
2
+ import type { CompassFilterMeta, DiscoveryLayout, PaginatedResponse, PaginationParams, SingleResponse } from '../types.js';
3
3
  /** Canonical protocol operation names returned by the adapter registry. */
4
4
  export declare const PROTOCOL_CAPABILITY_OPERATIONS: readonly ["discover", "stream", "backfill", "search", "resolveIdentity", "fetchProfile", "fetchThread", "publish", "reply", "react", "repost", "follow", "delete", "health", "checkpoint"];
5
5
  export declare const PROTOCOL_CAPABILITY_STATUSES: readonly ["supported", "planned", "unsupported"];
@@ -906,7 +906,8 @@ export interface SocialNetwork {
906
906
  url: string;
907
907
  };
908
908
  coverage: string;
909
- capabilities: Array<'profile_search' | 'profile_read' | 'original_author_posts' | 'minds_subscription' | 'following_feed' | 'conversation_read' | 'public_trends' | 'trend_feed' | 'popular_accounts' | 'post_search'>;
909
+ /** `compass_filter`: the Politics filter is served to this viewer (an app offers it only then); listed on every network. */
910
+ capabilities: Array<'profile_search' | 'profile_read' | 'original_author_posts' | 'minds_subscription' | 'following_feed' | 'conversation_read' | 'public_trends' | 'trend_feed' | 'popular_accounts' | 'post_search' | 'compass_filter'>;
910
911
  profileSearchPagination: boolean;
911
912
  postSearchAccountRequired?: boolean;
912
913
  /** Categories available from the current app configuration. */
@@ -930,8 +931,22 @@ export interface SocialProfile {
930
931
  postsCount: number | null;
931
932
  /** Nostr only: the NIP-05 name its domain has confirmed for this key ("lyn@primal.net"); `handle` stays the key. */
932
933
  nip05?: string;
934
+ /** A bridge (Bridgy Fed, Mostr) made this account from one on another network (SocialBridge). Present only on a bridged account. */
935
+ bridge?: SocialBridge;
933
936
  subscription: SocialSubscription | null;
934
937
  }
938
+ /**
939
+ * A post or account a bridge copied from another network: Bridgy Fed (`*.brid.gy`), Mostr (`mostr.pub`), or a Nostr
940
+ * event's NIP-48 `proxy` tag naming no bridge the server knows (`proxy`). `originId` names the original, normalised (a
941
+ * Bluesky at-uri or DID, an ActivityPub object URL or `user@host`, a Nostr event id or key, a web URL), when the bridge
942
+ * says. Where the server reads both, feeds, search and the radar already fold a copy into its original; an app that
943
+ * merges several networks' pages itself can fold the same way: a copy whose original is shown is the same post.
944
+ */
945
+ export interface SocialBridge {
946
+ via: 'bridgy_fed' | 'mostr' | 'proxy';
947
+ protocol: 'bluesky' | 'activitypub' | 'nostr' | 'web';
948
+ originId?: string;
949
+ }
935
950
  export interface SocialSubscription {
936
951
  id: string;
937
952
  protocol: string;
@@ -1028,6 +1043,8 @@ export interface SocialPost {
1028
1043
  * to signed-out visitors.
1029
1044
  */
1030
1045
  authorOptOut?: true;
1046
+ /** A bridge (Bridgy Fed, Mostr, a NIP-48 proxy tag) made this post from one on another network (SocialBridge). Present only on a copy read live. */
1047
+ bridge?: SocialBridge;
1031
1048
  }
1032
1049
  export interface SocialPostSearch {
1033
1050
  items: SocialPost[];
@@ -1290,6 +1307,182 @@ export interface SocialRadarStory {
1290
1307
  */
1291
1308
  editionStory?: string;
1292
1309
  }
1310
+ /**
1311
+ * The Viral read (`getPublicViral`, `getSharedViral`): what is moving fastest on Bluesky, the Fediverse, Nostr and
1312
+ * Minds right now, each measured against its own network's pace, from a snapshot the server's rank tick rebuilds about
1313
+ * every five minutes. A read never reaches a provider or the model. These types mirror the server's
1314
+ * ViralSnapshotContract; a contract test compares them key by key.
1315
+ */
1316
+ export type ViralNetwork = 'all' | 'minds' | 'bluesky' | 'activitypub' | 'nostr';
1317
+ export type ViralProtocol = 'bluesky' | 'activitypub' | 'nostr' | 'minds';
1318
+ /** The networks whose posts the read serves as a `SocialPost`. */
1319
+ export type ViralOpenProtocol = 'bluesky' | 'activitypub' | 'nostr';
1320
+ /**
1321
+ * `viral`: a measured rate well above its network's baseline (z of 1 or more) with two or more kinds of engagement;
1322
+ * `rising`: a measured rate speeding up (accel of 0.7 or more) on a post under six hours old; `hot`: everything else,
1323
+ * including every post without a measured rate.
1324
+ */
1325
+ export type ViralLabel = 'viral' | 'rising' | 'hot';
1326
+ /**
1327
+ * `ok`: a build served. `building`: no build yet. `screening`: served, but fewer than 30% of the top 200 posts have
1328
+ * a safety verdict yet. `off`: the read is not enabled for this app (or, on `getSharedViral` only, the API predates it):
1329
+ * no items, and nothing to retry until it is turned on.
1330
+ */
1331
+ export type ViralReadStatus = 'ok' | 'building' | 'screening' | 'off';
1332
+ /** Every parameter is optional. The server rejects any other parameter with a 400, so only these travel. */
1333
+ export interface ViralReadParams {
1334
+ /** Default `all`: every network, with a per-network quota so no one network fills the page. */
1335
+ network?: ViralNetwork;
1336
+ /** A Discover category id: only posts judged into it. */
1337
+ category?: string;
1338
+ /**
1339
+ * `video`: only posts whose own media includes a video. A member's read serves every such video. A visitor's read
1340
+ * (`getSharedViral`) still serves a post's media only where the server cleared it, so some items can arrive with
1341
+ * the video withheld (the card's `media` empty): keep `items.filter(viralItemHasVideo)` for a video-only player.
1342
+ */
1343
+ media?: 'video';
1344
+ /** Page size, default 20. */
1345
+ limit?: 5 | 10 | 20 | 40;
1346
+ /** The previous page's `cursor`. A cursor names its build; one whose build expired (30 minutes) ends the list. */
1347
+ cursor?: string;
1348
+ /** A Politics choice. A visitor (`getSharedViral`) may send only `all` or `hide`; anything else is a 400. */
1349
+ compass?: string;
1350
+ }
1351
+ /** How fast a post is gathering reactions, reposts and replies (per hour), against its network's baseline. */
1352
+ export interface ViralItemVelocity {
1353
+ perHour1h: number;
1354
+ perHour6h: number;
1355
+ /** The 1-hour rate against its network's trailing baseline, from -2 to 4 (1 or more is well above). */
1356
+ z: number;
1357
+ /** ln((1 + perHour1h) / (1 + perHour6h)), from -1 to 2: above 0 is speeding up. */
1358
+ accel: number;
1359
+ /** Readings behind the rate; null for a Minds post. */
1360
+ samples: number | null;
1361
+ spanHours: number;
1362
+ /** The rate comes from two or more readings over time (a Minds post's always does). An unmeasured post is never viral or rising. */
1363
+ measured: boolean;
1364
+ /** Who counted: Bluesky's AppView, one Fediverse server, a third party's report (Primal, for Nostr), or Minds itself. */
1365
+ countsOrigin: 'appview' | 'instance_local' | 'provider_reported' | 'minds';
1366
+ /** Minutes since the counts were last read, when known. */
1367
+ countsAgeMin?: number;
1368
+ /** Present when a third party reported the counts (`Primal`): show them as reported by it, never as verified. */
1369
+ countsReportedBy?: string;
1370
+ }
1371
+ /** Another post in the same spread group: the same link posted on another network. */
1372
+ export interface ViralRef {
1373
+ protocol: ViralProtocol;
1374
+ nativeId: string;
1375
+ }
1376
+ export interface ViralItemSpread {
1377
+ /** The networks sharing the post's link (its own alone when no other network carries it). */
1378
+ protocols: ViralProtocol[];
1379
+ /** Fediverse: how many servers' trends lists carried the post. */
1380
+ instances?: number;
1381
+ clusterId: null;
1382
+ alsoOn: ViralRef[];
1383
+ }
1384
+ /** A Minds post's card as the read serves it; open it at /post/:id. A visitor's carries media only where the server cleared it. */
1385
+ export interface ViralMindsCard {
1386
+ id: string;
1387
+ title: string | null;
1388
+ text: string;
1389
+ createdAt: string;
1390
+ author: {
1391
+ name: string | null;
1392
+ username: string | null;
1393
+ } | null;
1394
+ media: Array<{
1395
+ type: string;
1396
+ url: string;
1397
+ width: number | null;
1398
+ height: number | null;
1399
+ thumbnailUrl: string | null;
1400
+ hlsUrl: string | null;
1401
+ }>;
1402
+ likeCount: number;
1403
+ repostCount: number;
1404
+ replyCount: number;
1405
+ }
1406
+ /** What every ranked post carries, whichever network it comes from. */
1407
+ export interface ViralItemFields {
1408
+ /** Stable across builds: use it to drop a post already shown. */
1409
+ key: string;
1410
+ label: ViralLabel;
1411
+ score: number;
1412
+ /** 1-based position in this read's ordering, across pages. */
1413
+ rank: number;
1414
+ category: string | null;
1415
+ velocity: ViralItemVelocity;
1416
+ spread: ViralItemSpread;
1417
+ }
1418
+ /** A ranked Minds post; open it at /post/:id. */
1419
+ export interface ViralMindsItem extends ViralItemFields {
1420
+ protocol: 'minds';
1421
+ post: ViralMindsCard;
1422
+ }
1423
+ /** A ranked Bluesky, Fediverse or Nostr post. */
1424
+ export interface ViralSocialItem extends ViralItemFields {
1425
+ protocol: ViralOpenProtocol;
1426
+ post: SocialPost;
1427
+ }
1428
+ /**
1429
+ * One ranked post: check `protocol` and TypeScript narrows `post` (a `ViralMindsCard` for `minds`, a `SocialPost`
1430
+ * otherwise). No item carries a compass value, an author id, a safety flag or its media kind. A visitor's card keeps
1431
+ * its own images and video only where the server cleared them; a withheld one has empty `media` (a `SocialPost` also
1432
+ * has `hasAdditionalContent: true`). Even under `media: 'video'` a visitor's item can arrive without its video, so
1433
+ * test each one with `viralItemHasVideo` before a video player.
1434
+ */
1435
+ export type ViralItem = ViralMindsItem | ViralSocialItem;
1436
+ /**
1437
+ * Whether the item's own card carries a playable video. Use it on a `media: 'video'` read before a Watch-style
1438
+ * player: a visitor's read can serve an item whose video the server withheld (its card's `media` is then empty).
1439
+ * A video only in a quoted post does not count, as the server never counts a quote's media.
1440
+ */
1441
+ export declare function viralItemHasVideo(item: ViralItem): boolean;
1442
+ /** A network's trailing pace that velocity is measured against. */
1443
+ export interface ViralBaseline {
1444
+ median: number;
1445
+ p90: number;
1446
+ n: number;
1447
+ source: 'trailing' | 'tick' | 'prior';
1448
+ }
1449
+ export interface ViralProvider {
1450
+ name: string;
1451
+ ok: boolean;
1452
+ lastOkAt: string | null;
1453
+ observedAt: string;
1454
+ }
1455
+ export interface ViralReadMeta {
1456
+ network: ViralNetwork;
1457
+ category_filter: {
1458
+ selected: string;
1459
+ } | null;
1460
+ media_filter: 'video' | null;
1461
+ /** Null unless the read named a filtering `compass`: what Hide politics left out (sides and Mix are not applied here). */
1462
+ compass_filter: CompassFilterMeta | null;
1463
+ safety_filter: {
1464
+ withheld: number;
1465
+ pending: number;
1466
+ };
1467
+ }
1468
+ export interface ViralReadResponse {
1469
+ items: ViralItem[];
1470
+ /** Pass as `cursor` for the next page; null at the end of this build's list. */
1471
+ cursor: string | null;
1472
+ builtAt: string | null;
1473
+ status: ViralReadStatus;
1474
+ window: {
1475
+ primary: '1h';
1476
+ context: '6h';
1477
+ };
1478
+ baselines: Record<string, ViralBaseline>;
1479
+ provenance: {
1480
+ providers: ViralProvider[];
1481
+ };
1482
+ meta: ViralReadMeta;
1483
+ /** True when no further page exists in this build (always on `off` and `building`). */
1484
+ exhausted: boolean;
1485
+ }
1293
1486
  /** Read options for callers that hold their own deadline, such as a discovery page. */
1294
1487
  export interface SocialReadOptions {
1295
1488
  retry?: boolean;
@@ -1409,6 +1602,14 @@ export declare class ProtocolsResource {
1409
1602
  getSharedRadarStory(projectId: string, params: {
1410
1603
  cluster: string;
1411
1604
  }): Promise<SingleResponse<SocialRadarStory>>;
1605
+ /**
1606
+ * The Viral read without a key, for a signed-out visitor: posts whose authors allow wide reach only, and media only
1607
+ * where the server cleared it, so a `media: 'video'` read can carry items whose video was withheld (filter with
1608
+ * `viralItemHasVideo`). `status: 'off'` (no items, never thrown) until the server enables the read for the app;
1609
+ * an API that predates the read answers this path 401 `missing_api_key`, which resolves as off too. Every other
1610
+ * failure throws. Cacheable for a minute by a shared cache.
1611
+ */
1612
+ getSharedViral(projectId: string, params?: ViralReadParams, options?: SocialReadOptions): Promise<SingleResponse<ViralReadResponse>>;
1412
1613
  publicNetworks(): Promise<SingleResponse<SocialNetwork[]>>;
1413
1614
  /** Query the live public network. It does not import users or posts. */
1414
1615
  searchPublicPosts(params: SearchPublicPostsParams): Promise<SingleResponse<SocialPostSearch>>;
@@ -1477,6 +1678,12 @@ export declare class ProtocolsResource {
1477
1678
  getPublicRadarStory(params: {
1478
1679
  cluster: string;
1479
1680
  }, options?: SocialReadOptions): Promise<SingleResponse<SocialRadarStory>>;
1681
+ /**
1682
+ * The Viral read under the signed-in member's app key (scopes `users:read` and `posts:read`): every network's posts
1683
+ * the build keeps, less the member's own blocks, mutes and hidden posts. Never cached. `status: 'off'` resolves with
1684
+ * no items; a 401 or 403 always throws (a session or key failure is never reported as off).
1685
+ */
1686
+ getPublicViral(params?: ViralReadParams, options?: SocialReadOptions): Promise<SingleResponse<ViralReadResponse>>;
1480
1687
  /** Provider-ranked public accounts with explicit provenance; served from the app's index when a fresh snapshot exists. */
1481
1688
  getPublicPopularProfiles(params: {
1482
1689
  protocol: string;