@recursiv/sdk 0.7.104 → 0.7.109

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 CHANGED
@@ -69,6 +69,7 @@ for await (const chunk of r.agents.chatStream(agent.id, { message: 'Hello!' }))
69
69
  | `r.envVars` | `set`, `list`, `delete` (values write-only; secrets masked on read) |
70
70
  | `r.projects` | `create`, `deploy`, `executeCode`, `createSandbox`, `stopSandbox`, `deploymentLogs` |
71
71
  | `r.storage` | `ensureBucket`, `getUploadUrl`, `getDownloadUrl`, `listItems`, `deleteObject` |
72
+ | `r.telemetry` | `productCounts` — app-bound first-party aggregate counts; reviewed event names and counts only, retained at most 30 days |
72
73
  | `r.sandbox` | `execute` (anonymous, no API key needed) |
73
74
 
74
75
  ### AI Agents
@@ -123,8 +124,9 @@ for await (const chunk of r.agents.chatStream(agent.id, { message: 'Hello!' }))
123
124
  | `r.trust` | The signed trust log (0.7.96; 404 until the operator turns on `TRUST_LOG_PUBLIC_READS`): keyless `keys()` (every key that ever signed it), `mine({ before?, limit? })` (`users:read`: the caller's own signed events with the server's check `verified`). `r.moderation.publicLog` then carries `trust_log` on each action (`status`, and where the signed decision and reversal sit: `chain`, `seq`, `key_id`, `occurred_at`, `verified`). No read carries a hash or a signature |
124
125
  | `r.dispatcher` | Task queue, claims, signals, outcomes, webhooks |
125
126
  | `r.goals` | Org goals with task rollups (group dispatcher tasks) |
126
- | `r.linkPreview` | `get(url)` — a public link's Open Graph card (`data: null` when it has none). For a chat link pass `{ messageId }` or `{ conversationId }` on a signed-in key with `chat:read`: the link is fetched only when it is in the named message and a person wrote that message, or the caller wrote it in that conversation, else `refused: 'ai_link'`. A source with no id rejects (`ValidationError`). Not in encrypted chats: the server cannot read them, so leave `source` out there and apply the app's own rule |
127
- | `r.protocols` | Private Minds safety: `sourceControls` (current enablement and saved choices; filter with `control`, each item may carry a `display` label), `setSourceControl`, `onSourceControlChange`, `reportSource`. Delegated accounts: `nativeSocialAccounts`, `startNativeSocialConnection`, `finishNativeSocialConnection`, `disconnectNativeSocialAccount`. Native follows: `nativeFollowStates` (every connected account's state for one source, one read), `nativeFollowState`, `prepareNativeFollow`, `submitNativeFollow`, `nativeFollowStatus` (native following or pending approval, distinct from a Minds feed subscription; preserve the complete signed Nostr list). Native reposts and undo: `nativeRepostState`, `prepareNativeRepost`, `submitNativeRepost`, `nativeRepostStatus` (public posts, explicit approval, connected account, no uncertain retries). Native likes and unlikes: `nativeLikeState`, `prepareNativeLike`, `submitNativeLike`, `nativeLikeStatus` (connected authority, app enablement and explicit approval; resume pending actions instead of duplicating them). Reviewed replies and optional linked posts: `prepareNativeReply`, `submitNativeReply`, `nativeReplyStatus`, `nativeReplyHistory`, `prepareNativeReplyRemoval`, `removeNativeReply` (require deployed API support and app enablement). Keyless shared post links: `sharedNetworks(projectId)`, `getSharedConversation(projectId, { protocol, post, readRef?, replies?: 'none' })` (`replies: 'none'`, 0.7.78: the post alone for a card, with `repliesStatus: 'not_requested'`; `getPublicConversation` and `getConnectedSocialConversation` take it too). Member public reading: `searchPublicPosts` (selected account required for Bluesky/ActivityPub; public Nostr search), `getConnectedSocialConversation` (preserve result readerAccountId/readRef), `publicNetworks`, `searchPublicProfiles`, `getPublicProfile`, `getPublicAuthorFeed`, `getFollowingFeed`, `getPublicConversation`, `getPublicTrends`, `getPublicTrendFeed`; radar stories: `getPublicRadarStory` (one story by id with its posts; one that left the radar within its minimum lifetime, or within the last half hour, still answers live with `liveUntil` and the story that continues it; one that has left for longer comes with its archived copy and the live story it continues as; the id may be a World Today edition story's, and the answer then names it in `editionStory` under the story's radar id; a story a World Today story seeded carries `volume`, the first page of the posts that story counts across Minds and the open networks, whose `cursor` goes on with `curator.storyPosts` or `curator.memberStoryPosts`), keyless `getSharedRadarStory(projectId, { cluster })` and `getSharedRadarPosts(projectId, { cluster })` (keyless reads carry images and videos only from outlets and members marked `visitorMedia`; signed-in reads keep all media, so a share card uses only those); the Viral read (fastest-moving posts across Bluesky, the Fediverse, Nostr and Minds, each against its own network's pace; 0.7.59): `getPublicViral({ network?, category?, media?: 'video', limit?: 5 | 10 | 20 | 40, cursor?, compass? })` under the member's key and keyless `getSharedViral(projectId, params)` (visitors: `compass` `all` or `hide` only; a visitor's `media: 'video'` read can carry items whose video the server withheld, so filter with the exported `viralItemHasVideo` before a video player; `item.post` narrows on `item.protocol`). `status: 'off'` resolves with no items until the app enables the read, and `getSharedViral` also resolves an older API's 401 `missing_api_key` as off; `getPublicViral` throws every 401 and 403, so a session failure is never read as off; private app subscriptions: `listSubscriptions`, `subscribeToProfile`, `unsubscribeFromProfile`, `onSubscriptionChange`. `list` returns bounded adapter `maturity`, `limitations`, and exhaustive `supported`/`planned`/`unsupported` implementation capabilities (not provider health or enablement); settings, candidate refresh/status, search, and read-only quarantined candidate review via `listCandidates` / `getCandidate` |
127
+ | `r.media` | `sourceApprovals({ projectId, sources })` — cached public host/purpose judgments and exact owned hosts; no caller-triggered provider fetch or inference. Image/icon requests stay relayed; direct players use approved, unexpired decisions once enforcement is active |
128
+ | `r.linkPreview` | `get(url, source?, { projectId })` opts into expiring relayed image/icon capabilities; refresh before `media_expires_at`, and preserve source authorization. `get(url)` — a public link's Open Graph card (`data: null` when it has none). For a chat link pass `{ messageId }` or `{ conversationId }` on a signed-in key with `chat:read`: the link is fetched only when it is in the named message and a person wrote that message, or the caller wrote it in that conversation, else `refused: 'ai_link'`. A source with no id rejects (`ValidationError`). Not in encrypted chats: the server cannot read them, so leave `source` out there and apply the app's own rule |
129
+ | `r.protocols` | Private Minds safety: `sourceControls` (current enablement and saved choices; filter with `control`, each item may carry a `display` label), `setSourceControl`, `onSourceControlChange`, `reportSource`. Delegated accounts: `nativeSocialAccounts`, `startNativeSocialConnection`, `finishNativeSocialConnection`, `disconnectNativeSocialAccount`. Native follows: `nativeFollowStates` (every connected account's state for one source, one read), `nativeFollowState`, `prepareNativeFollow`, `submitNativeFollow`, `nativeFollowStatus` (native following or pending approval, distinct from a Minds feed subscription; preserve the complete signed Nostr list). Native reposts and undo: `nativeRepostState`, `prepareNativeRepost`, `submitNativeRepost`, `nativeRepostStatus` (public posts, explicit approval, connected account, no uncertain retries). Native likes and unlikes: `nativeLikeState`, `prepareNativeLike`, `submitNativeLike`, `nativeLikeStatus` (connected authority, app enablement and explicit approval; resume pending actions instead of duplicating them). Reviewed replies and optional linked posts: `prepareNativeReply`, `submitNativeReply`, `nativeReplyStatus`, `nativeReplyHistory`, `prepareNativeReplyRemoval`, `removeNativeReply` (require deployed API support and app enablement). Keyless shared post links: `sharedNetworks(projectId)`, `getSharedConversation(projectId, { protocol, post, readRef?, replies?: 'none' })` (`replies: 'none'`, 0.7.78: the post alone for a card, with `repliesStatus: 'not_requested'`; `getPublicConversation` and `getConnectedSocialConversation` take it too). Member public reading: `searchPublicPosts` (selected account required for Bluesky/ActivityPub; public Nostr search), `getConnectedSocialConversation` (preserve result readerAccountId/readRef), `publicNetworks`, `searchPublicProfiles`, `getPublicProfile`, `getPublicAuthorFeed`, `getFollowingFeed`, `getPublicConversation`, `getPublicTrends`, `getPublicTrendFeed`; a Fediverse community's newest threads, read-only (0.7.109): `getOpenGroupThreads({ group, limit?, cursor?, compass? })`, `group` being the `actor` of a groups read's `open_network` entry; radar stories: `getPublicRadarStory` (one story by id with its posts; one that left the radar within its minimum lifetime, or within the last half hour, still answers live with `liveUntil` and the story that continues it; one that has left for longer comes with its archived copy and the live story it continues as; the id may be a World Today edition story's, and the answer then names it in `editionStory` under the story's radar id; a story a World Today story seeded carries `volume`, the first page of the posts that story counts across Minds and the open networks, whose `cursor` goes on with `curator.storyPosts` or `curator.memberStoryPosts`), keyless `getSharedRadarStory(projectId, { cluster })` and `getSharedRadarPosts(projectId, { cluster })` (keyless reads carry images and videos only from outlets and members marked `visitorMedia`; signed-in reads keep all media, so a share card uses only those); the Viral read (fastest-moving posts across Bluesky, the Fediverse, Nostr and Minds, each against its own network's pace; 0.7.59): `getPublicViral({ network?, category?, media?: 'video', limit?: 5 | 10 | 20 | 40, cursor?, compass? })` under the member's key and keyless `getSharedViral(projectId, params)` (visitors: `compass` `all` or `hide` only; a visitor's `media: 'video'` read can carry items whose video the server withheld, so filter with the exported `viralItemHasVideo` before a video player; `item.post` narrows on `item.protocol`). `status: 'off'` resolves with no items until the app enables the read, and `getSharedViral` also resolves an older API's 401 `missing_api_key` as off; `getPublicViral` throws every 401 and 403, so a session failure is never read as off; private app subscriptions: `listSubscriptions`, `subscribeToProfile`, `unsubscribeFromProfile`, `onSubscriptionChange`. `list` returns bounded adapter `maturity`, `limitations`, and exhaustive `supported`/`planned`/`unsupported` implementation capabilities (not provider health or enablement); settings, candidate refresh/status, search, and read-only quarantined candidate review via `listCandidates` / `getCandidate` |
128
130
 
