@recursiv/sdk 0.7.104 → 0.7.116

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
@@ -68,7 +68,9 @@ for await (const chunk of r.agents.chatStream(agent.id, { message: 'Hello!' }))
68
68
  | `r.databases` | `ensure`, `create`, `list`, `getCredentials`, `query` |
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
+ | `r.deployments` | Operator release coordination: `reserveRelease`, `getRelease`, `advanceRelease`, `startRelease`, `finishRelease`, `recoverRelease`; follow the [shared release guide](../../docs/deploy/shared-releases.md) |
71
72
  | `r.storage` | `ensureBucket`, `getUploadUrl`, `getDownloadUrl`, `listItems`, `deleteObject` |
73
+ | `r.telemetry` | `productCounts` — app-bound first-party aggregate counts; reviewed event names and counts only, retained at most 30 days |
72
74
  | `r.sandbox` | `execute` (anonymous, no API key needed) |
73
75
 
74
76
  ### AI Agents
@@ -123,8 +125,9 @@ for await (const chunk of r.agents.chatStream(agent.id, { message: 'Hello!' }))
123
125
  | `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
126
  | `r.dispatcher` | Task queue, claims, signals, outcomes, webhooks |
125
127
  | `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` |
128
+ | `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 |
129
+ | `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 |
130
+ | `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
131
 
129
132
  Full reference: see [llm.md](./llm.md) (ships with this package) or the type definitions in `dist/`.
130
133
 
@@ -205,6 +208,8 @@ await r.projects.createSandbox('proj_1')
205
208
  await r.projects.stopSandbox('proj_1')
206
209
  ```
207
210
 
211
+ Shared API/frontend releases use `r.deployments` with a project-bound operator key carrying `projects:read` and `projects:write`. Read an existing reservation with `await r.deployments.getRelease(releaseId, projectId)`. Follow the [shared release guide](../../docs/deploy/shared-releases.md) for phase transitions, health proof and recovery; uncertain operations keep ownership held.
212
+
208
213
  ## AI agents
209
214
 
210
215
  ```typescript
@@ -316,6 +321,7 @@ const { apiKey, user } = await anonymous.auth.signInAndCreateKey(
316
321
  name: 'my-app',
317
322
  scopes: ['posts:read'],
318
323
  projectId: 'prj_xxx',
324
+ expiresAt: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000).toISOString(),
319
325
  consumeSession: true,
320
326
  },
321
327
  )
