@recursiv/sdk 0.7.89 → 0.7.96

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
@@ -100,7 +100,7 @@ for await (const chunk of r.agents.chatStream(agent.id, { message: 'Hello!' }))
100
100
  | `r.communities` | `create`, `join`, `search`, `members` |
101
101
  | `r.chat` | `send`, `dm`, `createGroup` (`encrypted: true` for an end-to-end encrypted group, 0.7.59), `messages` (cursor paging), `acceptRequest`, `declineRequest`, `leave`, `deleteConversation`; a private group's admin: `addMembers`, `removeMember`, `updateGroup` (0.7.59); photos and voice notes: `uploadAttachment`, `signAttachments` (private chat storage, 0.7.82) |
102
102
  | `r.tags` | `create`, `list` |
103
- | `r.signals` | `batch` (up to 100 engagement signals, such as views, dwell and hides, for the member's own ranking; `keepalive` for a page-leave flush), `record` (one signal) (0.7.71) |
103
+ | `r.signals` | `batch` (up to 100 engagement signals, such as views, dwell and hides, for the member's own ranking; `keepalive` for a page-leave flush), `record` (one signal) (0.7.71); `clearViewingHistory` ("Clear my viewing history"); typed `SignalMetadata`, an id per event so a retry is stored once, and `duplicates`/`limited`/`rejected` in the answer (0.7.93) |
104
104
  | `r.privacy` | `capabilities` (the platform privacy facts the deployed API's build enforces, and its commit); pair with `@recursiv/sdk/privacy` (`PRIVACY_FACTS`, `isFactLive`, `liveFacts`) (unreleased) |
105
105
  | `r.curator` | `today` (World Today; a world story can carry `story_key` and `discussion`, its count across networks), `storyPosts` / `memberStoryPosts` (one page of a story's posts, keyless or under the member's key), `storyPostPages` (walk every page), `forYou`, `run`, `refreshIfStale`, `seedDeck` |
106
106
  | `r.uploads` | `uploadMedia` — upload post/chat bytes end to end |
@@ -120,6 +120,7 @@ for await (const chunk of r.agents.chatStream(agent.id, { message: 'Hello!' }))
120
120
  | `r.integrations` | External service connections |
121
121
  | `r.admin` | User management, stats, content reports (`listReports`, `resolveReport`, `dismissReport`) |
122
122
  | `r.moderation` | Moderation actions: `publicLog`, `mine`, `appeal`; staff `apply`, `resolveAppeal`, `list({ target_type?, target_id? })` (one target's recorded actions, newest first, at most 100; none named = the network's latest; 0.7.74) and `reverse(actionId)` (sent once: a repeat answers 404 `action_not_found`; 0.7.74); agent grants `listAgentGrants`, `grantAgent`, `revokeAgent` |
123
+ | `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 |
123
124
  | `r.dispatcher` | Task queue, claims, signals, outcomes, webhooks |
124
125
  | `r.goals` | Org goals with task rollups (group dispatcher tasks) |
125
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 |
package/dist/index.d.ts CHANGED
@@ -60,6 +60,7 @@ import { JobsResource } from './resources/jobs.js';
60
60
  import { CuratorResource } from './resources/curator.js';
61
61
  import { ReportsResource } from './resources/reports.js';
62
62
  import { ModerationResource } from './resources/moderation.js';
63
+ import { TrustResource } from './resources/trust.js';
63
64
  import { JudgmentsResource } from './resources/judgments.js';
64
65
  import { LinkPreviewResource } from './resources/link-preview.js';
65
66
  import { SearchResource } from './resources/search.js';
@@ -134,6 +135,8 @@ export declare class Recursiv {
134
135
  readonly boosts: BoostsResource;
135
136
  readonly reports: ReportsResource;
136
137
  readonly moderation: ModerationResource;
138
+ /** The signed trust log: its public keys, and the member's own signed events (0.7.96). */
139
+ readonly trust: TrustResource;
137
140
  /** Jev's labels of a post: read them, dispute or appeal one, and (staff) review a dispute. */
138
141
  readonly judgments: JudgmentsResource;
139
142
  readonly linkPreview: LinkPreviewResource;
@@ -216,7 +219,9 @@ export type { ReportTargetType, ReportStatus, CreateReportInput, ReportRecord, R
216
219
  export { JudgmentsResource, isJudgmentSharedWordsError } from './resources/judgments.js';
217
220
  export type { JudgmentSubjectKind, JudgmentSource, JudgmentOverrideMethod, PostLabelsStatus, PostLabelsQuery, PostLabelsSubject, PostLabelsCategory, PostLabelsPolitics, PostJudgmentRecord, PostJudgment, PostSafetyAttribution, PostLabelsCategoryView, PostLabelsSummaryView, PostLabelsFullView, PostLabels, DisputeJudgmentInput, JudgmentDispute, ReviewJudgmentInput, JudgmentReviewUpheld, JudgmentReviewAwaitingReviewer, JudgmentReviewOverridden, JudgmentReview, JudgmentSharedWordsDetails, JudgmentSharedWordsError, } from './resources/judgments.js';
218
221
  export { ModerationResource } from './resources/moderation.js';
219
- export type { ModerationTargetType, ModerationActionType, ModerationAppealStatus, ModerationPrinciple, PublicModerationAction, PublicModerationLog, OwnModerationAppeal, OwnModerationAction, OwnModerationLog, ApplyModerationActionInput, ModerationAction, ListModerationActionsParams, RecordedModerationTargetType, AgentModerationMode, AgentModerationGrant, } from './resources/moderation.js';
222
+ export type { ModerationTargetType, ModerationActionType, ModerationAppealStatus, ModerationPrinciple, PublicModerationAction, PublicModerationLog, OwnModerationAppeal, OwnModerationAction, OwnModerationLog, ApplyModerationActionInput, ModerationAction, ListModerationActionsParams, RecordedModerationTargetType, AgentModerationMode, AgentModerationGrant, ModerationTrustStatus, ModerationTrustLog, ModerationTrustWindow, } from './resources/moderation.js';
223
+ export { TrustResource } from './resources/trust.js';
224
+ export type { TrustEnvelope, OwnTrustEvent, TrustPublicKey, OwnTrustEvents } from './resources/trust.js';
220
225
  export { LinkPreviewResource } from './resources/link-preview.js';
221
226
  export { SEARCH_SECTIONS, SearchResource } from './resources/search.js';
222
227
  export type { PublicSearchPage, PublicSearchTab, SearchAnswer, SearchGroup, SearchLadder, SearchOmitReason, SearchParams, SearchPerson, SearchPost, SearchRelated, SearchResponse, SearchSection, SearchStage, SearchStory, SearchTopic, SearchVia, } from './resources/search.js';
@@ -225,8 +230,8 @@ export { FEED_TOP_NETWORKS, FeedsResource } from './resources/feeds.js';
225
230
  export type { FeedStoriesParams, FeedStoriesResponse, FeedStory, FeedTopExplain, FeedTopItem, FeedTopMeta, FeedTopNetwork, FeedTopNetworkCard, FeedTopNetworkEntry, FeedTopNetworkMap, FeedTopParams, FeedTopResponse, FeedTopTarget, PublicFeedTopMeta, PublicFeedTopParams, PublicFeedTopResponse, } from './resources/feeds.js';
226
231
  export type { BookmarkCollection, BookmarkCollectionInput, BookmarkCollectionKind, BookmarkCollectionPatch, BookmarkCollectionsList, BookmarkCollectionTarget, BookmarkCollectionVisibility, BookmarkedPost, BookmarkInput, BookmarkItem, BookmarkKey, BookmarkKeyRow, BookmarkKeysPage, BookmarkKind, BookmarkMergeItem, BookmarkMergeResult, BookmarkProtocol, BookmarkRequestOptions, BookmarksPage, FiledBookmark, RemovedBookmarkCollection, SavedBookmark, UnfiledBookmark, UnsavedBookmark, } from './resources/bookmarks.js';
227
232
  export { AccountsResource } from './resources/accounts.js';
228
- export { SIGNAL_BATCH_MAX, SignalsResource } from './resources/signals.js';
229
- export type { SignalBatchResult, SignalEventInput, SignalEventType, SignalNotRecordedReason, SignalRecordResult, SignalSendOptions, } from './resources/signals.js';
233
+ export { SIGNAL_BATCH_MAX, SignalsResource, newClientEventId } from './resources/signals.js';
234
+ export type { ClearViewingHistoryResult, SignalBatchResult, SignalEventInput, SignalEventType, SignalExit, SignalMetadata, SignalNotRecordedReason, SignalRecordResult, SignalRejection, SignalSendOptions, SignalSurface, } from './resources/signals.js';
230
235
  export { SEALED_ALLOWANCE_REMAINING_HEADER, SEALED_REFUSAL_HEADER, SealedResource, SealedTransportError, } from './resources/sealed.js';
231
236
  export type { SealedAllowance, SealedAttestationInput, SealedConfig, SealedModel, SealedPlan, SealedPurpose, SealedQuotaDetails, SealedSendInput, SealedTarget, SealedTargetKind, SealedTransportErrorCode, } from './resources/sealed.js';
232
237
  export type { SiblingAccount, SiblingAccountsResponse, SwitchAccountResult, } from './resources/accounts.js';
package/dist/index.js CHANGED
@@ -61,6 +61,7 @@ import { JobsResource } from './resources/jobs.js';
61
61
  import { CuratorResource } from './resources/curator.js';
62
62
  import { ReportsResource } from './resources/reports.js';
63
63
  import { ModerationResource } from './resources/moderation.js';
64
+ import { TrustResource } from './resources/trust.js';
64
65
  import { JudgmentsResource } from './resources/judgments.js';
65
66
  import { LinkPreviewResource } from './resources/link-preview.js';
66
67
  import { SearchResource } from './resources/search.js';
@@ -135,6 +136,8 @@ export class Recursiv {
135
136
  boosts;
136
137
  reports;
137
138
  moderation;
139
+ /** The signed trust log: its public keys, and the member's own signed events (0.7.96). */
140
+ trust;
138
141
  /** Jev's labels of a post: read them, dispute or appeal one, and (staff) review a dispute. */
139
142
  judgments;
140
143
  linkPreview;
@@ -211,6 +214,7 @@ export class Recursiv {
211
214
  this.boosts = new BoostsResource(client);
212
215
  this.reports = new ReportsResource(client);
213
216
  this.moderation = new ModerationResource(client);
217
+ this.trust = new TrustResource(client);
214
218
  this.judgments = new JudgmentsResource(client);
215
219
  this.linkPreview = new LinkPreviewResource(client);
216
220
  this.search = new SearchResource(client);
@@ -274,12 +278,13 @@ export { STORY_POOL_MAX_POSTS, STORY_POST_NETWORKS, STORY_POSTS_PAGE_MAX } from
274
278
  export { ReportsResource } from './resources/reports.js';
275
279
  export { JudgmentsResource, isJudgmentSharedWordsError } from './resources/judgments.js';
276
280
  export { ModerationResource } from './resources/moderation.js';
281
+ export { TrustResource } from './resources/trust.js';
277
282
  export { LinkPreviewResource } from './resources/link-preview.js';
278
283
  export { SEARCH_SECTIONS, SearchResource } from './resources/search.js';
279
284
  export { BOOKMARK_MERGE_MAX_ITEMS, BookmarkCollectionsResource, BookmarksResource } from './resources/bookmarks.js';
280
285
  export { FEED_TOP_NETWORKS, FeedsResource } from './resources/feeds.js';
281
286
  export { AccountsResource } from './resources/accounts.js';
282
- export { SIGNAL_BATCH_MAX, SignalsResource } from './resources/signals.js';
287
+ export { SIGNAL_BATCH_MAX, SignalsResource, newClientEventId } from './resources/signals.js';
283
288
  export { SEALED_ALLOWANCE_REMAINING_HEADER, SEALED_REFUSAL_HEADER, SealedResource, SealedTransportError, } from './resources/sealed.js';
284
289
  export { OrganizationSettingsResource } from './resources/organization-settings.js';
285
290
  export { ProjectSettingsResource } from './resources/project-settings.js';
@@ -1,5 +1,6 @@
1
1
  import type { HttpClient } from '../client.js';
2
2
  import type { SingleResponse } from '../types.js';
3
+ import type { TrustEnvelope } from './trust.js';
3
4
  export type ModerationTargetType = 'post' | 'user' | 'message' | 'community' | 'agent' | 'source_actor' | 'source_post';
4
5
  export type ModerationActionType = 'down_rank' | 'label' | 'remove' | 'suspend' | 'ban';
5
6
  export type ModerationAppealStatus = 'none' | 'pending' | 'upheld' | 'overturned';
@@ -8,9 +9,30 @@ export interface ModerationPrinciple {
8
9
  title: string;
9
10
  summary: string;
10
11
  }
12
+ /**
13
+ * Whether a decision is in the signed trust log, by the server's check (0.7.96): `verified` (signed, and its signature,
14
+ * hash and link to the event before it hold), `unverified` (signed, and one of those does not), `unsigned` (decided
15
+ * before the log was written, or not signed yet), `unavailable` (the log could not be read just now).
16
+ */
17
+ export type ModerationTrustStatus = 'verified' | 'unverified' | 'unsigned' | 'unavailable';
18
+ /** One decision's signed envelopes: where they sit in the chain and what signed them, never the moderator, the subject or a hash. */
19
+ export interface ModerationTrustLog {
20
+ status: ModerationTrustStatus;
21
+ decision: TrustEnvelope | null;
22
+ reversal: TrustEnvelope | null;
23
+ }
24
+ /** The page as a whole: unverified if any action is, verified if any is signed, else unsigned. */
25
+ export interface ModerationTrustWindow {
26
+ status: ModerationTrustStatus;
27
+ verified: number;
28
+ unverified: number;
29
+ unsigned: number;
30
+ }
11
31
  export interface PublicModerationAction {
12
32
  id: string;
13
33
  target_type: ModerationTargetType;
34
+ /** One sentence: what kind of thing was acted on and how, never which one. */
35
+ description?: string;
14
36
  action: ModerationActionType;
15
37
  policy: string | null;
16
38
  actor_type: 'human' | 'ai' | 'system';
@@ -18,6 +40,8 @@ export interface PublicModerationAction {
18
40
  reversed_at: string | null;
19
41
  created_at: string;
20
42
  appeal_status: ModerationAppealStatus;
43
+ /** Present once the operator turns on the signed log's reads (TRUST_LOG_PUBLIC_READS); absent before. */
44
+ trust_log?: ModerationTrustLog;
21
45
  }
22
46
  export interface PublicModerationLog {
23
47
  principles: ModerationPrinciple[];
@@ -27,6 +51,8 @@ export interface PublicModerationLog {
27
51
  offset: number;
28
52
  has_more: boolean;
29
53
  };
54
+ /** Present once the operator turns on the signed log's reads; absent before. */
55
+ trust_log?: ModerationTrustWindow;
30
56
  }
31
57
  export interface OwnModerationAppeal {
32
58
  id: string;
@@ -6,24 +6,117 @@ import type { SingleResponse } from '../types.js';
6
6
  * one, so send them in a request of their own if the API may be older.
7
7
  */
8
8
  export type SignalEventType = 'view' | 'click' | 'dwell' | 'react_up' | 'react_down' | 'save' | 'hide' | 'share' | 'mute_source' | 'open_chat' | 'ask_agent' | 'more_like' | 'unhide';
9
- /** One engagement signal. An open-network post has no Minds id: name it in `metadata` (e.g. `{ ref }`) instead. */
9
+ /**
10
+ * Where a signal happened: each one a public place. In the Minds app the server lists these (and Discover's older
11
+ * `discover_featured`, `discover_hot`, `discover_rediscover`) and refuses any other name, `other` included, with a 400
12
+ * (`private_surface`). Another app's well-formed name the server does not list is stored as `other`, so a new surface
13
+ * still lands there; add it here (and to the server's SIGNAL_SURFACES) to keep it apart. There is no `group`: a group
14
+ * can be an encrypted chat, which never sends a signal.
15
+ */
16
+ export type SignalSurface = 'home_feed' | 'discovery' | 'discovery_story' | 'discovery_module' | 'discover_category' | 'discover_for_you' | 'postcard' | 'story' | 'swipe' | 'watch' | 'clip_strip' | 'discover_videos' | 'story_cluster' | 'headline_gallery' | 'post_clip' | 'search_videos' | 'profile' | 'search' | 'post' | 'other';
17
+ /** How a watch, or a card's time on screen, ended. */
18
+ export type SignalExit = 'swipe' | 'leave' | 'hide' | 'tap';
19
+ /**
20
+ * A signal's metadata: the only keys the server keeps (0.7.93, the ingest contract). A key not listed here is dropped.
21
+ * A listed key with a value of the wrong type or past its limit refuses that event (named in `rejections`, never
22
+ * stored); `null` or `''` reads as absent. Times are milliseconds.
23
+ */
24
+ export interface SignalMetadata {
25
+ /**
26
+ * Where it happened (up to 64 characters). In the Minds app one of SignalSurface's names other than `other`;
27
+ * elsewhere an unlisted name is stored as `other`.
28
+ */
29
+ surface?: SignalSurface | (string & {});
30
+ /** Which feed of the surface, e.g. `all`, `category`, `for-you:world` (up to 64). */
31
+ feed?: string;
32
+ /** The block of the feed, e.g. `top` (up to 32). */
33
+ block?: string;
34
+ /** The 0-based row in the feed (0 to 100,000). */
35
+ position?: number;
36
+ /** The place within a module that holds several (0 to 10,000). */
37
+ slot?: number;
38
+ /** A category feed's category (up to 64). */
39
+ category?: string;
40
+ /**
41
+ * An item with no Minds post id: an open-network post `<protocol>/<native id>` (native id up to 2,048 characters, no
42
+ * whitespace), a story lead `cluster:<64 hex>` or a World Today story `today:<uuid>`. Stored in an indexed column.
43
+ */
44
+ ref?: string;
45
+ /** How long the clip played (0 to one hour). Held to `duration_ms` x (`loops` + 1) and to the wall clock. */
46
+ watch_ms?: number;
47
+ /** The clip's length (0 to six hours). */
48
+ duration_ms?: number;
49
+ completed?: boolean;
50
+ /** Times the clip looped (0 to 1,000). */
51
+ loops?: number;
52
+ sound_on?: boolean;
53
+ /** How it ended. A watch under a second that ended in a `swipe` is stored as a skip. */
54
+ exit?: SignalExit;
55
+ /** Cards on screen at once in a grid (1 to 200). Readers divide a grid card's dwell by it; it never divides watch time. */
56
+ visible_count?: number;
57
+ autoplay?: boolean;
58
+ /**
59
+ * The client's id for this event (a letter or digit, then up to 63 of `A-Za-z0-9_.:-`): an event sent again with the
60
+ * same id is stored once. `batch` and `record` add one to an event that has none.
61
+ */
62
+ client_event_id?: string;
63
+ /** The event's place in the client's session (0 to 2^31 - 1). */
64
+ session_seq?: number;
65
+ /** How the action was taken, e.g. `less_like_this`, `module_hide` (up to 64). */
66
+ via?: string;
67
+ /** What the action came from, e.g. `postcard_bookmark` (up to 64). */
68
+ source?: string;
69
+ /** How the item was shown, e.g. `video` (up to 32). */
70
+ presentation?: string;
71
+ }
72
+ /** One engagement signal. An open-network post has no Minds id: name it by `metadata.ref` instead. */
10
73
  export interface SignalEventInput {
11
74
  event_type: SignalEventType;
12
75
  /** The Minds post's id (a UUID). */
13
76
  post_id?: string;
14
- /** Milliseconds on screen, for `dwell` (0 to one hour). */
77
+ /** Milliseconds on screen, for `dwell` (0 to one hour; a card's counts up to five minutes). */
15
78
  dwell_ms?: number;
16
- metadata?: Record<string, unknown>;
79
+ metadata?: SignalMetadata;
17
80
  }
18
81
  /**
19
82
  * Why nothing was stored, on a 200: `no_project_scope` (the key is not bound to an app, so there is no app database
20
- * for the signal) or `project_db_unavailable` (the app database did not take it; signals are best-effort).
83
+ * for the signal), `project_db_unavailable` (the app database did not take it; signals are best-effort),
84
+ * `viewing_off` (the member turned off "Use what I watch and read to personalize", so a dwell or open is not kept),
85
+ * `rate_limited` (past the member's events a minute) or `duplicate` (`record` only: an event with this
86
+ * `client_event_id` is already stored).
21
87
  */
22
- export type SignalNotRecordedReason = 'no_project_scope' | 'project_db_unavailable';
88
+ export type SignalNotRecordedReason = 'no_project_scope' | 'project_db_unavailable' | 'viewing_off' | 'rate_limited' | 'duplicate';
89
+ /** Why the server refused one event of a batch: a metadata value of the wrong type or past its limit. */
90
+ export interface SignalRejection {
91
+ /** The event's index in the batch. */
92
+ index: number;
93
+ reason: 'invalid_metadata' | 'invalid_ref' | 'invalid_client_event_id';
94
+ /** The metadata key. */
95
+ key: string;
96
+ }
23
97
  export interface SignalBatchResult {
24
- /** Events stored. */
98
+ /** Events stored by this call. */
25
99
  recorded: number;
100
+ /**
101
+ * Events the server did not keep because the member's viewing personalization is off: dwell and opens. Views and
102
+ * explicit choices are always kept. Absent when none were left out.
103
+ */
104
+ withheld?: number;
26
105
  reason?: SignalNotRecordedReason;
106
+ /** Events already stored (the same `client_event_id`, in this batch or an earlier one): stored once. */
107
+ duplicates?: number;
108
+ /** Events past the member's rate (600 a minute by default): not stored. */
109
+ limited?: number;
110
+ /** Events refused by the contract: not stored. The rest of the batch is. */
111
+ rejected?: number;
112
+ /** The first ten refused events, and why. */
113
+ rejections?: SignalRejection[];
114
+ }
115
+ /** What "Clear my viewing history" did: the member's views, dwell, Watch time and opens in this app are gone. */
116
+ export interface ClearViewingHistoryResult {
117
+ cleared: true;
118
+ /** Rows deleted. */
119
+ deleted: number;
27
120
  }
28
121
  export interface SignalRecordResult {
29
122
  recorded: boolean;
@@ -44,15 +137,45 @@ export declare const SIGNAL_BATCH_MAX = 100;
44
137
  * Engagement signals (views, dwell, hides, "More like this") for the member's own ranking, under the member's
45
138
  * app-bound key (`agents:write`). They land in the app's database and feed For You. Best-effort: a signal the server
46
139
  * cannot store still answers 200 with `recorded` 0 or false and a `reason`.
140
+ *
141
+ * Only from a public place, never a private message: a `metadata` key or placement (`surface`, `feed`, `source`,
142
+ * `context`, `route`...) that names a chat, DM, inbox, encrypted or Sealed conversation, by any spelling (`chatList`,
143
+ * `dmThread`, `/messages/1`), is refused with a 400 (`private_surface`), and nothing in that request is stored. An app
144
+ * names its own public places (`{ source: 'discover' }`). In the Minds app the placements (`surface`, `feed`, `source`,
145
+ * `via`, `layoutSource`) must be ones the server lists (its feeds, Discover, Watch, stories, the pager and post cards),
146
+ * and any other key that names a place (`context`) is refused there too.
147
+ *
148
+ * That check reads the metadata as sent. The server then keeps only SignalMetadata's keys, each within its limit; a
149
+ * batch names any event it refused and stores the rest. `record` answers 400 (`invalid_signal`) for an event it refuses.
47
150
  */
48
151
  export declare class SignalsResource {
49
152
  private client;
153
+ /**
154
+ * The id each event object was sent with. An event sent again (a retry of the same request, or the app sending the
155
+ * same object after a failed flush) carries the same id, so the server stores it once. Weak: an event the app has let
156
+ * go is not held here.
157
+ */
158
+ private readonly eventIds;
50
159
  constructor(client: HttpClient);
51
160
  /**
52
161
  * Store up to SIGNAL_BATCH_MAX events in one request (POST /signals/batch). The server reads a post's newest hide or
53
162
  * unhide, so send a batch only after the one before it has landed when order matters. An empty array stores nothing.
163
+ * Each event without a `metadata.client_event_id` is sent with one (the same one each time that object is sent).
54
164
  */
55
165
  batch(events: SignalEventInput[], options?: SignalSendOptions): Promise<SingleResponse<SignalBatchResult>>;
56
166
  /** Store one event (POST /signals/post). Prefer `batch`; this is for an API that predates it (404 there). */
57
167
  record(event: SignalEventInput, options?: SignalSendOptions): Promise<SingleResponse<SignalRecordResult>>;
168
+ /**
169
+ * "Clear my viewing history" (DELETE /signals/history): the member's views, dwell, Watch time and opens in this app
170
+ * are deleted, and what was built from them is rebuilt. Their explicit choices (hides, "More like this", saves,
171
+ * shares, votes) stay. A delete that could not be done is an error (503), never a success.
172
+ */
173
+ clearViewingHistory(): Promise<SingleResponse<ClearViewingHistoryResult>>;
174
+ /** The event as sent: the caller's object is never changed. */
175
+ private withEventId;
58
176
  }
177
+ /**
178
+ * A fresh event id the server's grammar takes: a random UUID where the platform has one, otherwise the time and about
179
+ * 104 random bits (React Native's Hermes has no crypto.randomUUID). Unique per member is all it needs to be.
180
+ */
181
+ export declare function newClientEventId(): string;
@@ -4,23 +4,79 @@ export const SIGNAL_BATCH_MAX = 100;
4
4
  * Engagement signals (views, dwell, hides, "More like this") for the member's own ranking, under the member's
5
5
  * app-bound key (`agents:write`). They land in the app's database and feed For You. Best-effort: a signal the server
6
6
  * cannot store still answers 200 with `recorded` 0 or false and a `reason`.
7
+ *
8
+ * Only from a public place, never a private message: a `metadata` key or placement (`surface`, `feed`, `source`,
9
+ * `context`, `route`...) that names a chat, DM, inbox, encrypted or Sealed conversation, by any spelling (`chatList`,
10
+ * `dmThread`, `/messages/1`), is refused with a 400 (`private_surface`), and nothing in that request is stored. An app
11
+ * names its own public places (`{ source: 'discover' }`). In the Minds app the placements (`surface`, `feed`, `source`,
12
+ * `via`, `layoutSource`) must be ones the server lists (its feeds, Discover, Watch, stories, the pager and post cards),
13
+ * and any other key that names a place (`context`) is refused there too.
14
+ *
15
+ * That check reads the metadata as sent. The server then keeps only SignalMetadata's keys, each within its limit; a
16
+ * batch names any event it refused and stores the rest. `record` answers 400 (`invalid_signal`) for an event it refuses.
7
17
  */
8
18
  export class SignalsResource {
9
19
  client;
20
+ /**
21
+ * The id each event object was sent with. An event sent again (a retry of the same request, or the app sending the
22
+ * same object after a failed flush) carries the same id, so the server stores it once. Weak: an event the app has let
23
+ * go is not held here.
24
+ */
25
+ eventIds = new WeakMap();
10
26
  constructor(client) {
11
27
  this.client = client;
12
28
  }
13
29
  /**
14
30
  * Store up to SIGNAL_BATCH_MAX events in one request (POST /signals/batch). The server reads a post's newest hide or
15
31
  * unhide, so send a batch only after the one before it has landed when order matters. An empty array stores nothing.
32
+ * Each event without a `metadata.client_event_id` is sent with one (the same one each time that object is sent).
16
33
  */
17
34
  batch(events, options) {
18
- return this.client.post('/signals/batch', { events }, sendOptions(options));
35
+ return this.client.post('/signals/batch', { events: events.map((event) => this.withEventId(event)) }, sendOptions(options));
19
36
  }
20
37
  /** Store one event (POST /signals/post). Prefer `batch`; this is for an API that predates it (404 there). */
21
38
  record(event, options) {
22
- return this.client.post('/signals/post', event, sendOptions(options));
39
+ return this.client.post('/signals/post', this.withEventId(event), sendOptions(options));
40
+ }
41
+ /**
42
+ * "Clear my viewing history" (DELETE /signals/history): the member's views, dwell, Watch time and opens in this app
43
+ * are deleted, and what was built from them is rebuilt. Their explicit choices (hides, "More like this", saves,
44
+ * shares, votes) stay. A delete that could not be done is an error (503), never a success.
45
+ */
46
+ clearViewingHistory() {
47
+ return this.client.delete('/signals/history');
48
+ }
49
+ /** The event as sent: the caller's object is never changed. */
50
+ withEventId(event) {
51
+ if (!event || typeof event !== 'object')
52
+ return event;
53
+ const given = event.metadata?.client_event_id;
54
+ if (typeof given === 'string' && given)
55
+ return event;
56
+ let id = this.eventIds.get(event);
57
+ if (!id) {
58
+ id = newClientEventId();
59
+ this.eventIds.set(event, id);
60
+ }
61
+ return { ...event, metadata: { ...event.metadata, client_event_id: id } };
62
+ }
63
+ }
64
+ /**
65
+ * A fresh event id the server's grammar takes: a random UUID where the platform has one, otherwise the time and about
66
+ * 104 random bits (React Native's Hermes has no crypto.randomUUID). Unique per member is all it needs to be.
67
+ */
68
+ export function newClientEventId() {
69
+ const cryptoApi = globalThis.crypto;
70
+ if (typeof cryptoApi?.randomUUID === 'function') {
71
+ try {
72
+ return cryptoApi.randomUUID();
73
+ }
74
+ catch {
75
+ // A non-secure context can refuse it: fall through.
76
+ }
23
77
  }
78
+ const random = () => Math.floor(Math.random() * 2 ** 52).toString(36).padStart(11, '0');
79
+ return `${Date.now().toString(36)}-${random()}${random()}`;
24
80
  }
25
81
  function sendOptions(options) {
26
82
  if (!options)
@@ -0,0 +1,85 @@
1
+ import type { HttpClient } from '../client.js';
2
+ import type { SingleResponse } from '../types.js';
3
+ /**
4
+ * Where one event sits in the signed trust log, which key signed it, and whether the server's check of it holds
5
+ * (0.7.96). The public moderation log carries one per signed decision and reversal (`PublicModerationAction.trust_log`):
6
+ * never the moderator, the subject or the payload, and never a hash or a signature. The log's v1 encoding signs the
7
+ * moderator and the subject unsalted, so a published hash or signature would let anyone test guesses of who acted on
8
+ * what; `verified` is the server's verdict, not something a client re-checks.
9
+ */
10
+ export interface TrustEnvelope {
11
+ /** The chain the event is on: the network's id. */
12
+ chain: string;
13
+ seq: number;
14
+ key_id: string;
15
+ algorithm: 'ed25519';
16
+ occurred_at: string;
17
+ /** The server's own check: the signature, the hash, and the link to the event before it. */
18
+ verified: boolean;
19
+ }
20
+ /** One of the caller's own events: what they did, where it sits in the log, and the server's check of it. */
21
+ export interface OwnTrustEvent extends TrustEnvelope {
22
+ id: number;
23
+ network_id: string;
24
+ project_id: string | null;
25
+ /** e.g. `follow`, `unfollow`, `block`, `mute`, `hide`, `appeal`, `moderation_action` (a moderator's own decision). */
26
+ kind: string;
27
+ actor: {
28
+ kind: 'user' | 'agent' | 'system';
29
+ user_id: string | null;
30
+ principal_id: string | null;
31
+ };
32
+ subject: {
33
+ kind: string;
34
+ id: string | null;
35
+ key: string | null;
36
+ principal_id: string | null;
37
+ };
38
+ payload: Record<string, unknown>;
39
+ }
40
+ /** A public key that signed the log. A retired or compromised key still verifies what it signed before that instant. */
41
+ export interface TrustPublicKey {
42
+ key_id: string;
43
+ algorithm: 'ed25519';
44
+ /** base64 SPKI DER. */
45
+ public_key: string;
46
+ activated_at: string | null;
47
+ retired_at: string | null;
48
+ compromised_at: string | null;
49
+ }
50
+ export interface OwnTrustEvents {
51
+ events: OwnTrustEvent[];
52
+ /** The keys that signed this page's events. */
53
+ keys: TrustPublicKey[];
54
+ pagination: {
55
+ limit: number;
56
+ before: number | null;
57
+ has_more: boolean;
58
+ next_before: number | null;
59
+ };
60
+ }
61
+ /**
62
+ * The signed trust log (0.7.96). Both reads answer 404 (`NotFoundError`) until the operator turns on
63
+ * TRUST_LOG_PUBLIC_READS. Signed moderation decisions are on the public moderation log itself
64
+ * (`r.moderation.publicLog`, `trust_log` on each action).
65
+ */
66
+ export declare class TrustResource {
67
+ private client;
68
+ constructor(client: HttpClient);
69
+ /**
70
+ * Keyless: every public key that ever signed the log, retired and compromised ones included. Public material: pin
71
+ * them to see a rotation, or check a full row an auditor holds.
72
+ */
73
+ keys(): Promise<SingleResponse<{
74
+ keys: TrustPublicKey[];
75
+ }>>;
76
+ /**
77
+ * The caller's own events, newest first: the follows, blocks, mutes, hides and appeals they made (a moderator's own
78
+ * decisions too), each with where it sits in the log and the server's check. Never a hash or a signature. Page with
79
+ * `before: pagination.next_before`. Needs `users:read`.
80
+ */
81
+ mine(params?: {
82
+ before?: number;
83
+ limit?: number;
84
+ }): Promise<SingleResponse<OwnTrustEvents>>;
85
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The signed trust log (0.7.96). Both reads answer 404 (`NotFoundError`) until the operator turns on
3
+ * TRUST_LOG_PUBLIC_READS. Signed moderation decisions are on the public moderation log itself
4
+ * (`r.moderation.publicLog`, `trust_log` on each action).
5
+ */
6
+ export class TrustResource {
7
+ client;
8
+ constructor(client) {
9
+ this.client = client;
10
+ }
11
+ /**
12
+ * Keyless: every public key that ever signed the log, retired and compromised ones included. Public material: pin
13
+ * them to see a rotation, or check a full row an auditor holds.
14
+ */
15
+ keys() {
16
+ return this.client.get('/transparency/trust/keys');
17
+ }
18
+ /**
19
+ * The caller's own events, newest first: the follows, blocks, mutes, hides and appeals they made (a moderator's own
20
+ * decisions too), each with where it sits in the log and the server's check. Never a hash or a signature. Page with
21
+ * `before: pagination.next_before`. Needs `users:read`.
22
+ */
23
+ mine(params = {}) {
24
+ return this.client.get('/trust/me', { before: params.before, limit: params.limit });
25
+ }
26
+ }
package/dist/types.d.ts CHANGED
@@ -1651,6 +1651,12 @@ export interface ContentPreferences {
1651
1651
  * that predate it: treat absent as off.
1652
1652
  */
1653
1653
  sealed_adult?: boolean;
1654
+ /**
1655
+ * "Use what I watch and read to personalize": on by default. Off, the server stores only views (so a post the member
1656
+ * saw is not served again) and their explicit choices, and no ranking reads what they watched or read. Absent on
1657
+ * servers that predate it: treat absent as on.
1658
+ */
1659
+ personalize_from_viewing?: boolean;
1654
1660
  }
1655
1661
  export interface UpdateNotificationChannelsInput {
1656
1662
  /** Master switch: sets in-app on every kind. */
@@ -1698,6 +1704,8 @@ export interface UpdateContentPreferencesInput {
1698
1704
  * accepted.
1699
1705
  */
1700
1706
  sealed_adult?: boolean;
1707
+ /** "Use what I watch and read to personalize". `false` is the member's opt-out; `true` turns it back on. */
1708
+ personalize_from_viewing?: boolean;
1701
1709
  }
1702
1710
  export interface Session {
1703
1711
  id: string;
package/llm.md CHANGED
@@ -826,7 +826,7 @@ Methods: `list`, `get`, `create`, `delete`
826
826
  ## Platform Resources
827
827
 
828
828
  - `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
- - `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`). `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.
829
+ - `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
830
 
831
831
  ```typescript
832
832
  await r.signals.batch([{ event_type: 'view', post_id: post.id }, { event_type: 'dwell', post_id: post.id, dwell_ms: 4200 }])
@@ -866,6 +866,7 @@ await r.appSubscriptions.changeTier({ tier: 'pro', proration_date: quote.prorati
866
866
  ```
867
867
  - `r.admin` — Admin operations (user management, stats, network settings); content reports: `listReports({ status? })`, `resolveReport(id)`, `dismissReport(id)` (project-bound keys see only their app's reports)
868
868
  - `r.moderation` — Moderation actions. Staff (admin key), 0.7.74: `const { data: actions } = await r.moderation.list({ target_type: 'user', target_id: userId })` reads one target's recorded actions (`ModerationAction`: `id`, `target_type` incl. `source_item`, `action`, `label`, `reason`, `actor_id`, `actor_type`, `source_report_id`, `expires_at`, `reversed_at`, `created_at`), newest first, at most 100, inside the caller's network and a project-bound key's app; `list()` with no target reads the network's latest. Unban = reverse each ban still in force: `for (const a of actions.filter(a => a.action === 'ban' && !a.reversed_at)) await r.moderation.reverse(a.id)`; the target's state is derived again from what is still in force. `reverse` is sent once, never retried: 404 `action_not_found` (`NotFoundError`) means already reversed or not in the caller's scope; 409 `use_judgment_review` means a reviewed label override (change it with `r.judgments.review`). An older SDK has neither method. Also `apply`, `publicLog`, `mine`, `appeal`, `resolveAppeal`, and agent grants `listAgentGrants`, `grantAgent(agentId, 'shadow' | 'act')`, `revokeAgent`.
869
+ - `r.trust` — The signed trust log (0.7.96; both reads answer 404, `NotFoundError`, until the operator turns on `TRUST_LOG_PUBLIC_READS`). `r.trust.keys()` (keyless) lists every public key that ever signed it (`key_id`, `algorithm` `ed25519`, `public_key` base64 SPKI DER, `activated_at`, `retired_at`, `compromised_at`). `r.trust.mine({ before?, limit? })` (`users:read`) reads the caller's own signed events, newest first (their follows, blocks, mutes, hides, appeals, and a moderator's own decisions: `kind`, `actor`, `subject`, `payload`, `chain`, `seq`, `key_id`, `occurred_at`, and the server's check `verified`) with the `keys` that signed the page; page with `before: pagination.next_before`. On the public log, `r.moderation.publicLog(networkId)` then carries `trust_log` on each action (`status`: `verified` | `unverified` | `unsigned` | `unavailable`, and the signed `decision` and `reversal`: `chain`, `seq`, `key_id`, `algorithm`, `occurred_at`, `verified`) and on the page (`status` and the count of each). No read carries a hash or a signature (the v1 encoding signs the moderator and the subject unsalted, so one would let anyone test guesses of who acted on what): `verified` is the server's verdict.
869
870
  - `r.judgments` (0.7.71) — Jev's labels of one post. `get({ kind: 'post' | 'network_post', id })`: switch on `data.view` — `'full'` for the post's author and staff (`judgments` with answers, `safety` verdict, `viral.withheld`; staff also `reviewer_ids`, `review_note`), `'summary'` for other members (`category`, `politics` band in words with its `judgment_id`; never a probability or cell). `data.status === 'unavailable'` (returned, not thrown): hide label actions. `getPublic(projectId, { kind, id })` on an anonymous client: `view: 'category'` only. `dispute(judgmentId, { question_key: 'placement', proposed: 'cm' | … | 'not_political', note? })` (safety: `question_key: 'safety'`, author only): the author's dispute is an appeal; a repeat answers `existing: true`. Staff: `review(judgmentId, { outcome: 'uphold' } | { outcome: 'override', proposed, note?, apply_to_shared? })`, sent once (never retried): a compass override answers 202 `{ status: 'awaiting_reviewer', agreeing, needed }` until a second reviewer (or an agreeing Jev re-judge) proposes the same; a safety override to `'none'` that would free other posts throws 409 `shared_words` (`isJudgmentSharedWordsError(e)`, `e.details.shared_posts`) until resent with `apply_to_shared: true`. Budgets (60 reads/hour, 25 of one author's posts, 20 disputes/day) throw `RateLimitError` with `scope: 'action'` at once. Example: `const { data } = await r.judgments.get({ kind: 'post', id }); if (data.status === 'ok' && data.view === 'summary' && data.politics) await r.judgments.dispute(data.politics.judgment_id, { question_key: 'placement', proposed: 'not_political' })`
870
871
  - `r.inviteCodes` / `r.inviteCodesAdmin` — Invite code management
871
872
  - `r.organizationSettings` / `r.organizationSecurity` — Org configuration
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@recursiv/sdk",
3
- "version": "0.7.89",
3
+ "version": "0.7.96",
4
4
  "description": "TypeScript SDK for the Recursiv API",
5
5
  "license": "FSL-1.1-ALv2",
6
6
  "type": "module",