129
131
  Full reference: see [llm.md](./llm.md) (ships with this package) or the type definitions in `dist/`.
130
132
 
@@ -387,7 +389,9 @@ const { system, user, references } = renderBundleForModel(bundle, 'explain', { b
387
389
 
388
390
  #### Post pictures as pictures
389
391
 
390
- Pass each media item's `id`, the payload's `checked_at` (when Minds last matched the picture against the child-safety hash lists) and its `held: false` mark into `buildSubjectBundle`, then list the candidates, run your own checks on them, and render with the ones that passed:
392
+ A post picture's `checked_at` is the time this read found it passed Minds' current checks (recursiv#5012): visibility, holds and adult labels. Where the server's media-safety floor is on (CONTENT_MEDIA_SAFETY_ENABLED), one of those labels is a vision model's stored verdict that the picture is not suggestive; no image classifier is run for `checked_at` itself. No hash bank (NCMEC, StopNCII) and no CSAM classifier is among the checks.
393
+
394
+ Pass each media item's `id`, the payload's `checked_at` and its `held: false` mark into `buildSubjectBundle`, then list the candidates, run your own checks on every one of them, and render with the ones that passed:
391
395
 
392
396
  ```typescript
393
397
  import { type BundlePicture, bundlePictures, renderBundleForModel } from '@recursiv/sdk/assistant'
@@ -3,9 +3,9 @@ import type { AssistIntent, BundlePicture, BundlePictureOptions, RenderBundleOpt
3
3
  export declare const MEMBER_LINE_PREFIX = "| ";
4
4
  /**
5
5
  * When a post picture may go to a model as a picture (sealed-full-scope-v1 2.1, 2.2): at most `max_pictures` in one
6
- * message, pictures from the phone included (the caller passes the room left as `max`); only while Minds' last
7
- * child-safety check of it is younger than `fresh_ms`; a check time up to `clock_skew_ms` ahead of the caller's clock
8
- * still counts (clocks drift), and one further ahead is not a time Minds wrote, so it does not.
6
+ * message, pictures from the phone included (the caller passes the room left as `max`); only while the time it last
7
+ * passed Minds' current checks (`checked_at`) is younger than `fresh_ms`; a check time up to `clock_skew_ms` ahead of
8
+ * the caller's clock still counts (clocks drift), and one further ahead is not a time Minds wrote, so it does not.
9
9
  */
10
10
  export declare const BUNDLE_PICTURE_LIMITS: Readonly<{
11
11
  max_pictures: 3;
@@ -12,11 +12,12 @@
12
12
  *
13
13
  * Pictures (sealed-full-scope-v1 2.1, 2.2): with `pictures` given, a picture on the asked-about post, its parent or
14
14
  * its quote (never a reply's) also goes as a picture, an image part inside the content block after the content's text,
15
- * when Minds' last child-safety check of it (`checked_at`) is under 24 hours old by the caller's clock, the caller
16
- * approved it (`allowed`, from bundlePictures) and the caller's image cache holds its prepared JPEG; at most 3 per
17
- * message, fewer when the message carries pictures from the phone, never for 'translate'. Every other picture goes as
18
- * its alt text and stored caption only, as before. The renderer never reads a clock or fetches: the clock and the
19
- * cached bytes are the caller's.
15
+ * when the time it last passed Minds' current checks (`checked_at`; SubjectMediaInput says what those checks are and
16
+ * are not) is under 24 hours old by the caller's clock, the caller approved it (`allowed`, from bundlePictures, after
17
+ * the caller's own checks) and the caller's image cache holds its prepared JPEG; at most 3 per message, fewer when the
18
+ * message carries pictures from the phone, never for 'translate'. Every other picture goes as its alt text and stored
19
+ * caption only, as before. The renderer never reads a clock or fetches: the clock and the cached bytes are the
20
+ * caller's.
20
21
  */
21
22
  import { count, isoTime, plainHandle, plainId, referencesOf, webAddress } from './subjectBundle.js';
22
23
  import { ASSIST_INTENTS } from './types.js';
@@ -27,9 +28,9 @@ const LANGUAGE_PATTERN = /^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8}){0,3}$/;
27
28
  export const MEMBER_LINE_PREFIX = '| ';
28
29
  /**
29
30
  * When a post picture may go to a model as a picture (sealed-full-scope-v1 2.1, 2.2): at most `max_pictures` in one
30
- * message, pictures from the phone included (the caller passes the room left as `max`); only while Minds' last
31
- * child-safety check of it is younger than `fresh_ms`; a check time up to `clock_skew_ms` ahead of the caller's clock
32
- * still counts (clocks drift), and one further ahead is not a time Minds wrote, so it does not.
31
+ * message, pictures from the phone included (the caller passes the room left as `max`); only while the time it last
32
+ * passed Minds' current checks (`checked_at`) is younger than `fresh_ms`; a check time up to `clock_skew_ms` ahead of
33
+ * the caller's clock still counts (clocks drift), and one further ahead is not a time Minds wrote, so it does not.
33
34
  */
34
35
  export const BUNDLE_PICTURE_LIMITS = Object.freeze({
35
36
  max_pictures: 3,
@@ -55,10 +55,14 @@ export interface SubjectMediaInput {
55
55
  id?: string | null;
56
56
  /**
57
57
  * When the picture last passed Minds' current checks (the post payload's `media[].checked_at`, sent with `held:
58
- * false`, sealed-full-scope-v1 2.1): an ISO time with its zone, written only by the server. Under Bill's 2026-10-11
59
- * departure from gate P1 no hash-bank check is among them (recursiv#5012). Kept only when `held` is exactly `false`. A picture can go to a model as a picture only while this is under 24 hours old by the
60
- * renderer's clock; absent, not an ISO time with its zone, or without `held: false`, means alt text and the stored
61
- * caption only, as for every other picture.
58
+ * false`, sealed-full-scope-v1 2.1): an ISO time with its zone, written only by the server. The checks are
59
+ * visibility, holds and adult labels (recursiv#5012). Where the server's media-safety floor is on
60
+ * (CONTENT_MEDIA_SAFETY_ENABLED), one of those labels is a vision model's stored verdict that the picture is not
61
+ * suggestive; no image classifier is run for this field. No hash bank (NCMEC, StopNCII) and no CSAM classifier is
62
+ * among the checks, so run your own checks on the picture too. Kept only when `held` is exactly `false`. A picture
63
+ * can go to a model as a picture only while this is under 24 hours old by the renderer's clock; absent, not an ISO
64
+ * time with its zone, or without `held: false`, means alt text and the stored caption only, as for every other
65
+ * picture.
62
66
  */
63
67
  checked_at?: string | null;
64
68
  }
@@ -171,8 +175,8 @@ export interface BundleMedia {
171
175
  /** Pictures only: the media's id, when the input had a plain one. Never written into model input. */
172
176
  id?: string;
173
177
  /**
174
- * Pictures only: Minds' last child-safety check of it (ISO time, UTC), when the input had a valid one and marked the
175
- * picture not held (`held: false`).
178
+ * Pictures only: when it last passed Minds' current checks (ISO time, UTC; `SubjectMediaInput.checked_at` says what
179
+ * those checks are and are not), when the input had a valid one and marked the picture not held (`held: false`).
176
180
  */
177
181
  checked_at?: string;
178
182
  }
@@ -300,7 +304,10 @@ export interface BundlePicture {
300
304
  media_id: string;
301
305
  /** Which picture of its item it is, from 1, as the content numbers them ("picture 2 alt text"). */
302
306
  number: number;
303
- /** Minds' last child-safety check of it (ISO time, UTC), from the bundle, for your own freshness check. */
307
+ /**
308
+ * When it last passed Minds' current checks (ISO time, UTC; `SubjectMediaInput.checked_at` says what those checks
309
+ * are and are not), from the bundle, for your own freshness check. It is not a reason to skip your own checks.
310
+ */
304
311
  checked_at: string;
305
312
  }
306
313
  export interface BundlePictureOptions {
package/dist/index.d.ts CHANGED
@@ -3,6 +3,8 @@ import { DiscoveryResource } from './resources/discovery.js';
3
3
  export * from './resources/discovery.js';
4
4
  export * from './feed-definition.js';
5
5
  export * from './social-protocol-ids.js';
6
+ import { TelemetryResource } from './resources/telemetry.js';
7
+ export * from './resources/telemetry.js';
6
8
  import { AuthResource } from './resources/auth.js';
7
9
  import { PostsResource } from './resources/posts.js';
8
10
  import { UsersResource } from './resources/users.js';
@@ -124,6 +126,7 @@ export declare class Recursiv {
124
126
  readonly swarms: SwarmsResource;
125
127
  readonly templates: TemplatesResource;
126
128
  readonly media: MediaResource;
129
+ readonly telemetry: TelemetryResource;
127
130
  readonly webhooks: WebhooksResource;
128
131
  readonly jobs: JobsResource;
129
132
  readonly curator: CuratorResource;
@@ -248,7 +251,7 @@ export { BoostsResource } from './resources/boosts.js';
248
251
  export type { BoostBalance, BoostBalanceResponse, BoostCampaign, BoostCampaignResponse, BoostEventResult, BoostLedgerEntry, BoostLedgerKind, BoostPlanCredit, BoostRejectionReason, BoostReviewStatus, BoostStatus, BoostTopupCheckout, BoostTopupConfirmation, BoostTopupLimits, } from './resources/boosts.js';
249
252
  export { ProtocolsResource } from './resources/protocols.js';
250
253
  export { PROTOCOL_ADAPTER_MATURITIES, PROTOCOL_CAPABILITY_OPERATIONS, PROTOCOL_CAPABILITY_STATUSES, SOCIAL_FOLLOWING_SOURCE_STATUSES, viralItemHasVideo, } from './resources/protocols.js';
251
- export type { SocialProfile, SocialBridge, SocialSubscription, SocialSubscriptionChange, SocialProfilePage, SocialRadar, SocialRadarCluster, SocialRadarMember, SocialRadarParams, SocialRadarSourceKind, SocialRadarPosts, SocialRadarStory, SocialRadarStoryArchive, ViralBaseline, ViralItem, ViralItemFields, ViralItemSpread, ViralItemVelocity, ViralLabel, ViralMindsCard, ViralMindsItem, ViralNetwork, ViralOpenProtocol, ViralProtocol, ViralProvider, ViralReadMeta, ViralReadParams, ViralReadResponse, ViralReadStatus, ViralRef, ViralSocialItem, SocialNetwork, SocialPost, SocialConversation, SocialTrend, SocialTrendProvenance, SocialTrends, SocialTrendSourceIndex, SocialTrendFeed, SocialPopularProfiles, SocialAuthorFeed, SocialFollowingFeed, SocialFollowingSource, SocialFollowingSourceStatus, SearchPublicProfilesParams, SearchPublicPostsParams, SocialPostSearch, HostedProtocolConnections, HostedProtocol, HostedProtocolIdentity, HostedNostrChallenge, HostedNostrIdentityProof, NativeNostrPrepareInput, NativeNostrPrepared, NativeNostrAccount, NativeNostrPostState, NativeNostrReceipt, NativeSocialProtocol, FederationIdentity, FederationIdentityView, NativeSocialAccount, NativeNetwork, NativeNetworkState, NativeNetworkPresence, NativeNetworkActivation, NativeNetworkOff, SetNativeNetworkInput, NativeSocialCallbackInput, NativeReplyPrepareInput, NativePostPrepareInput, NativeMediaInput, NativeMediaItem, NativePostBatch, NativePostQuote, NativePostHistory, NativeDelegatedPostHistoryItem, NativeNostrPostHistoryItem, NativeFollowSourceInput, NativeFollowPrepareInput, NativeFollowTarget, NativeFollowReceipt, NativeFollowState, NativeFollowAccount, NativeFollowStates, NativeFollowReview, NativeLikeSourceInput, NativeRepostSourceInput, NativeLikePrepareInput, NativeRepostPrepareInput, NativeLikeTarget, NativeRepostTarget, NativeLikeReceipt, NativeRepostReceipt, NativeLikeState, NativeRepostState, NativeLikeReview, NativeRepostReview, NativeReplyReceipt, NativeReplyBatch, NativeReplyHistory, NativeDelegatedReplyHistoryItem, NativeNostrReplyResult, NativeNostrReplyHistoryItem, NativeReplyRemovalReceipt, NativeReplyRemovalPreview, SocialSourceRef, SocialSourceControl, SocialSourceControlDisplay, SocialSourceControlChange, HostedPostPreview, ConnectHostedIdentityInput, CreateHostedIdentityInput, CreatedHostedProtocolIdentity, PreviewHostedPostInput, PublishHostedPostInput, ListProtocolCandidatesParams, ProtocolAdapterMaturity, ProtocolAdapterMetadata, ProtocolCandidateListResponse, ProtocolCandidatePageMeta, ProtocolCapabilityOperation, ProtocolCapabilityStatus, ProtocolCollectionStatus, ProtocolIngestionActivityReason, ProtocolIngestionCircuitHealth, ProtocolIngestionDeadLetterGroup, ProtocolIngestionHealthPosition, ProtocolIngestionHealthReport, ProtocolIngestionHealthRuntime, ProtocolIngestionHealthSection, ProtocolIngestionLaneActivity, ProtocolIngestionLaneCurrentJob, ProtocolIngestionLaneDeadLetters, ProtocolIngestionLaneHealth, ProtocolIngestionLaneJobs, ProtocolIngestionReplayResult, ProtocolIngestionStatus, ProtocolQuarantinedCandidate, ProtocolRefreshResult, ProtocolSettings, ProtocolSettingsUpdateResult, ReplayProtocolIngestionJobInput, UpdateSettingsInput as UpdateProtocolSettingsInput, } from './resources/protocols.js';
254
+ export type { SocialProfile, SocialBridge, SocialSubscription, SocialSubscriptionChange, SocialProfilePage, SocialRadar, SocialRadarCluster, SocialRadarMember, SocialRadarParams, SocialRadarSourceKind, SocialRadarPosts, SocialRadarStory, SocialRadarStoryArchive, ViralBaseline, ViralItem, ViralItemFields, ViralItemSpread, ViralItemVelocity, ViralLabel, ViralMindsCard, ViralMindsItem, ViralNetwork, ViralOpenProtocol, ViralProtocol, ViralProvider, ViralReadMeta, ViralReadParams, ViralReadResponse, ViralReadStatus, ViralRef, ViralSocialItem, SocialNetwork, SocialPost, SocialConversation, SocialTrend, SocialTrendProvenance, SocialTrends, SocialTrendSourceIndex, SocialTrendFeed, SocialPopularProfiles, SocialAuthorFeed, OpenGroupThreads, OpenGroupThreadsGroup, OpenGroupThreadsParams, SocialFollowingFeed, SocialFollowingSource, SocialFollowingSourceStatus, SearchPublicProfilesParams, SearchPublicPostsParams, SocialPostSearch, HostedProtocolConnections, HostedProtocol, HostedProtocolIdentity, HostedNostrChallenge, HostedNostrIdentityProof, NativeNostrPrepareInput, NativeNostrPrepared, NativeNostrAccount, NativeNostrPostState, NativeNostrReceipt, NativeSocialProtocol, FederationIdentity, FederationIdentityView, NativeSocialAccount, NativeNetwork, NativeNetworkState, NativeNetworkPresence, NativeNetworkActivation, NativeNetworkOff, SetNativeNetworkInput, NativeSocialCallbackInput, NativeReplyPrepareInput, NativePostPrepareInput, NativeMediaInput, NativeMediaItem, NativePostBatch, NativePostQuote, NativePostHistory, NativeDelegatedPostHistoryItem, NativeNostrPostHistoryItem, NativeFollowSourceInput, NativeFollowPrepareInput, NativeFollowTarget, NativeFollowReceipt, NativeFollowState, NativeFollowAccount, NativeFollowStates, NativeFollowReview, NativeLikeSourceInput, NativeRepostSourceInput, NativeLikePrepareInput, NativeRepostPrepareInput, NativeLikeTarget, NativeRepostTarget, NativeLikeReceipt, NativeRepostReceipt, NativeLikeState, NativeRepostState, NativeLikeReview, NativeRepostReview, NativeReplyReceipt, NativeReplyBatch, NativeReplyHistory, NativeDelegatedReplyHistoryItem, NativeNostrReplyResult, NativeNostrReplyHistoryItem, NativeReplyRemovalReceipt, NativeReplyRemovalPreview, SocialSourceRef, SocialSourceControl, SocialSourceControlDisplay, SocialSourceControlChange, HostedPostPreview, ConnectHostedIdentityInput, CreateHostedIdentityInput, CreatedHostedProtocolIdentity, PreviewHostedPostInput, PublishHostedPostInput, ListProtocolCandidatesParams, ProtocolAdapterMaturity, ProtocolAdapterMetadata, ProtocolCandidateListResponse, ProtocolCandidatePageMeta, ProtocolCapabilityOperation, ProtocolCapabilityStatus, ProtocolCollectionStatus, ProtocolIngestionActivityReason, ProtocolIngestionCircuitHealth, ProtocolIngestionDeadLetterGroup, ProtocolIngestionHealthPosition, ProtocolIngestionHealthReport, ProtocolIngestionHealthRuntime, ProtocolIngestionHealthSection, ProtocolIngestionLaneActivity, ProtocolIngestionLaneCurrentJob, ProtocolIngestionLaneDeadLetters, ProtocolIngestionLaneHealth, ProtocolIngestionLaneJobs, ProtocolIngestionReplayResult, ProtocolIngestionStatus, ProtocolQuarantinedCandidate, ProtocolRefreshResult, ProtocolSettings, ProtocolSettingsUpdateResult, ReplayProtocolIngestionJobInput, UpdateSettingsInput as UpdateProtocolSettingsInput, } from './resources/protocols.js';
252
255
  export { InboxResource } from './resources/inbox.js';
253
256
  export { NetworkResource } from './resources/network.js';
254
257
  export { SimulatorResource } from './resources/simulator.js';
@@ -272,7 +275,7 @@ export type { Swarm, SwarmMember, CreateSwarmInput, ListSwarmsParams, ScheduledT
272
275
  export { TemplatesResource } from './resources/templates.js';
273
276
  export type { Template, CreateTemplateInput, UpdateTemplateInput, ListTemplatesParams } from './resources/templates.js';
274
277
  export { MediaResource } from './resources/media.js';
275
- export type { GenerateImageInput, GeneratedImage, ImageProvider } from './resources/media.js';
278
+ export type { GenerateImageInput, GeneratedImage, ImageProvider, MediaSourceApprovalInput, MediaSourceApprovalResponse } from './resources/media.js';
276
279
  export { RecursivError, AuthenticationError, AuthorizationError, LegacyHandoffError, NotFoundError, ValidationError, InsufficientCreditsError, RateLimitError, ConflictError, isActionRateLimitScope, EmailNotSentError, } from './errors.js';
277
280
  export type { RateLimitDetails } from './errors.js';
278
281
  export { parseSSE } from './lib/sse.js';
package/dist/index.js CHANGED
@@ -3,6 +3,8 @@ import { DiscoveryResource } from './resources/discovery.js';
3
3
  export * from './resources/discovery.js';
4
4
  export * from './feed-definition.js';
5
5
  export * from './social-protocol-ids.js';
6
+ import { TelemetryResource } from './resources/telemetry.js';
7
+ export * from './resources/telemetry.js';
6
8
  import { HttpClient } from './client.js';
7
9
  import { AuthResource } from './resources/auth.js';
8
10
  import { PostsResource } from './resources/posts.js';
@@ -125,6 +127,7 @@ export class Recursiv {
125
127
  swarms;
126
128
  templates;
127
129
  media;
130
+ telemetry;
128
131
  webhooks;
129
132
  jobs;
130
133
  curator;
@@ -204,6 +207,7 @@ export class Recursiv {
204
207
  this.swarms = new SwarmsResource(client);
205
208
  this.templates = new TemplatesResource(client);
206
209
  this.media = new MediaResource(client);
210
+ this.telemetry = new TelemetryResource(client);
207
211
  this.webhooks = new WebhooksResource(client);
208
212
  this.jobs = new JobsResource(client);
209
213
  this.curator = new CuratorResource(client);
@@ -9,6 +9,8 @@ export interface LinkPreview {
9
9
  siteName: string | null;
10
10
  }
11
11
  export interface LinkPreviewResponse {
12
+ /** Relayed preview images expire; refetch their metadata before this time. */
13
+ media_expires_at?: string;
12
14
  /**
13
15
  * The preview, or null when the link cannot be previewed: the page refused a
14
16
  * server (a private repository, a paywall or bot wall), is not HTML, or could
@@ -69,5 +71,7 @@ export declare class LinkPreviewResource {
69
71
  * @example
70
72
  * const { data, refused } = await r.linkPreview.get(url, { messageId: message.id });
71
73
  */
72
- get(url: string, source?: LinkPreviewSource | null): Promise<LinkPreviewResponse>;
74
+ get(url: string, source?: LinkPreviewSource | null, options?: {
75
+ projectId: string;
76
+ }): Promise<LinkPreviewResponse>;
73
77
  }
@@ -28,8 +28,12 @@ export class LinkPreviewResource {
28
28
  * @example
29
29
  * const { data, refused } = await r.linkPreview.get(url, { messageId: message.id });
30
30
  */
31
- async get(url, source) {
31
+ async get(url, source, options) {
32
32
  const params = { url };
33
+ if (options) {
34
+ params.project_id = options.projectId;
35
+ params.relay_media = '1';
36
+ }
33
37
  if (source != null) {
34
38
  // A source with no id must not turn into a bare request: the server would
35
39
  // then fetch the link with no check at all.
@@ -12,9 +12,31 @@ export interface GeneratedImage {
12
12
  provider: ImageProvider;
13
13
  prompt: string;
14
14
  }
15
+ export interface MediaSourceApprovalInput {
16
+ projectId: string;
17
+ sources: Array<{
18
+ host: string;
19
+ purpose: 'video' | 'audio' | 'player' | 'icon';
20
+ }>;
21
+ }
22
+ export interface MediaSourceApprovalResponse {
23
+ enabled: boolean;
24
+ enforce: boolean;
25
+ policy: string;
26
+ owned_hosts: string[];
27
+ sources: Array<{
28
+ host: string;
29
+ purpose: 'video' | 'audio' | 'player' | 'icon';
30
+ decision: 'approved' | 'relay' | 'pending';
31
+ expires_at: string | null;
32
+ proxy_url?: string | null;
33
+ }>;
34
+ }
15
35
  export declare class MediaResource {
16
36
  private client;
17
37
  constructor(client: HttpClient);
38
+ /** Read cached automatic provider reviews. Sends host/purpose only; never initiates a policy or media fetch. */
39
+ sourceApprovals(input: MediaSourceApprovalInput): Promise<SingleResponse<MediaSourceApprovalResponse>>;
18
40
  /** Generate an AI image and return a URL for use in posts */
19
41
  generateImage(input: GenerateImageInput): Promise<SingleResponse<GeneratedImage>>;
20
42
  }
@@ -3,6 +3,10 @@ export class MediaResource {
3
3
  constructor(client) {
4
4
  this.client = client;
5
5
  }
6
+ /** Read cached automatic provider reviews. Sends host/purpose only; never initiates a policy or media fetch. */
7
+ sourceApprovals(input) {
8
+ return this.client.get('/media/source-approvals', { project_id: input.projectId, sources: JSON.stringify(input.sources) });
9
+ }
6
10
  /** Generate an AI image and return a URL for use in posts */
7
11
  generateImage(input) {
8
12
  return this.client.post('/media/generate', input);
@@ -1288,6 +1288,48 @@ export interface SocialAuthorFeed {
1288
1288
  cursor: string | null;
1289
1289
  coverage: 'original_posts';
1290
1290
  }
1291
+ /** A Fediverse community (a Lemmy or PieFed community) as its threads page names it (0.7.109). */
1292
+ export interface OpenGroupThreadsGroup {
1293
+ /** Stable (`ap-…`) and never a Minds group id: never send it to a Minds groups route. */
1294
+ id: string;
1295
+ network: 'activitypub';
1296
+ /** The network as members read it ("ActivityPub"). */
1297
+ network_label: string;
1298
+ /** The community's Group actor id on its own server: the `group` its page is read by. */
1299
+ nativeId: string;
1300
+ /** `name@server`, as the network writes the community's address. */
1301
+ handle: string;
1302
+ name: string;
1303
+ /** Plain text. */
1304
+ description: string;
1305
+ /** The host of the server it lives on. */
1306
+ server: string;
1307
+ /** A community is read here, not joined (phase 1). */
1308
+ read_only: true;
1309
+ joinable: false;
1310
+ }
1311
+ /** A page of a Fediverse community's newest threads (`getOpenGroupThreads`, 0.7.109). */
1312
+ export interface OpenGroupThreads {
1313
+ group: OpenGroupThreadsGroup;
1314
+ /** Each thread's opening post, newest first, after every floor the server applies for this member. */
1315
+ items: SocialPost[];
1316
+ /** The next page's opaque cursor, or null at the end of what the community's server listed. */
1317
+ cursor: string | null;
1318
+ /** The community's newest threads as its own server's outbox lists them; older threads are not read. */
1319
+ coverage: 'community_outbox';
1320
+ /** Present when `compass` asked for a filter: what Hide politics left out. */
1321
+ compass_filter?: CompassFilterMeta;
1322
+ }
1323
+ export interface OpenGroupThreadsParams {
1324
+ /** The community's Group actor id (an https address): the `actor` of a groups read's `open_network` entry. */
1325
+ group: string;
1326
+ /** 1 to 25; the server's default is 20. */
1327
+ limit?: number;
1328
+ /** The `cursor` of the page before, as it was given. */
1329
+ cursor?: string;
1330
+ /** A Politics choice as `getPublicTrends` takes it: `hide` leaves out the threads in politics. */
1331
+ compass?: string;
1332
+ }
1291
1333
  export interface SocialTrendProvenance {
1292
1334
  discovery?: DiscoveryLayout;
1293
1335
  provider: {
@@ -1944,6 +1986,17 @@ export declare class ProtocolsResource {
1944
1986
  limit?: number;
1945
1987
  cursor?: string;
1946
1988
  }): Promise<SingleResponse<SocialAuthorFeed>>;
1989
+ /**
1990
+ * A Fediverse community's newest threads, read-only (0.7.109; GET /protocols/social/groups/threads): a Lemmy or PieFed
1991
+ * community that a groups read (`communities.list`, the group recommender) lists under `open_network`, read by its
1992
+ * `actor` as `group`. Every floor the server applies for this member is applied: the group words and tier-1 rules, the
1993
+ * app's moderation and the member's own blocks and mutes of the community, authors who opted out of indexing, the term
1994
+ * list and the safety verdict, and #nsfw threads unless the member turned Show NSFW on. Requires `users:read` and
1995
+ * `posts:read` on the member's app key. A 404 means the app does not offer groups from other networks
1996
+ * (OPEN_GROUPS_AP_ENABLED off) or does not list that community; a 400 an address it does not read; a 503 that the
1997
+ * community's server could not be read.
1998
+ */
1999
+ getOpenGroupThreads(params: OpenGroupThreadsParams, options?: SocialReadOptions): Promise<SingleResponse<OpenGroupThreads>>;
1947
2000
  listSubscriptions(): Promise<SingleResponse<SocialSubscription[]>>;
1948
2001
  /** Recent originals from the current app user's followed accounts, with explicit source failures. */
1949
2002
  getFollowingFeed(params?: {
@@ -231,6 +231,23 @@ export class ProtocolsResource {
231
231
  protocol: params.protocol, actor: params.actor, limit: params.limit, cursor: params.cursor,
232
232
  });
233
233
  }
234
+ /**
235
+ * A Fediverse community's newest threads, read-only (0.7.109; GET /protocols/social/groups/threads): a Lemmy or PieFed
236
+ * community that a groups read (`communities.list`, the group recommender) lists under `open_network`, read by its
237
+ * `actor` as `group`. Every floor the server applies for this member is applied: the group words and tier-1 rules, the
238
+ * app's moderation and the member's own blocks and mutes of the community, authors who opted out of indexing, the term
239
+ * list and the safety verdict, and #nsfw threads unless the member turned Show NSFW on. Requires `users:read` and
240
+ * `posts:read` on the member's app key. A 404 means the app does not offer groups from other networks
241
+ * (OPEN_GROUPS_AP_ENABLED off) or does not list that community; a 400 an address it does not read; a 503 that the
242
+ * community's server could not be read.
243
+ */
244
+ getOpenGroupThreads(params, options) {
245
+ // Only the four keys the server's strict query schema takes, each only when given.
246
+ return this.client.get('/protocols/social/groups/threads', { group: params.group,
247
+ ...(params.limit !== undefined ? { limit: params.limit } : {}),
248
+ ...(params.cursor !== undefined ? { cursor: params.cursor } : {}),
249
+ ...(params.compass !== undefined ? { compass: params.compass } : {}) }, ...readOptions(options));
250
+ }
234
251
  listSubscriptions() {
235
252
  return this.client.get('/protocols/social/subscriptions');
236
253
  }
@@ -0,0 +1,17 @@
1
+ import type { HttpClient } from '../client.js';
2
+ import type { SingleResponse } from '../types.js';
3
+ export type ProductEvent = 'auth.otp_verified' | 'community.join' | 'person.follow' | 'network.connect_started' | 'network.connect_finished' | 'social.follow_submitted' | 'social.like_submitted' | 'social.repost_submitted' | 'social.post_submitted' | 'social.reply_submitted' | 'signal' | 'screen.view';
4
+ export interface ProductCountsInput {
5
+ counts: Array<{
6
+ event: ProductEvent;
7
+ count: number;
8
+ }>;
9
+ }
10
+ export declare class TelemetryResource {
11
+ private readonly client;
12
+ constructor(client: HttpClient);
13
+ /** Count reviewed events within the member's app. No paths, member IDs or properties; requires an app-bound key. */
14
+ productCounts(input: ProductCountsInput): Promise<SingleResponse<{
15
+ accepted: boolean;
16
+ }>>;
17
+ }
@@ -0,0 +1,10 @@
1
+ export class TelemetryResource {
2
+ client;
3
+ constructor(client) {
4
+ this.client = client;
5
+ }
6
+ /** Count reviewed events within the member's app. No paths, member IDs or properties; requires an app-bound key. */
7
+ productCounts(input) {
8
+ return this.client.post('/telemetry/product-counts', input, { retry: false });
9
+ }
10
+ }
package/dist/types.d.ts CHANGED
@@ -244,9 +244,11 @@ export interface MediaItem {
244
244
  unplayable?: boolean;
245
245
  /**
246
246
  * On an image only, in the member's own app with Sealed on: the time (ISO 8601) this read found the picture passed
247
- * Minds' current checks (visibility, holds, adult labels). No hash-bank or image-classifier check runs. Absent for any
248
- * other item, and whenever a check could not be read. Sealed sends a post picture only while this is under 24 hours
249
- * old.
247
+ * Minds' current checks: visibility, holds and adult labels. Where the server's media-safety floor is on
248
+ * (CONTENT_MEDIA_SAFETY_ENABLED), one of those labels is a vision model's stored verdict that the picture is not
249
+ * suggestive; no image classifier is run for this field. No hash bank (NCMEC, StopNCII) and no CSAM classifier is
250
+ * among the checks, so run your own. Absent for any other item, and whenever a check could not be read. Sealed sends
251
+ * a post picture only while this is under 24 hours old.
250
252
  */
251
253
  checked_at?: string;
252
254
  /**
@@ -394,6 +396,7 @@ export interface PostRepliesPage {
394
396
  next_cursor: string | null;
395
397
  };
396
398
  }
399
+ export type PostDestination = 'minds' | 'activitypub' | 'atproto' | 'nostr';
397
400
  export interface CreatePostInput {
398
401
  content: string;
399
402
  content_format?: 'plain' | 'markdown';
@@ -405,6 +408,8 @@ export interface CreatePostInput {
405
408
  /** Tag names to resolve or create atomically when the post is published. */
406
409
  tag_names?: string[];
407
410
  media_urls?: string[];
411
+ /** Exact publishing selection. Include minds; omitted only by legacy clients. */
412
+ destinations?: PostDestination[];
408
413
  }
409
414
  export interface CreatedPost {
410
415
  id: string;
package/llm.md CHANGED
@@ -28,6 +28,7 @@ const r = new Recursiv() // reads RECURSIV_API_KEY from env
28
28
  | Receive external webhooks (Stripe, etc.) | `r.webhooks.register()` | Custom webhook server |
29
29
  | Server-side cron / scheduled tasks | `r.jobs.create()` | External cron services |
30
30
  | A member's engagement signals (views, dwell, hides) | `r.signals.batch()` | Raw fetch to `/signals/*`, or a resource's `client` |
31
+ | Coarse first-party product counts | `r.telemetry.productCounts()` | Member IDs, paths, URLs or arbitrary event properties |
31
32
 
32
33
  **IMPORTANT:** Do NOT store structured app data (records, inventory, tasks, settings) as JSON inside `r.posts.content`. Posts are for social content. Use `r.databases` for application data.
33
34
 
@@ -825,6 +826,18 @@ Methods: `list`, `get`, `create`, `delete`
825
826
 
826
827
  ## Platform Resources
827
828
 
829
+ ### r.telemetry — First-party aggregate product counts
830
+
831
+ `productCounts({ counts: [{ event, count }] })` sends reviewed event totals under a human member's project-bound key with `users:write`. It is unbillable, including for a free project with zero credits; authentication, scopes and rate limits still apply. No anonymous or agent keys. Only counts are stored in a network/project/UTC-day bucket, retained at most 30 days; member IDs, session IDs, paths, URLs and extra properties are refused. Use `r.signals` separately for personal feed ranking.
832
+
833
+ `event` is `auth.otp_verified`, `community.join`, `person.follow`, `network.connect_started`, `network.connect_finished`, `social.follow_submitted`, `social.like_submitted`, `social.repost_submitted`, `social.post_submitted`, `social.reply_submitted`, `signal` or `screen.view`. Each `count` is an integer from 1 to 1000; send 1-12 entries. Do not replay an accepted batch: counts increment, and the SDK deliberately does not retry this optional write. `{ data: { accepted: false } }` means storage was unavailable, not that the member's action failed. Catch request failures without blocking the app.
834
+
835
+ ```typescript
836
+ // r holds the signed-in member's app-bound key; send no route or member properties.
837
+ const result = await r.telemetry.productCounts({ counts: [{ event: 'screen.view', count: 1 }] }).catch(() => null)
838
+ const counted = result?.data.accepted === true
839
+ ```
840
+
828
841
  - `r.settings` — User preferences, sessions, password changes, account deletion. `revokeOtherSessions({ keep_session_token? })` signs out every other device and keeps this key (and the proven session); `revokeAllSessions()` also revokes this key. Accounts with no password confirm `requestEmailChange({ new_email, email_code })` with a code from `requestConfirmationCode('email_change')`. With a project-bound key, `getPreferences()` includes app-scoped `content`; persist changes with `updateContent({ show_nsfw?, autoplay_video?, ai_enabled?, default_feed?, sealed_adult? })`; `content.default_feed_chosen` is true once the member set `default_feed` themselves (absent on older APIs). `privacy.age_is_adult` is true only when the member answered the age question 18 or older (false for 13-17 and for no answer; only their own preferences carry it). `content.sealed_adult` is the Sealed adult-content setting: it reads true only while it is on and the member is a known adult, and `updateContent({ sealed_adult: true })` from anyone else throws `AuthorizationError` with code `sealed_adult_needs_adult` and saves nothing in that request; `sealed_adult: false` is always accepted. Both are absent on older APIs (treat as false). Notifications (0.7.72): `getPreferences()` returns `notifications.channels`, each kind (`post_mention`, `post_reply`, `post_repost`, `post_reaction`, `follow`, `message`, and the operator kinds) with `{ inApp, push, email }`; `updateNotifications({ channels: { post_reaction: { push: false } } })` changes that kind only, while `in_app`/`push`/`email` stay master switches over every kind. `disableAccount({ password })` (0.7.71) deactivates the account (hidden, nothing deleted; signing back in restores it), asked once; an API that does not serve deactivation answers 404 `NotFoundError`, so keep the control hidden until yours does.
829
842
  - `r.signals` (0.7.71) — The member's engagement signals for their own ranking (For You), under their app-bound key (`agents:write`): `batch(events, { keepalive?, retry? })` sends up to `SIGNAL_BATCH_MAX` (100) `{ event_type, post_id?, dwell_ms?, metadata? }` to POST /signals/batch; `record(event)` sends one to POST /signals/post (for an API before the batch route, which answers that one 404). `event_type`: `view`, `click`, `dwell`, `react_up`, `react_down`, `save`, `hide`, `share`, `mute_source`, `open_chat`, `ask_agent`, `more_like`, `unhide`. Best-effort: a signal the server cannot store answers 200 with `recorded` 0 (or false) and a `reason` (`no_project_scope`, `project_db_unavailable`, `viewing_off`, `rate_limited`, or for `record` `duplicate`). `keepalive: true` is for a flush as the page is hidden or unloads: the request is on the wire before the call returns and is asked once. Only from a public place, never a private message: a `metadata` key or placement (`surface`, `feed`, `source`, `context`, `route`...) naming a chat, DM, inbox, encrypted or Sealed conversation by any spelling (`chatList`, `dmThread`, `/messages/1`) answers 400 (`private_surface`) and nothing in that request is stored; this is read on the metadata as sent, before any key is dropped. An app names its own public places (`{ source: 'discover' }`). In the Minds app a `surface` must be one of `SignalSurface`'s names other than `other` (each a public place the server lists), a `feed`, `source`, `via` or `layoutSource` one the server lists (its feeds, Discover, Watch, stories, the pager and post cards), and any other key naming a place (`context`) is refused there too. With the member's "Use what I watch and read to personalize" off (`settings.updateContent({ personalize_from_viewing: false })`, on by default), only views and explicit choices are stored and `withheld` counts the dwell and opens left out. `clearViewingHistory()` (DELETE /signals/history) is "Clear my viewing history": the member's views, dwell, Watch time and opens in this app are deleted; their hides, "More like this", saves, shares and votes stay. Ingest contract (0.7.93): `metadata` is `SignalMetadata`; the server keeps only its keys (`surface`, `feed`, `block`, `position`, `slot`, `category`, `ref`, `watch_ms`, `duration_ms`, `completed`, `loops`, `sound_on`, `exit` (`swipe`/`leave`/`hide`/`tap`), `visible_count`, `autoplay`, `client_event_id`, `session_seq`, `via`, `source`, `presentation`) and drops the rest; a listed key of the wrong type or past its limit refuses that event (batch: `rejected` + the first ten `rejections` `{ index, reason, key }`, the rest stored; record: 400 `invalid_signal`). `batch`/`record` add a `client_event_id` to each event without one (the same id each time that object is sent), so a retry is stored once (`duplicates`). An open-network item is named by `metadata.ref` (`<protocol>/<native id>`, `cluster:<id>`, `today:<uuid>`). Watch time is held to `duration_ms` x (`loops` + 1) and to the wall clock, a card's dwell to 5 min; a watch under 1 s ending in `exit: 'swipe'` is stored as a skip. Past 600 events a minute per member the rest are `limited` (record: `reason: 'rate_limited'`).
830
843
 
@@ -873,7 +886,7 @@ await r.appSubscriptions.changeTier({ tier: 'pro', proration_date: quote.prorati
873
886
  - 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.
874
887
  - 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.
875
888
  - 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.
876
- - `r.protocols` — Private app safety: `sourceControls` returns current `enabled` plus paged saved choices; `setSourceControl`, `onSourceControlChange` and `reportSource` share app scope. Example: `const choices = new Map(); const stop = r.protocols.onSourceControlChange(change => choices.set(JSON.stringify([change.source, change.control]), change.enabled));` then `await r.protocols.setSourceControl({ source: { protocol: "bluesky", kind: "actor", nativeId: "did:plc:aaaaaaaaaaaaaaaaaaaaaaaa" }, control: "mute", enabled: true, scope: "minds" });` and call `stop(); choices.clear()` on account change/unmount. Notifications follow an exact server confirmation, never an uncertain response. These preferences and reports apply in Minds, not on the remote network. Open network ids (0.7.87, readers first): `import { SOCIAL_PROTOCOL_IDS, isSocialProtocolId } from '@recursiv/sdk'` lists every network a post, account or `SocialSourceRef` may name: bluesky, activitypub, nostr and farcaster (a Farcaster account by its FID, "3"; a cast by `<fid>:0x<40 lowercase hex>`). Listing an id serves nothing: read only what `publicNetworks()` lists, and skip an id this SDK does not know. Writes stay narrower: a native reply, follow, like or repost takes only `NATIVE_WRITE_PROTOCOLS` (bluesky, activitypub, nostr; `isNativeWriteProtocol(post.protocol)` before offering one), and a feed top's `networks` always names those three while a Farcaster lane may be absent (`networks.farcaster ?? []`). Keyless post links: `const visitor = new Recursiv({ anonymous: true, inheritEnvironmentApiKey: false }); const { data: thread } = await visitor.protocols.getSharedConversation(projectId, { protocol: 'nostr', post: publicEventId })`; `sharedNetworks(projectId)` lists public conversation capabilities. Only configured apps; app moderation and rate limits apply, no member state or write authority. Member public reading: `publicNetworks`, `searchPublicProfiles`, `getPublicProfile`, `getPublicAuthorFeed`, `getFollowingFeed`, `getPublicConversation`, `getPublicTrends`, `getPublicTrendFeed`; private app subscriptions: `listSubscriptions`, `subscribeToProfile`, `unsubscribeFromProfile`, `onSubscriptionChange`. `getFollowingFeed` sources: a slow account is `late` (status `late`, or `ok` with `late: true` while its held first page is shown; older APIs say `unavailable` with `late: true`), never a failure: read the next page for its posts (`lateNetworks` names its network). Example after selecting a returned source post: `const { data: thread } = await r.protocols.getPublicConversation({ protocol: post.protocol, post: post.nativeId, readRef: post.readRef })`. Render `focus`, oldest-first `ancestors` and direct `replies`; `public_provider_window` never promises every reply, and `limited` marks known omissions. The author's own thread (0.7.81): `thread` holds the focus author's continuation below the post, oldest first (a "1/" post's 2/, 3/, 4/ ..., at most 20 parts, each held to the same moderation and member controls as a reply); its first part is not repeated in `replies`, so render `focus`, `thread`, then `replies` (`thread ?? []`: older APIs omit it, and a `replies: 'none'` read answers none). A card (a post shared into a chat, a link preview) asks for the post alone, 0.7.78: `getPublicConversation({ protocol, post, replies: 'none' })` (also `getSharedConversation` and `getConnectedSocialConversation`) answers `focus` with no `ancestors` or `replies` and `repliesStatus: 'not_requested'`; a Nostr note answers from one relay read instead of a whole thread's. An older API answers 400 `invalid_input`: read without it. Requires both `users:read` and `posts:read` on the current human app key. Protocol discovery/settings: `const { data: adapters } = await r.protocols.list()` returns bounded `{ id, name, icon, color, maturity, capabilities, limitations }` metadata; `supported` means implemented, not configured, healthy, authorized, or enabled. Methods: `list`, `status`, `listCandidates`, `getCandidate`, `search`, `getSettings`, `refresh`, `updateSettings`.
889
+ - `r.protocols` — Private app safety: `sourceControls` returns current `enabled` plus paged saved choices; `setSourceControl`, `onSourceControlChange` and `reportSource` share app scope. Example: `const choices = new Map(); const stop = r.protocols.onSourceControlChange(change => choices.set(JSON.stringify([change.source, change.control]), change.enabled));` then `await r.protocols.setSourceControl({ source: { protocol: "bluesky", kind: "actor", nativeId: "did:plc:aaaaaaaaaaaaaaaaaaaaaaaa" }, control: "mute", enabled: true, scope: "minds" });` and call `stop(); choices.clear()` on account change/unmount. Notifications follow an exact server confirmation, never an uncertain response. These preferences and reports apply in Minds, not on the remote network. Open network ids (0.7.87, readers first): `import { SOCIAL_PROTOCOL_IDS, isSocialProtocolId } from '@recursiv/sdk'` lists every network a post, account or `SocialSourceRef` may name: bluesky, activitypub, nostr and farcaster (a Farcaster account by its FID, "3"; a cast by `<fid>:0x<40 lowercase hex>`). Listing an id serves nothing: read only what `publicNetworks()` lists, and skip an id this SDK does not know. Writes stay narrower: a native reply, follow, like or repost takes only `NATIVE_WRITE_PROTOCOLS` (bluesky, activitypub, nostr; `isNativeWriteProtocol(post.protocol)` before offering one), and a feed top's `networks` always names those three while a Farcaster lane may be absent (`networks.farcaster ?? []`). Keyless post links: `const visitor = new Recursiv({ anonymous: true, inheritEnvironmentApiKey: false }); const { data: thread } = await visitor.protocols.getSharedConversation(projectId, { protocol: 'nostr', post: publicEventId })`; `sharedNetworks(projectId)` lists public conversation capabilities. Only configured apps; app moderation and rate limits apply, no member state or write authority. Member public reading: `publicNetworks`, `searchPublicProfiles`, `getPublicProfile`, `getPublicAuthorFeed`, `getFollowingFeed`, `getPublicConversation`, `getPublicTrends`, `getPublicTrendFeed`; a Fediverse community a groups read lists under `open_network` (0.7.109): `const { data: page } = await r.protocols.getOpenGroupThreads({ group: entry.actor, limit: 20 })` reads its newest threads read-only (`page.group`, `page.items`); `limit` is 1 to 25, `compass: 'hide'` leaves out the threads in politics, and the next page passes `cursor: page.cursor` while it is set (404 while the app does not offer groups from other networks); private app subscriptions: `listSubscriptions`, `subscribeToProfile`, `unsubscribeFromProfile`, `onSubscriptionChange`. `getFollowingFeed` sources: a slow account is `late` (status `late`, or `ok` with `late: true` while its held first page is shown; older APIs say `unavailable` with `late: true`), never a failure: read the next page for its posts (`lateNetworks` names its network). Example after selecting a returned source post: `const { data: thread } = await r.protocols.getPublicConversation({ protocol: post.protocol, post: post.nativeId, readRef: post.readRef })`. Render `focus`, oldest-first `ancestors` and direct `replies`; `public_provider_window` never promises every reply, and `limited` marks known omissions. The author's own thread (0.7.81): `thread` holds the focus author's continuation below the post, oldest first (a "1/" post's 2/, 3/, 4/ ..., at most 20 parts, each held to the same moderation and member controls as a reply); its first part is not repeated in `replies`, so render `focus`, `thread`, then `replies` (`thread ?? []`: older APIs omit it, and a `replies: 'none'` read answers none). A card (a post shared into a chat, a link preview) asks for the post alone, 0.7.78: `getPublicConversation({ protocol, post, replies: 'none' })` (also `getSharedConversation` and `getConnectedSocialConversation`) answers `focus` with no `ancestors` or `replies` and `repliesStatus: 'not_requested'`; a Nostr note answers from one relay read instead of a whole thread's. An older API answers 400 `invalid_input`: read without it. Requires both `users:read` and `posts:read` on the current human app key. Protocol discovery/settings: `const { data: adapters } = await r.protocols.list()` returns bounded `{ id, name, icon, color, maturity, capabilities, limitations }` metadata; `supported` means implemented, not configured, healthy, authorized, or enabled. Methods: `list`, `status`, `listCandidates`, `getCandidate`, `search`, `getSettings`, `refresh`, `updateSettings`.
877
890
  - 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.
878
891
  - Native networks (0.7.89): every eligible member is on ActivityPub, AT Protocol and Nostr by rule. `const { data } = await r.protocols.networks()` lists `{ network, state, reason, mode, handle, engageDefault }` (read-only; `ready` = set up on their first post or interaction, show "Will be set up when you first post or interact"). `activateNetwork(network)` = "Set up now" (`pending` + `retryAt` over budget; 409 `network_off` after the member's off switch). `setNetwork(network, { enabled: false, removePast: true })` turns it off and removes what was shared; `{ enabled: true }` turns it back on (403 `network_locked`, 409 `not_rolled_out`/`age_band_required`). Needs users:read (+ users:write to change). A linked outside account is a Settings choice, never a step in a main flow.
879
892
  - Hosted Bluesky account on the app's own PDS: `const { data } = await r.protocols.createHostedIdentity({ protocol: 'atproto', handle: 'alice', email })`. Only the label is sent; `data.handle` carries the domain and `data.password` is returned this once, never stored. Never auto-retried. Either the account is finished or it is removed again. 409 `handle_unavailable` = choose another label; 503 `handle_dns_unavailable`/`handle_unresolvable`/`handle_unverified` = operator setup, retry later unchanged.
@@ -1083,3 +1096,9 @@ Bluesky quote: accounts must advertise `quote`; call `prepareNativePost({clientM
1083
1096
  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.
1084
1097
 
1085
1098
  Topic browsing can request `getPublicTrends({ protocol: "bluesky", includePreviews: true })`. Optional `SocialTrend.preview` is a real image-bearing post from that topic, never a generated illustration. The server samples at most eight posts for each of ten topics, with three concurrent reads and a shared 2.5-second deadline; missing images leave usable text topics. App moderation and user source controls filter previews on every request. Bluesky CDN image attachments, quote-with-media images, and unlabeled link-preview thumbnails are supported; omitted content retains the source link.
1099
+
1100
+ ## Public media privacy
1101
+
1102
+ `await r.media.sourceApprovals({ projectId, sources: [{ host: 'www.youtube-nocookie.com', purpose: 'player' }] })` reads only cached judgments for a registered app. Respect `enabled`, `enforce`, exact `owned_hosts` and expiry; unknown/stale provider results remain pending. Static images/icons stay relayed. A judgment permits only its exact host and purpose, and does not establish downstream player hosts or tenant ownership.
1103
+
1104
+ `await r.linkPreview.get(url, { messageId }, { projectId })` opts a signed-in chat preview into expiring opaque image/icon URLs. Keep previews in a source/member-bound memory cache, refresh before `media_expires_at`, clear on sign-out, and never fall back to a bare private chat URL. Existing two-argument calls preserve their wire behavior.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@recursiv/sdk",
3
- "version": "0.7.104",
3
+ "version": "0.7.109",
4
4
  "description": "TypeScript SDK for the Recursiv API",
5
5
  "license": "FSL-1.1-ALv2",
6
6
  "type": "module",