@@ -324,6 +330,7 @@ const { apiKey, user } = await anonymous.auth.signInAndCreateKey(
324
330
  `consumeSession: true` atomically retires the temporary login session as the
325
331
  API key is minted. Use it when the customer app stores and authenticates only
326
332
  with the returned API key; omit it for flows that still need the cookie session.
333
+ `createApiKey(input, sessionToken)` requires a signed-in session. Its optional `expiresAt` must be in the future and at most one year away. The creation receipt includes the stored `projectId`, `networkId` and `expiresAt`; validate these bindings before storing an operator key, and keep the returned key out of logs.
327
334
 
328
335
  ## Next.js server auth (`@recursiv/sdk/next`)
329
336
 
@@ -387,7 +394,9 @@ const { system, user, references } = renderBundleForModel(bundle, 'explain', { b
387
394
 
388
395
  #### Post pictures as pictures
389
396
 
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:
397
+ 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.
398
+
399
+ 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
400
 
392
401
  ```typescript
393
402
  import { type BundlePicture, bundlePictures, renderBundleForModel } from '@recursiv/sdk/assistant'
@@ -938,7 +947,9 @@ Both take a Politics choice, `compass` (`all`, `mix`, `hide`, or cells in the or
938
947
  `meta.compass_filter` (`status` `applied` or `unavailable`, and counts only), and each session
939
948
  page carries its Minds read's answer as `page.compass` (`unavailable`: the page is unfiltered). Only Minds
940
949
  posts are filtered, so a view that leaves posts out reads best with `source: 'minds'`.
941
- The catalog lists `compass_filter` on every network when the filter is served to the viewer.
950
+ The catalog lists `compass_filter` on every network when the filter is served to the viewer, and `compass_sides`
951
+ with it (0.7.116) when the viewer may choose sides and Mix too: a visitor's catalog lists it where the operator serves
952
+ visitors the whole filter; without it, offer a visitor Hide politics only.
942
953
  Hide politics (`compass: 'hide'`) also applies on its own, with or without compass judgments on the server, to
943
954
  `sdk.discovery.videos`, `protocols.getPublicTrends` and the radar (`getPublicRadar`, keyless `getSharedRadar`; 0.7.62):
944
955
  it leaves out what is categorised politics (a story not themed yet, or `mixed`, too) and says what it did in
@@ -955,7 +966,7 @@ From 0.7.72 Hide politics on those reads that need no compass judgment (the rada
955
966
  visitor's Discover posts) applies for every caller, whether or not the operator serves them the full filter: where it
956
967
  does not, Hide is the floor (the server's own `political` flags and browse categories, no judgment read) and
957
968
  `meta.compass_filter` says `status: 'applied'` with `basis: 'flags'`. Choose sides, Mix sides and the catalog's
958
- `compass_filter` capability still follow the operator's audience.
969
+ `compass_filter` and `compass_sides` capabilities still follow the operator's audience.
959
970
 
960
971
  Every story names one outlet count (0.7.71): radar stories, World Today stories, category strip stories, feed-top and search stories carry `outlets: { total, mainstream, independent, other }`, the distinct outlets of the articles the story lists (buckets sum to `total`). Show `outlets.total`; `sources.outlet` counts articles, not outlets.
961
972
 
@@ -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;
@@ -201,6 +204,7 @@ export { EnvVarsResource } from './resources/env-vars.js';
201
204
  export type { QueryDatabaseInput, QueryResult } from './resources/databases.js';
202
205
  export { GitHubResource } from './resources/github.js';
203
206
  export { DeploymentsResource } from './resources/deployments.js';
207
+ export type { DeploymentReleaseInput, DeploymentRelease, DeploymentReleaseOperation, DeploymentReleaseRecoveryInput } from './resources/deployments.js';
204
208
  export type { PrepareProtocolServersInput, PreparedProtocolServers, ProtocolServerPreparation, ProtocolServerUpgradePreview, ProtocolServerUpgradeInput, ProtocolServerUpgradeStatus } from './resources/deployments.js';
205
209
  export { IntegrationsResource } from './resources/integrations.js';
206
210
  export type { ExecuteToolInput, ExecuteToolResult } from './resources/integrations.js';
@@ -248,7 +252,7 @@ export { BoostsResource } from './resources/boosts.js';
248
252
  export type { BoostBalance, BoostBalanceResponse, BoostCampaign, BoostCampaignResponse, BoostEventResult, BoostLedgerEntry, BoostLedgerKind, BoostPlanCredit, BoostRejectionReason, BoostReviewStatus, BoostStatus, BoostTopupCheckout, BoostTopupConfirmation, BoostTopupLimits, } from './resources/boosts.js';
249
253
  export { ProtocolsResource } from './resources/protocols.js';
250
254
  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';
255
+ 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
256
  export { InboxResource } from './resources/inbox.js';
253
257
  export { NetworkResource } from './resources/network.js';
254
258
  export { SimulatorResource } from './resources/simulator.js';
@@ -272,7 +276,7 @@ export type { Swarm, SwarmMember, CreateSwarmInput, ListSwarmsParams, ScheduledT
272
276
  export { TemplatesResource } from './resources/templates.js';
273
277
  export type { Template, CreateTemplateInput, UpdateTemplateInput, ListTemplatesParams } from './resources/templates.js';
274
278
  export { MediaResource } from './resources/media.js';
275
- export type { GenerateImageInput, GeneratedImage, ImageProvider } from './resources/media.js';
279
+ export type { GenerateImageInput, GeneratedImage, ImageProvider, MediaSourceApprovalInput, MediaSourceApprovalResponse } from './resources/media.js';
276
280
  export { RecursivError, AuthenticationError, AuthorizationError, LegacyHandoffError, NotFoundError, ValidationError, InsufficientCreditsError, RateLimitError, ConflictError, isActionRateLimitScope, EmailNotSentError, } from './errors.js';
277
281
  export type { RateLimitDetails } from './errors.js';
278
282
  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);
@@ -130,6 +130,8 @@ export interface VerifyOtpInput {
130
130
  export interface CreateApiKeyInput {
131
131
  name: string;
132
132
  scopes: string[];
133
+ /** ISO timestamp in the future, at most one year from creation. */
134
+ expiresAt?: string;
133
135
  /**
134
136
  * Optional human-readable note describing what the key is for. Stored on the
135
137
  * key record and surfaced in the dashboard / key listings.
@@ -197,6 +199,9 @@ export interface ApiKeyResult {
197
199
  scopes: string[];
198
200
  organizationId?: string | null;
199
201
  projectId?: string | null;
202
+ networkId?: string | null;
203
+ /** The server's stored expiry; null denotes a key without an expiry. */
204
+ expiresAt?: string | null;
200
205
  /** ISO timestamp of when the key was created. */
201
206
  createdAt?: string | null;
202
207
  }
@@ -31,6 +31,42 @@ export interface CreateDeploymentInput {
31
31
  commit_hash?: string;
32
32
  commit_message?: string;
33
33
  }
34
+ export interface DeploymentReleaseInput {
35
+ project_id: string;
36
+ /** Caller-generated random 32-byte secret, encoded as 64 lowercase hex characters. Never log or store in source. */
37
+ capability: string;
38
+ api_revision: string;
39
+ frontend_revision: string;
40
+ expected_frontend_revision: string;
41
+ frontend_deploy: boolean;
42
+ coordinator_repository: string;
43
+ run_id: number;
44
+ attempt: number;
45
+ }
46
+ export interface DeploymentRelease {
47
+ deployment_id: string;
48
+ status: string;
49
+ phase: 'reserved' | 'api_started' | 'api_verified' | 'started' | 'complete' | 'aborted';
50
+ api_revision: string;
51
+ frontend_revision: string;
52
+ expected_frontend_revision: string;
53
+ frontend_deploy: boolean;
54
+ coordinator_repository: string;
55
+ run_id: number;
56
+ attempt: number;
57
+ provider_deployment_id: string | null;
58
+ /** Non-secret recovery link; replay still requires both original and replacement capabilities. */
59
+ replacement_deployment_id: string | null;
60
+ }
61
+ export interface DeploymentReleaseOperation {
62
+ project_id: string;
63
+ capability: string;
64
+ }
65
+ export interface DeploymentReleaseRecoveryInput extends DeploymentReleaseOperation {
66
+ replacement: DeploymentReleaseInput;
67
+ /** Short-lived GitHub OIDC token from the main protected Promote workflow. Never log it. */
68
+ coordinator_identity_token: string;
69
+ }
34
70
  export interface GetLogsParams {
35
71
  coolify_app_uuid: string;
36
72
  lines?: number;
@@ -146,6 +182,22 @@ export interface HostedServiceObservation extends HostedServiceReservation {
146
182
  export declare class DeploymentsResource {
147
183
  private client;
148
184
  constructor(client: HttpClient);
185
+ /** Atomically block competing production deployments before promoting the API. Retry explicitly with the SAME capability. */
186
+ reserveRelease(input: DeploymentReleaseInput): Promise<SingleResponse<DeploymentRelease>>;
187
+ getRelease(deploymentId: string, projectId: string): Promise<SingleResponse<DeploymentRelease>>;
188
+ /** Mark immediately before API mutation, then only after the existing API health gates pass. */
189
+ advanceRelease(deploymentId: string, input: DeploymentReleaseOperation & {
190
+ phase: 'api_started' | 'api_verified';
191
+ api_revision: string;
192
+ }): Promise<SingleResponse<DeploymentRelease>>;
193
+ /** Consume the authenticated reservation after API verification; an unknown earlier trigger is never replayed. */
194
+ startRelease(deploymentId: string, input: DeploymentReleaseOperation): Promise<SingleResponse<DeploymentRelease>>;
195
+ /** Abort before API mutation, or finish an API-only release after proving the frontend unchanged. */
196
+ finishRelease(deploymentId: string, input: DeploymentReleaseOperation & {
197
+ operation: 'abort_before_api' | 'complete_api_only' | 'complete_paired';
198
+ }): Promise<SingleResponse<DeploymentRelease>>;
199
+ /** Atomically hand a settled interrupted release to a new protected runner; retry explicitly with identical ownership. */
200
+ recoverRelease(deploymentId: string, input: DeploymentReleaseRecoveryInput): Promise<SingleResponse<DeploymentRelease>>;
149
201
  /** Prepare a stopped staging service from a maintained recipe. Requires an unbound owner key. */
150
202
  prepareHostedService(input: PrepareHostedServiceInput): Promise<SingleResponse<HostedServiceReservation>>;
151
203
  getHostedService(scope: HostedServiceScope): Promise<SingleResponse<HostedServiceObservation | null>>;
@@ -3,6 +3,29 @@ export class DeploymentsResource {
3
3
  constructor(client) {
4
4
  this.client = client;
5
5
  }
6
+ /** Atomically block competing production deployments before promoting the API. Retry explicitly with the SAME capability. */
7
+ reserveRelease(input) {
8
+ return this.client.post('/deployments/releases/reserve', input, { retry: false });
9
+ }
10
+ getRelease(deploymentId, projectId) {
11
+ return this.client.get(`/deployments/releases/${deploymentId}`, { project_id: projectId });
12
+ }
13
+ /** Mark immediately before API mutation, then only after the existing API health gates pass. */
14
+ advanceRelease(deploymentId, input) {
15
+ return this.client.post(`/deployments/releases/${deploymentId}/advance`, input, { retry: false });
16
+ }
17
+ /** Consume the authenticated reservation after API verification; an unknown earlier trigger is never replayed. */
18
+ startRelease(deploymentId, input) {
19
+ return this.client.post(`/deployments/releases/${deploymentId}/start`, input, { retry: false });
20
+ }
21
+ /** Abort before API mutation, or finish an API-only release after proving the frontend unchanged. */
22
+ finishRelease(deploymentId, input) {
23
+ return this.client.post(`/deployments/releases/${deploymentId}/finish`, input, { retry: false });
24
+ }
25
+ /** Atomically hand a settled interrupted release to a new protected runner; retry explicitly with identical ownership. */
26
+ recoverRelease(deploymentId, input) {
27
+ return this.client.post(`/deployments/releases/${deploymentId}/recover`, input, { retry: false });
28
+ }
6
29
  /** Prepare a stopped staging service from a maintained recipe. Requires an unbound owner key. */
7
30
  prepareHostedService(input) {
8
31
  return this.client.post('/deployments/hosted-services/prepare', input);
@@ -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);
@@ -1118,8 +1118,12 @@ export interface SocialNetwork {
1118
1118
  url: string;
1119
1119
  };
1120
1120
  coverage: string;
1121
- /** `compass_filter`: the Politics filter is served to this viewer (an app offers it only then); listed on every network. */
1122
- 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'>;
1121
+ /**
1122
+ * `compass_filter`: the Politics filter is served to this viewer (an app offers it only then); listed on every network.
1123
+ * `compass_sides` (0.7.116): this viewer may also choose sides and Mix sides, not Hide politics alone; a visitor's
1124
+ * catalog lists it where the operator serves visitors the whole filter. Without it, offer a visitor Hide politics only.
1125
+ */
1126
+ 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' | 'compass_sides'>;
1123
1127
  profileSearchPagination: boolean;
1124
1128
  postSearchAccountRequired?: boolean;
1125
1129
  /** Categories available from the current app configuration. */
@@ -1288,6 +1292,48 @@ export interface SocialAuthorFeed {
1288
1292
  cursor: string | null;
1289
1293
  coverage: 'original_posts';
1290
1294
  }
1295
+ /** A Fediverse community (a Lemmy or PieFed community) as its threads page names it (0.7.109). */
1296
+ export interface OpenGroupThreadsGroup {
1297
+ /** Stable (`ap-…`) and never a Minds group id: never send it to a Minds groups route. */
1298
+ id: string;
1299
+ network: 'activitypub';
1300
+ /** The network as members read it ("ActivityPub"). */
1301
+ network_label: string;
1302
+ /** The community's Group actor id on its own server: the `group` its page is read by. */
1303
+ nativeId: string;
1304
+ /** `name@server`, as the network writes the community's address. */
1305
+ handle: string;
1306
+ name: string;
1307
+ /** Plain text. */
1308
+ description: string;
1309
+ /** The host of the server it lives on. */
1310
+ server: string;
1311
+ /** A community is read here, not joined (phase 1). */
1312
+ read_only: true;
1313
+ joinable: false;
1314
+ }
1315
+ /** A page of a Fediverse community's newest threads (`getOpenGroupThreads`, 0.7.109). */
1316
+ export interface OpenGroupThreads {
1317
+ group: OpenGroupThreadsGroup;
1318
+ /** Each thread's opening post, newest first, after every floor the server applies for this member. */
1319
+ items: SocialPost[];
1320
+ /** The next page's opaque cursor, or null at the end of what the community's server listed. */
1321
+ cursor: string | null;
1322
+ /** The community's newest threads as its own server's outbox lists them; older threads are not read. */
1323
+ coverage: 'community_outbox';
1324
+ /** Present when `compass` asked for a filter: what Hide politics left out. */
1325
+ compass_filter?: CompassFilterMeta;
1326
+ }
1327
+ export interface OpenGroupThreadsParams {
1328
+ /** The community's Group actor id (an https address): the `actor` of a groups read's `open_network` entry. */
1329
+ group: string;
1330
+ /** 1 to 25; the server's default is 20. */
1331
+ limit?: number;
1332
+ /** The `cursor` of the page before, as it was given. */
1333
+ cursor?: string;
1334
+ /** A Politics choice as `getPublicTrends` takes it: `hide` leaves out the threads in politics. */
1335
+ compass?: string;
1336
+ }
1291
1337
  export interface SocialTrendProvenance {
1292
1338
  discovery?: DiscoveryLayout;
1293
1339
  provider: {
@@ -1944,6 +1990,17 @@ export declare class ProtocolsResource {
1944
1990
  limit?: number;
1945
1991
  cursor?: string;
1946
1992
  }): Promise<SingleResponse<SocialAuthorFeed>>;
1993
+ /**
1994
+ * A Fediverse community's newest threads, read-only (0.7.109; GET /protocols/social/groups/threads): a Lemmy or PieFed
1995
+ * community that a groups read (`communities.list`, the group recommender) lists under `open_network`, read by its
1996
+ * `actor` as `group`. Every floor the server applies for this member is applied: the group words and tier-1 rules, the
1997
+ * app's moderation and the member's own blocks and mutes of the community, authors who opted out of indexing, the term
1998
+ * list and the safety verdict, and #nsfw threads unless the member turned Show NSFW on. Requires `users:read` and
1999
+ * `posts:read` on the member's app key. A 404 means the app does not offer groups from other networks
2000
+ * (OPEN_GROUPS_AP_ENABLED off) or does not list that community; a 400 an address it does not read; a 503 that the
2001
+ * community's server could not be read.
2002
+ */
2003
+ getOpenGroupThreads(params: OpenGroupThreadsParams, options?: SocialReadOptions): Promise<SingleResponse<OpenGroupThreads>>;
1947
2004
  listSubscriptions(): Promise<SingleResponse<SocialSubscription[]>>;
1948
2005
  /** Recent originals from the current app user's followed accounts, with explicit source failures. */
1949
2006
  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
 
@@ -99,6 +100,8 @@ const { data: logs } = await r.projects.deploymentLogs(project.id, deployment.id
99
100
 
100
101
  Methods: `list`, `get`, `create`, `delete`, `deploy`, `executeCode`, `sandbox`, `createSandbox`, `stopSandbox`, `deployment`, `deploymentLogs`, `deployments`
101
102
 
103
+ Operator API/frontend coordination: `r.deployments.reserveRelease(input)`, `getRelease(releaseId, projectId)`, `advanceRelease(releaseId, input)`, `startRelease(releaseId, input)`, `finishRelease(releaseId, input)`, `recoverRelease(originalReleaseId, input)`. Example read: `const { data: release } = await r.deployments.getRelease(releaseId, projectId)`. Use a project-bound `projects:read`/`projects:write` operator key; follow [shared release phases, live proof and recovery](../../docs/deploy/shared-releases.md). Keep capabilities out of logs; uncertain operations retain ownership.
104
+
102
105
  ### r.storage — Bucket-based file storage
103
106
 
104
107
  ```typescript
@@ -389,10 +392,10 @@ const session = await r.auth.signUp({ name: 'Jane', email: 'jane@example.com', p
389
392
  const session = await r.auth.signIn({ email: 'jane@example.com', password: 'secure12345!' })
390
393
 
391
394
  const key = await r.auth.createApiKey(
392
- { name: 'my-key', scopes: ['posts:read', 'agents:write'], description: 'CI bot' },
395
+ { name: 'my-key', scopes: ['posts:read', 'agents:write'], description: 'CI bot', expiresAt: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000).toISOString() },
393
396
  session.token,
394
397
  )
395
- // key: { key, id, name, prefix, lastFourChars, scopes, createdAt }
398
+ // key: { key, id, name, prefix, lastFourChars, scopes, projectId, networkId, expiresAt, createdAt }
396
399
 
397
400
  await r.auth.signOut(session.token)
398
401
  ```
@@ -401,6 +404,7 @@ await r.auth.signOut(session.token)
401
404
  Bearer auth, with a Cookie fallback for native/server runtimes. `getSession`
402
405
  also answers `session: { id, expiresAt }`; an API-key client lists sessions with
403
406
  `is_current: false`, so it matches `session.id` to mark its own device.
407
+ `createApiKey` requires a signed-in session; optional `expiresAt` is a future ISO timestamp at most one year away. Validate the receipt's stored `projectId`, `networkId` and `expiresAt` before storing an operator key, and never log its key value.
404
408
 
405
409
  Methods: `signUp`, `signIn`, `getSession`, `signOut`, `createApiKey`, `signInAndCreateKey`, `signUpAndCreateKey`, `verifyOtpAndCreateKey`
406
410
 
@@ -825,6 +829,18 @@ Methods: `list`, `get`, `create`, `delete`
825
829
 
826
830
  ## Platform Resources
827
831
 
832
+ ### r.telemetry — First-party aggregate product counts
833
+
834
+ `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.
835
+
836
+ `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.
837
+
838
+ ```typescript
839
+ // r holds the signed-in member's app-bound key; send no route or member properties.
840
+ const result = await r.telemetry.productCounts({ counts: [{ event: 'screen.view', count: 1 }] }).catch(() => null)
841
+ const counted = result?.data.accepted === true
842
+ ```
843
+
828
844
  - `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
845
  - `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
846
 
@@ -846,7 +862,7 @@ const showCheckedLine = isFactLive('sealed.attest_before_send', capabilities, {
846
862
  - `r.notifications` — The notification stream: `list({ category: 'social' })` (rows carry `actor`, `reaction_type` and a `post_preview` with the viewer's `user_reaction`/`reposted_by_viewer_id` and counts; an `agent_needs_input` row carries `answered`, true once you have written in the agent's thread since it asked, when its title reads "You answered <agent>"), `category: 'mentions'` (0.7.72) for a Mentions tab (mentions and replies only; `unreadCount` and `markAllAsRead` take it too), `unreadCount({ category })` for badges (`const { data } = await r.notifications.unreadCount({ category: 'social' })`), `markAsRead(id)`, `archive(id)` to dismiss, `markAllAsRead(category)` or `markAllAsRead({ category, organization_id })`, plus push token `registerToken`/`unregisterToken`. A push's `data.notificationId` is the row to mark read on tap. DMs are not in the stream.
847
863
  - `r.uploads` — Legacy media upload URLs (prefer `r.storage` for new projects)
848
864
  - `r.github` — GitHub integration (getRepository, getTree, getFileContent, getCommits, createFromTemplate)
849
- - `r.deployments` — Deployment management (prefer `r.projects.deploy()` for new projects)
865
+ - `r.deployments` — App deployment management plus protected API/frontend reservations (`reserveRelease`, `getRelease`, `advanceRelease`, `startRelease`, `finishRelease`, `recoverRelease`); see the [shared release guide](../../docs/deploy/shared-releases.md). Use `r.projects.deploy()` for ordinary app deploys.
850
866
  - `r.integrations` — External service integrations and agent tool connections. Connections: `listConnections(orgId)`, `connect()`, `confirmConnection()`. Deterministic tool execution (no agent/LLM in the loop): `executeTool({ connection_id, tool, arguments })` — run a tool yourself with explicit args + pagination and get the raw result back (e.g. `{ tool: 'GMAIL_FETCH_EMAILS', arguments: { query, max_results } }`); use for reliable batch coverage, vs `agents.chat` for autonomous behavior. Tool approvals: `listPendingExecutions(conversationId)`, `approveExecution(id)`, `rejectExecution(id)` — handles both integration tools and brain/platform tools (provision_sandbox etc.); use these to unblock a permission-mode agent waiting on tool approval
851
867
  - `r.email` — Email campaigns, batch sending, and transactional email. **Pass `category: 'transactional'`** for receipts, invites, confirmations and alerts: `r.email.send({ to, subject, html, category: 'transactional' })`. Without it the send is treated as marketing and held for anyone who has not given marketing consent. A send where every recipient was held throws `EmailNotSentError` (its message carries the reason). A `status: 'partial'` result lists each recipient's `status` and `reason` in `messages`.
852
868
  - `r.webhooks` — Register inbound webhook endpoints: `register()`, `list()`, `delete()`. Webhook events execute handler code in project sandbox.
@@ -873,17 +889,17 @@ await r.appSubscriptions.changeTier({ tier: 'pro', proration_date: quote.prorati
873
889
  - 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
890
  - 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
891
  - 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`.
892
+ - `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
893
  - 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
894
  - 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
895
  - 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.
880
896
  - Anonymous discovery: `const visitor = new Recursiv({ anonymous: true, inheritEnvironmentApiKey: false }); const { data } = await visitor.protocols.getSharedTrends(projectId, { protocol: 'nostr', limit: 20, discovery: true })`. Use `sharedNetworks(projectId)` capabilities; open a returned topic with `getSharedTrendFeed(projectId, { protocol: 'bluesky', topic: trend.nativeId, cursor })`. Configured apps only; no account authority or writes. App moderation and per-IP/app rate limits apply. Categories and Jev layouts reuse cached judgments without initiating paid inference; handle partial categories and finite provider windows. Category depth: `r.posts.list({ category: 'science', sort: 'hot', discovery: true })` pages the hot window by offset and then by keyset (`meta.next` is `{ offset }`, `{ cursor }` or `null`; pass a cursor back as `cursor`); when a stage ends, `meta.fallover` lists the next feeds in order (`category_older`, `category_network`, `category` with `via`, `for_you`, `all`; `feed` with `ref` is declared in 0.7.60 but not sent yet, so skip kinds you cannot open). One call for a category's modules: `const { data } = await r.discovery.category({ category: 'science' })` returns `stories`, `network`, `clips`, `people`, `groups`, each `{ items, ms, reason? }`. Watch's Minds videos: `const { data, meta } = await r.discovery.videos({ limit: 12 })` returns public video posts in hot order from a list shared per app (rebuilt about once a minute), each re-checked per call for the viewer; page on `meta.next_cursor`. With no category about one row in four is an open-network clip (0.7.78): `isWatchNetworkClip(row)` tells `{ kind: 'social', id, post: SocialPost }` from a `Post`; open and react to it as the network's post (its `id` is never a Minds id).
881
897
  - Progressive Discover feed (0.7.71): `const feed = r.discovery.createProgressiveFeed({ networks, category, recommend, top: { ref: 'builtin:all' }, seen, seenClusters }); const round = await feed.next(part => draw(part))` opens one `createFeed` session per source (Minds with the radar and suggestions, one per network with `public_trends`) and hands each source's page on as a `DiscoveryPart` (`{ source, round, modules, composed?, mixed? }`) the moment it lands, instead of a page that waits for its slowest network (up to 15 s); a round waits `DISCOVERY_STRAGGLER_MS` past its first answer and rejects only when every source failed. The first load is held until Minds and one other network answered (or `FIRST_SCREEN_MS`, 2.5 s, after the first answer; `firstScreenMs: VISITOR_FIRST_SCREEN_MS` for a visitor) and arrives as one `composed: true` part from `composeFirstScreen`: the top's first post when it is a playable video, else a picture lead from the best 8 by engagement (never a story, rail, video strip, column, `isSensitivePost` post or `linksAdultSite` porn link; 0.7.80: never a post with no picture or clip and no engagement while the screen holds one with either, which then leads: media and engagement, else media, else engagement, as `mergeDiscoveryArrival` leads a feed's top and the feed after a line), then every network's posts by engagement (`DiscoveryEngagementMerge`) with row 2 from another network than the lead's when one is left (`FIRST_SCREEN_NETWORK_ROWS`), a story as row 3 and a rail as row 5, nothing dropped. Merge later `mixed` parts with `mergeDiscoveryArrival(rows, arriving, reachedIds)`. `top`/`seen`/`seenClusters`/`bucket` reach every session; the top is read once per open. Also `retryFailed()`, `onStatus(listener)`, `setNetworks(catalog)`; settings `perSource`, `firstScreen`, `onLanded`. For you: `createProgressiveFeed({ createFeed: o => r.discovery.createPersonalFeed(o) }, options, { perSource: false })`. A session's `seen` now also leaves seen network posts out of trend pages, previews and topic pages; in a category a topic page with nothing new ends its topic and after `DISCOVERY_EMPTY_TOPIC_PAGES` (3) in a row the network's other topics are skipped. 0.7.84: a featured module carries `theme` only when its lead window was themed so and most of its own posts were judged into that category (a category feed's posts, by the read's category filter); a feed with no category titles every module by its kind alone ("Videos", not "Videos in Technology" over an unjudged Nostr clip). A post is shown once by every identity (`discoveryItemIdentities(item)`: its key, an open-network post's network id, and each media file by address and by content hash, so a second note carrying the same clip is the same post) across a session's pages, a progressive feed's sources (`withoutSeenItems` takes identities or keys), `mergeDiscoveryArrival` and gallery pooling; `withoutRepeatedDiscoveryItems(rows, shown)` does the same for an app's own rows.
882
- - Viral read (0.7.59; off until the server enables it for the app): `const { data } = await r.protocols.getPublicViral({ media: 'video', category: 'science', limit: 20 })` (member key, users:read + posts:read, never cached) or keyless `visitor.protocols.getSharedViral(projectId, { media: 'video', compass: 'hide' })` (visitors send compass `all` or `hide` only). Params, all optional: `network` (all, minds, bluesky, activitypub, nostr), `category`, `media: 'video'`, `limit` 5/10/20/40, `cursor` (the last page's; a cursor whose 30-minute build expired ends the list), `compass`. Each item: `key` (stable; drop ones already shown), `protocol`, `post` (checking `protocol` narrows it: `ViralMindsCard` for minds, open at /post/:id; otherwise a `SocialPost`), `label` viral/rising/hot, `score`, `rank`, `category`, `velocity` (show counts with `countsReportedBy` as reported, never verified), `spread`. A visitor's read serves media only where the server cleared it, so even with `media: 'video'` some items arrive with the video withheld (empty `media`): `import { viralItemHasVideo } from '@recursiv/sdk'` and keep `data.items.filter(viralItemHasVideo)` before a vertical video player. `status` ok/building/screening/off: off resolves with no items, and `getSharedViral` also resolves an older API's 401 `missing_api_key` as off; `getPublicViral` throws every 401/403 (never off), so catch it and skip that step.
898
+ - Viral read (0.7.59; off until the server enables it for the app): `const { data } = await r.protocols.getPublicViral({ media: 'video', category: 'science', limit: 20 })` (member key, users:read + posts:read, never cached) or keyless `visitor.protocols.getSharedViral(projectId, { media: 'video', compass: 'hide' })` (visitors send compass `all` or `hide`, and a side or `mix` only where the catalog lists `compass_sides`). Params, all optional: `network` (all, minds, bluesky, activitypub, nostr), `category`, `media: 'video'`, `limit` 5/10/20/40, `cursor` (the last page's; a cursor whose 30-minute build expired ends the list), `compass`. Each item: `key` (stable; drop ones already shown), `protocol`, `post` (checking `protocol` narrows it: `ViralMindsCard` for minds, open at /post/:id; otherwise a `SocialPost`), `label` viral/rising/hot, `score`, `rank`, `category`, `velocity` (show counts with `countsReportedBy` as reported, never verified), `spread`. A visitor's read serves media only where the server cleared it, so even with `media: 'video'` some items arrive with the video withheld (empty `media`): `import { viralItemHasVideo } from '@recursiv/sdk'` and keep `data.items.filter(viralItemHasVideo)` before a vertical video player. `status` ok/building/screening/off: off resolves with no items, and `getSharedViral` also resolves an older API's 401 `missing_api_key` as off; `getPublicViral` throws every 401/403 (never off), so catch it and skip that step.
883
899
  - Public trends: require `public_trends` from `publicNetworks()`, then `getPublicTrends({ protocol, limit?, cursor?, compass?, media? })` (`compass: 'hide'` leaves out politics, 0.7.62; `media: 'video'`, 0.7.73, Bluesky: the network's own window of video posts, a `kind: posts` page paged by its cursor, no category or layout, 404 `unsupported_protocol` for a network without one, 400 on an API that predates it; keyless `getSharedTrends(projectId, { ..., media: 'video' })` holds only clips a visitor may see and counts the rest in `mediaFilter.withheld`); render `kind: topics` as source topics and `kind: posts` as source posts. With `trend_feed`, open only a returned current topic using `getPublicTrendFeed({ protocol, topic: trend.nativeId, limit?, cursor? })`. Show provider, observedAt and provider_ranked_window coverage; null rankingWindow is unknown, not a daily/global ranking. Preserve source order, canonical IDs and readRef, and opaque cursors. Topic catalogs do not paginate; Mastodon trends stop at 200 source positions. Both methods require users:read plus posts:read on the current human app key; Nostr trends are unsupported. Propagate errors; trend_unavailable means refresh the topic list. No ingestion or native writes.
884
900
  - One outlet count per story (0.7.71): a radar story (list, story read, archive), a World Today story (`outlets`, with `outlet_count` its total and `lane_mix` its lanes), a category strip story, a `feeds.top`/`feeds.stories` story and a search story carry `outlets: { total, mainstream, independent, other }`, the distinct outlets of the articles the story lists (an allow-listed outlet once, an unlisted host in `other`; the buckets sum to `total`). Show `outlets.total`; `sources.outlet` counts articles. `DiscoveryCluster.outletCount` reads it. An older server omits it.
885
901
  - Political stories (0.7.67): every radar story (`getPublicRadar`, `getSharedRadar`, a story read) and every World Today story (`r.curator.today()`) carries `political`: true when a quarter of its judged posts are politics, its theme or topic is politics, or its headline or summary names an election, a war or troop deployment, a court or justice, a legislature, or a party or politician. `compass: 'hide'` leaves those out whatever their theme; `r.curator.today({ compass: 'hide' })` does the same to the front page, the next story leads, and `meta.compass_filter` counts what was left out. A story from `r.feeds.stories()` or a `r.feeds.top()` lead carries `political` too, and both reads, and `r.curator.forYouWorld()`, leave political stories out under `compass: 'hide'` whatever their theme. An older server omits `political`: decide by `theme` as before.
886
- - Hide politics as a floor (0.7.72): the radar, `r.curator.today()`, `getPublicTrends`, keyless `visitor.protocols.getSharedTrends(projectId, { protocol, compass: 'hide' })` (visitors send `all` or `hide` only; an older API answers 400 to `compass` there), the viral read, `r.discovery.videos` and a visitor's Discover posts apply `compass: 'hide'` for every caller. Where the operator does not serve the caller the full filter, the answer's `meta.compass_filter` is `{ status: 'applied', basis: 'flags', ... }`: left out by the server's own `political` flags and browse categories, no judgment read. Treat `applied` with or without `basis` as filtered. Choose sides and Mix sides still answer `unavailable` until the operator serves them, and the catalog's `compass_filter` capability still says when to offer Politics in Filters.
902
+ - Hide politics as a floor (0.7.72): the radar, `r.curator.today()`, `getPublicTrends`, keyless `visitor.protocols.getSharedTrends(projectId, { protocol, compass: 'hide' })` (visitors send `all` or `hide`, and a side or `mix` only where the catalog lists `compass_sides`, 0.7.116; an older API answers 400 to `compass` there), the viral read, `r.discovery.videos` and a visitor's Discover posts apply `compass: 'hide'` for every caller. Where the operator does not serve the caller the full filter, the answer's `meta.compass_filter` is `{ status: 'applied', basis: 'flags', ... }`: left out by the server's own `political` flags and browse categories, no judgment read. Treat `applied` with or without `basis` as filtered. Choose sides and Mix sides still answer `unavailable` until the operator serves them, and the catalog's `compass_filter` capability still says when to offer Politics in Filters; `compass_sides` (0.7.116) says the viewer may choose sides and Mix too (a visitor only where the operator serves visitors the whole filter).
887
903
  - Post language: a `SocialPost` may carry `language`, the lower-case primary subtag the post declares (`'de'`, `'en'`; Bluesky `langs[0]`, Mastodon `language`, a Nostr NIP-32 `ISO-639-1` label). It is absent when the post declares none (most Nostr notes) and on posts from the source index; it is never guessed and no post is dropped for it. Order by it (the viewer's language first, undeclared next), never filter on it.
888
904
  - `listCandidates({ protocol?, limit?, cursor? })` and `getCandidate(id)` are read-only quarantine review, not published-post search. Both require `admin` scope, a project-bound key, a live admin role, and existing project-admin access. The key supplies app/network scope; neither method accepts another project or network. They never invoke refresh, change settings, or publish.
889
905
  - Lists return `{ data: ProtocolQuarantinedCandidate[], meta: { limit, has_more, next_cursor } }`; use the opaque `next_cursor`, not offset pagination. The API defaults `limit` to 20 and permits at most 50. Details return `{ data: ProtocolQuarantinedCandidate }`. Every candidate has `state: 'quarantined'` and `content_format: 'plain'`; render its bounded `content` as text, respect `content_truncated`, and distinguish upstream `published_at` from local `observed_at` / `created_at`.
@@ -1083,3 +1099,9 @@ Bluesky quote: accounts must advertise `quote`; call `prepareNativePost({clientM
1083
1099
  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
1100
 
1085
1101
  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.
1102
+
1103
+ ## Public media privacy
1104
+
1105
+ `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.
1106
+
1107
+ `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.116",
4
4
  "description": "TypeScript SDK for the Recursiv API",
5
5
  "license": "FSL-1.1-ALv2",
6
6
  "type": "module",