@recursiv/sdk 0.7.80 → 0.7.82
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 +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.js +2 -2
- package/dist/realtime.d.ts +1 -1
- package/dist/resources/chat.d.ts +73 -1
- package/dist/resources/chat.js +83 -1
- package/dist/resources/network.d.ts +6 -0
- package/dist/resources/protocols.d.ts +9 -1
- package/dist/resources/reports.d.ts +24 -0
- package/dist/resources/reports.js +12 -0
- package/dist/resources/sealed.d.ts +65 -18
- package/dist/resources/sealed.js +53 -3
- package/dist/types.d.ts +94 -2
- package/llm.md +6 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -98,7 +98,7 @@ for await (const chunk of r.agents.chatStream(agent.id, { message: 'Hello!' }))
|
|
|
98
98
|
| `r.bookmarks` | `save`, `unsave`, `posts`, `list`, `keys`, `merge` — the member's saved posts, open-network posts and articles, synced across devices; `collections`: `list`, `create`, `update`, `remove`, `add`, `removeItem`, `items`, `posts` — private named collections of saves (0.7.68, where `network.getConfig()` advertises `capabilities.bookmark_collections`) |
|
|
99
99
|
| `r.feeds` | `top` (what the member is most likely to engage with, rotated by what they saw, with open-network lanes and the lead story), `stories` (the story tail), `publicTop` (a visitor's top, keyless and edge-cacheable) |
|
|
100
100
|
| `r.communities` | `create`, `join`, `search`, `members` |
|
|
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) |
|
|
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
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) |
|
|
104
104
|
| `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` |
|
package/dist/index.d.ts
CHANGED
|
@@ -142,7 +142,7 @@ export declare class Recursiv {
|
|
|
142
142
|
readonly accounts: AccountsResource;
|
|
143
143
|
/** The member's engagement signals (views, dwell, hides, "More like this") for their own ranking. */
|
|
144
144
|
readonly signals: SignalsResource;
|
|
145
|
-
/** Sealed mode's config and transport:
|
|
145
|
+
/** Sealed mode's config and transport: targets, models, allowance, the attestation relay and the sealed send (no crypto; flag-gated). */
|
|
146
146
|
readonly sealed: SealedResource;
|
|
147
147
|
readonly realtime: RealtimeClient;
|
|
148
148
|
constructor(config?: Partial<RecursivConfig>);
|
|
@@ -165,7 +165,8 @@ export { POSTS_BATCH_MAX, PostsResource } from './resources/posts.js';
|
|
|
165
165
|
export type { CategoryOrder, CategoryOrderMeta, CategoryOrderReason, PostBatch, PostBatchItem } from './resources/posts.js';
|
|
166
166
|
export { UsersResource } from './resources/users.js';
|
|
167
167
|
export { CommunitiesResource } from './resources/communities.js';
|
|
168
|
-
export { ChatResource } from './resources/chat.js';
|
|
168
|
+
export { ChatResource, CHAT_ATTACHMENT_REF_PREFIX, chatAttachmentKeysIn, chatAttachmentRefsIn } from './resources/chat.js';
|
|
169
|
+
export type { ChatAttachmentRef, ChatAttachmentUploadInput, ChatAttachmentUploadUrl, ChatAttachmentUploadUrlInput, SignedChatAttachment, } from './resources/chat.js';
|
|
169
170
|
export { E2eeResource } from './resources/e2ee.js';
|
|
170
171
|
export type { E2eeAccountReset, E2eeDevice, E2eeDevicesRealtimeEvent, E2eeEnvelope, E2eeEvent, E2eeHasDependentsDetails, E2eeKeyPackageStatus, E2eeLink, E2eeLinkCancelledBy, E2eeLinkProgress, E2eeLinkRealtimeEvent, E2eeLinkState, E2eeObjectDownload, E2eeObjectUpload, E2eeRealtimeEvent, E2eeResetCodeSent, E2eeResetErrorCode, E2eeRevokeResponse, E2eeStartRequest, E2eeStatus, E2eeStatusReason, } from './resources/e2ee.js';
|
|
171
172
|
export { ProjectsResource } from './resources/projects.js';
|
|
@@ -207,7 +208,7 @@ export type { AppSubscriptionBoostCredit, AppSubscriptionCheckoutResult, AppSubs
|
|
|
207
208
|
export type { CuratorSource, CuratorPrompt, CuratorRunInput, CuratorRunResult, CuratorTodayEdition, CuratorTodayExcerpt, CuratorTodayLeadImage, CuratorTodayParams, CuratorTodayRadar, CuratorTodayReaction, CuratorTodayResponse, CuratorTodaySource, CuratorTodayStory, CuratorTodayDiscussion, CuratorStoryDiscussion, CuratorStoryLaneOutcome, CuratorStoryMindsPost, CuratorStoryPostItem, CuratorStoryPostPagesOptions, CuratorStoryPostsPage, CuratorStoryPostsParams, CuratorStoryReader, CuratorStorySocialPost, CuratorStoryVolumeLanes, CuratorStoryVolumeMeta, StoryPostNetwork, StoryPostPool, StoryPostSort, ForYouMeta, ForYouParams, ForYouSurface, ForYouWorldBlock, ForYouWorldBlockType, ForYouWorldCategoryShortcut, ForYouWorldItem, ForYouWorldMeta, ForYouWorldParams, ForYouWorldPart, ForYouWorldResponse, ForYouWorldStory, ForYouWorldStoryTarget, ForYouWorldViral, } from './resources/curator.js';
|
|
208
209
|
export { STORY_POOL_MAX_POSTS, STORY_POST_NETWORKS, STORY_POSTS_PAGE_MAX } from './resources/curator.js';
|
|
209
210
|
export { ReportsResource } from './resources/reports.js';
|
|
210
|
-
export type { ReportTargetType, ReportStatus, CreateReportInput, ReportRecord, ResolveReportInput, } from './resources/reports.js';
|
|
211
|
+
export type { ReportTargetType, ReportStatus, CreateReportInput, ReportRecord, ResolveReportInput, ReportedMessageAttachment, ReportedMessageAttachments, } from './resources/reports.js';
|
|
211
212
|
export { JudgmentsResource, isJudgmentSharedWordsError } from './resources/judgments.js';
|
|
212
213
|
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';
|
|
213
214
|
export { ModerationResource } from './resources/moderation.js';
|
|
@@ -223,7 +224,7 @@ export { AccountsResource } from './resources/accounts.js';
|
|
|
223
224
|
export { SIGNAL_BATCH_MAX, SignalsResource } from './resources/signals.js';
|
|
224
225
|
export type { SignalBatchResult, SignalEventInput, SignalEventType, SignalNotRecordedReason, SignalRecordResult, SignalSendOptions, } from './resources/signals.js';
|
|
225
226
|
export { SEALED_ALLOWANCE_REMAINING_HEADER, SEALED_REFUSAL_HEADER, SealedResource, SealedTransportError, } from './resources/sealed.js';
|
|
226
|
-
export type { SealedAllowance, SealedConfig, SealedSendInput, SealedTransportErrorCode } from './resources/sealed.js';
|
|
227
|
+
export type { SealedAllowance, SealedAttestationInput, SealedConfig, SealedModel, SealedSendInput, SealedTarget, SealedTargetKind, SealedTransportErrorCode, } from './resources/sealed.js';
|
|
227
228
|
export type { SiblingAccount, SiblingAccountsResponse, SwitchAccountResult, } from './resources/accounts.js';
|
|
228
229
|
export type { LinkPreview, LinkPreviewResponse, } from './resources/link-preview.js';
|
|
229
230
|
export type { AgentCapabilityConsentRequirement, AgentCapabilityDataSensitivity, AgentCapabilityDomain, AgentCapabilityExportBoundary, AgentCapabilityRetentionClass, AgentCapabilitySubdomain, AgentCapabilitySubject, AgentCapabilityTrustTier, AgentToolAudience, AgentToolScope, BrowserUseRunTaskInput, ProjectBrainSettings, ProjectToolBundleClassification, ProjectToolBundleKey, ProjectToolPermission, ProjectToolPermissionOverrides, UpdateProjectSettingsInput, } from './resources/project-brain.js';
|
package/dist/index.js
CHANGED
|
@@ -143,7 +143,7 @@ export class Recursiv {
|
|
|
143
143
|
accounts;
|
|
144
144
|
/** The member's engagement signals (views, dwell, hides, "More like this") for their own ranking. */
|
|
145
145
|
signals;
|
|
146
|
-
/** Sealed mode's config and transport:
|
|
146
|
+
/** Sealed mode's config and transport: targets, models, allowance, the attestation relay and the sealed send (no crypto; flag-gated). */
|
|
147
147
|
sealed;
|
|
148
148
|
realtime;
|
|
149
149
|
constructor(config) {
|
|
@@ -236,7 +236,7 @@ export { AuthResource } from './resources/auth.js';
|
|
|
236
236
|
export { POSTS_BATCH_MAX, PostsResource } from './resources/posts.js';
|
|
237
237
|
export { UsersResource } from './resources/users.js';
|
|
238
238
|
export { CommunitiesResource } from './resources/communities.js';
|
|
239
|
-
export { ChatResource } from './resources/chat.js';
|
|
239
|
+
export { ChatResource, CHAT_ATTACHMENT_REF_PREFIX, chatAttachmentKeysIn, chatAttachmentRefsIn } from './resources/chat.js';
|
|
240
240
|
export { E2eeResource } from './resources/e2ee.js';
|
|
241
241
|
export { ProjectsResource } from './resources/projects.js';
|
|
242
242
|
export { AgentsResource, SUPPORTED_MODELS, AGENT_DIRECTORY_SORTS, AGENT_DIRECTORY_MAX_LIMIT } from './resources/agents.js';
|
package/dist/realtime.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ export interface ChatMessageEvent {
|
|
|
14
14
|
isAi?: boolean;
|
|
15
15
|
};
|
|
16
16
|
createdAt: string;
|
|
17
|
-
/** The message's metadata as stored; `kind: 'e2ee_start_request'` is an encryption request (MessageMetadata). */
|
|
17
|
+
/** The message's metadata as stored; `kind: 'e2ee_start_request'` is an encryption request, and an agent's reply carries `ai`, its AiTurnStamp (MessageMetadata). */
|
|
18
18
|
metadata?: Record<string, unknown>;
|
|
19
19
|
}
|
|
20
20
|
export interface ChatMessageEditEvent {
|
package/dist/resources/chat.d.ts
CHANGED
|
@@ -116,7 +116,11 @@ export declare class ChatResource {
|
|
|
116
116
|
type: string;
|
|
117
117
|
};
|
|
118
118
|
}>>;
|
|
119
|
-
/**
|
|
119
|
+
/**
|
|
120
|
+
* Edit a message's words (only the sender can edit). Keep each `chat-attachment:` reference it carries as it is: the
|
|
121
|
+
* server refuses an edit that drops, adds or swaps one (400 `attachment_refs_changed`, 0.7.82). To take an attachment
|
|
122
|
+
* back, delete the message.
|
|
123
|
+
*/
|
|
120
124
|
editMessage(messageId: string, input: {
|
|
121
125
|
content: string;
|
|
122
126
|
}): Promise<SingleResponse<Message>>;
|
|
@@ -130,4 +134,72 @@ export declare class ChatResource {
|
|
|
130
134
|
unreadCount(conversationId: string): Promise<SingleResponse<{
|
|
131
135
|
unread_count: number;
|
|
132
136
|
}>>;
|
|
137
|
+
/**
|
|
138
|
+
* A PUT URL for one photo or voice note to send in a plaintext chat (0.7.82). With the server's private chat
|
|
139
|
+
* storage on, `private` is true, the URL writes into a private bucket and `ref` is `chat-attachment:<key>`, the text
|
|
140
|
+
* to send; with it off, `ref` is the upload's public URL. An end-to-end encrypted chat answers 409
|
|
141
|
+
* `conversation_encrypted`: its attachments go through `r.e2ee`. Most callers want `uploadAttachment`.
|
|
142
|
+
*/
|
|
143
|
+
attachmentUploadUrl(conversationId: string, input: ChatAttachmentUploadUrlInput): Promise<SingleResponse<ChatAttachmentUploadUrl>>;
|
|
144
|
+
/**
|
|
145
|
+
* Upload one photo or voice note for a plaintext chat end to end and resolve the text to send for it (0.7.82): read
|
|
146
|
+
* the bytes, get the URL, PUT them with the matching Content-Type, and reject a failed transfer. Send `ref` as the
|
|
147
|
+
* message (or within it): a private reference renders through `signAttachments`, a public URL as before.
|
|
148
|
+
*/
|
|
149
|
+
uploadAttachment(conversationId: string, input: ChatAttachmentUploadInput): Promise<ChatAttachmentUploadUrl>;
|
|
150
|
+
/**
|
|
151
|
+
* Short-lived URLs for a conversation's private attachments (0.7.82), at most 50 keys a call (the keys in its
|
|
152
|
+
* messages' `chat-attachment:` references; `chatAttachmentKeysIn` finds them). The server signs a key only for a
|
|
153
|
+
* member of the conversation, and only while a message its sender wrote there still carries it; any other key
|
|
154
|
+
* comes back with `url: null`. A URL works for about five minutes and is served `private, no-store`: ask again
|
|
155
|
+
* rather than keeping it.
|
|
156
|
+
*/
|
|
157
|
+
signAttachments(conversationId: string, keys: string[]): Promise<SingleResponse<{
|
|
158
|
+
attachments: SignedChatAttachment[];
|
|
159
|
+
}>>;
|
|
160
|
+
}
|
|
161
|
+
export interface ChatAttachmentUploadUrlInput {
|
|
162
|
+
/** image/jpeg, image/png, image/webp, image/gif, audio/mpeg, audio/mp4, audio/wav, audio/ogg or audio/webm. */
|
|
163
|
+
content_type: string;
|
|
164
|
+
/** The exact byte count the PUT will send (signed into the URL; at most 50 MB). */
|
|
165
|
+
content_length: number;
|
|
166
|
+
}
|
|
167
|
+
export interface ChatAttachmentUploadUrl {
|
|
168
|
+
/** True when the attachment goes to the server's private chat storage. */
|
|
169
|
+
private: boolean;
|
|
170
|
+
/** PUT the bytes here with the same Content-Type. */
|
|
171
|
+
upload_url: string;
|
|
172
|
+
key: string;
|
|
173
|
+
/** The text to send for this attachment: `chat-attachment:<key>` when private, else the public URL. */
|
|
174
|
+
ref: string;
|
|
175
|
+
}
|
|
176
|
+
export interface ChatAttachmentUploadInput {
|
|
177
|
+
/** Caller-held bytes, such as a browser File or React Native Blob. */
|
|
178
|
+
blob?: Blob;
|
|
179
|
+
/** A local URI that the SDK should read into a Blob first. */
|
|
180
|
+
uri?: string;
|
|
181
|
+
/** Defaults to blob.type. */
|
|
182
|
+
contentType?: string;
|
|
183
|
+
/** Optional transport override for React Native adapters and focused tests. */
|
|
184
|
+
fetchImpl?: typeof fetch;
|
|
185
|
+
}
|
|
186
|
+
export interface SignedChatAttachment {
|
|
187
|
+
key: string;
|
|
188
|
+
/** A GET URL valid until `expires_at`, or null when this caller may not open the key. */
|
|
189
|
+
url: string | null;
|
|
190
|
+
expires_at: string | null;
|
|
191
|
+
}
|
|
192
|
+
/** The text a message carries before a private chat attachment's key. */
|
|
193
|
+
export declare const CHAT_ATTACHMENT_REF_PREFIX = "chat-attachment:";
|
|
194
|
+
/** One private attachment a message's text refers to. */
|
|
195
|
+
export interface ChatAttachmentRef {
|
|
196
|
+
/** The whole reference as it appears in the text (`chat-attachment:<key>`). */
|
|
197
|
+
ref: string;
|
|
198
|
+
/** The key to pass to `signAttachments`. */
|
|
199
|
+
key: string;
|
|
200
|
+
kind: 'image' | 'audio';
|
|
133
201
|
}
|
|
202
|
+
/** The private attachments a message's text refers to, in order, each once (0.7.82). */
|
|
203
|
+
export declare function chatAttachmentRefsIn(content: string | null | undefined): ChatAttachmentRef[];
|
|
204
|
+
/** The private attachment keys a message's text refers to, for `signAttachments` (0.7.82). */
|
|
205
|
+
export declare function chatAttachmentKeysIn(content: string | null | undefined): string[];
|
package/dist/resources/chat.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { RecursivError } from '../errors.js';
|
|
1
2
|
export class ChatResource {
|
|
2
3
|
client;
|
|
3
4
|
constructor(client) {
|
|
@@ -127,7 +128,11 @@ export class ChatResource {
|
|
|
127
128
|
reactToMessage(messageId, input) {
|
|
128
129
|
return this.client.post(`/chat/messages/${messageId}/react`, input);
|
|
129
130
|
}
|
|
130
|
-
/**
|
|
131
|
+
/**
|
|
132
|
+
* Edit a message's words (only the sender can edit). Keep each `chat-attachment:` reference it carries as it is: the
|
|
133
|
+
* server refuses an edit that drops, adds or swaps one (400 `attachment_refs_changed`, 0.7.82). To take an attachment
|
|
134
|
+
* back, delete the message.
|
|
135
|
+
*/
|
|
131
136
|
editMessage(messageId, input) {
|
|
132
137
|
return this.client.put(`/chat/messages/${messageId}`, input);
|
|
133
138
|
}
|
|
@@ -143,4 +148,81 @@ export class ChatResource {
|
|
|
143
148
|
unreadCount(conversationId) {
|
|
144
149
|
return this.client.get('/chat/unread-count', { conversation_id: conversationId });
|
|
145
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* A PUT URL for one photo or voice note to send in a plaintext chat (0.7.82). With the server's private chat
|
|
153
|
+
* storage on, `private` is true, the URL writes into a private bucket and `ref` is `chat-attachment:<key>`, the text
|
|
154
|
+
* to send; with it off, `ref` is the upload's public URL. An end-to-end encrypted chat answers 409
|
|
155
|
+
* `conversation_encrypted`: its attachments go through `r.e2ee`. Most callers want `uploadAttachment`.
|
|
156
|
+
*/
|
|
157
|
+
attachmentUploadUrl(conversationId, input) {
|
|
158
|
+
return this.client.post(`/chat/conversations/${conversationId}/attachments/upload-url`, input);
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Upload one photo or voice note for a plaintext chat end to end and resolve the text to send for it (0.7.82): read
|
|
162
|
+
* the bytes, get the URL, PUT them with the matching Content-Type, and reject a failed transfer. Send `ref` as the
|
|
163
|
+
* message (or within it): a private reference renders through `signAttachments`, a public URL as before.
|
|
164
|
+
*/
|
|
165
|
+
async uploadAttachment(conversationId, input) {
|
|
166
|
+
const hasBlob = input.blob !== undefined;
|
|
167
|
+
const hasUri = typeof input.uri === 'string' && input.uri.length > 0;
|
|
168
|
+
if (hasBlob === hasUri)
|
|
169
|
+
throw attachmentError(0, 'Provide exactly one of blob or uri.', 'invalid_upload_source');
|
|
170
|
+
const doFetch = input.fetchImpl ?? fetch;
|
|
171
|
+
let blob = input.blob;
|
|
172
|
+
if (!blob && input.uri) {
|
|
173
|
+
const source = await doFetch(input.uri);
|
|
174
|
+
if (!source.ok)
|
|
175
|
+
throw attachmentError(source.status, `Could not read upload source (HTTP ${source.status}).`, 'upload_source_failed');
|
|
176
|
+
blob = await source.blob();
|
|
177
|
+
}
|
|
178
|
+
if (!blob)
|
|
179
|
+
throw attachmentError(0, 'Upload source did not produce any bytes.', 'empty_upload_source');
|
|
180
|
+
const contentType = input.contentType?.trim() || blob.type?.trim();
|
|
181
|
+
if (!contentType)
|
|
182
|
+
throw attachmentError(0, 'A content type is required for chat attachments.', 'missing_content_type');
|
|
183
|
+
const minted = await this.attachmentUploadUrl(conversationId, { content_type: contentType, content_length: blob.size });
|
|
184
|
+
if (!minted?.data?.upload_url || !minted.data.ref) {
|
|
185
|
+
throw attachmentError(0, 'The API did not return a chat attachment upload URL.', 'missing_upload_url');
|
|
186
|
+
}
|
|
187
|
+
const response = await doFetch(minted.data.upload_url, { method: 'PUT', body: blob, headers: { 'Content-Type': contentType } });
|
|
188
|
+
if (!response.ok)
|
|
189
|
+
throw attachmentError(response.status, `Chat attachment upload failed (HTTP ${response.status}).`, 'chat_attachment_upload_failed');
|
|
190
|
+
return minted.data;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Short-lived URLs for a conversation's private attachments (0.7.82), at most 50 keys a call (the keys in its
|
|
194
|
+
* messages' `chat-attachment:` references; `chatAttachmentKeysIn` finds them). The server signs a key only for a
|
|
195
|
+
* member of the conversation, and only while a message its sender wrote there still carries it; any other key
|
|
196
|
+
* comes back with `url: null`. A URL works for about five minutes and is served `private, no-store`: ask again
|
|
197
|
+
* rather than keeping it.
|
|
198
|
+
*/
|
|
199
|
+
signAttachments(conversationId, keys) {
|
|
200
|
+
return this.client.post(`/chat/conversations/${conversationId}/attachments/sign`, { keys });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function attachmentError(status, message, code) {
|
|
204
|
+
return new RecursivError(status, { type: 'upload_error', message, code });
|
|
205
|
+
}
|
|
206
|
+
/** The text a message carries before a private chat attachment's key. */
|
|
207
|
+
export const CHAT_ATTACHMENT_REF_PREFIX = 'chat-attachment:';
|
|
208
|
+
const CHAT_ATTACHMENT_REF_RE = /chat-attachment:(chat\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.([a-z0-9]{2,5}))/g;
|
|
209
|
+
/** The extensions the server mints a private attachment key with (and jpeg), as it parses one; anything else is text. */
|
|
210
|
+
const IMAGE_EXTENSIONS = new Set(['jpg', 'jpeg', 'png', 'webp', 'gif']);
|
|
211
|
+
const AUDIO_EXTENSIONS = new Set(['mp3', 'm4a', 'wav', 'ogg', 'weba']);
|
|
212
|
+
/** The private attachments a message's text refers to, in order, each once (0.7.82). */
|
|
213
|
+
export function chatAttachmentRefsIn(content) {
|
|
214
|
+
if (!content?.includes(CHAT_ATTACHMENT_REF_PREFIX))
|
|
215
|
+
return [];
|
|
216
|
+
const found = new Map();
|
|
217
|
+
for (const match of content.matchAll(CHAT_ATTACHMENT_REF_RE)) {
|
|
218
|
+
const extension = match[2];
|
|
219
|
+
const kind = AUDIO_EXTENSIONS.has(extension) ? 'audio' : IMAGE_EXTENSIONS.has(extension) ? 'image' : null;
|
|
220
|
+
if (kind && !found.has(match[1]))
|
|
221
|
+
found.set(match[1], { ref: match[0], key: match[1], kind });
|
|
222
|
+
}
|
|
223
|
+
return [...found.values()];
|
|
224
|
+
}
|
|
225
|
+
/** The private attachment keys a message's text refers to, for `signAttachments` (0.7.82). */
|
|
226
|
+
export function chatAttachmentKeysIn(content) {
|
|
227
|
+
return chatAttachmentRefsIn(content).map((attachment) => attachment.key);
|
|
146
228
|
}
|
|
@@ -11,10 +11,16 @@ interface NetworkConfig {
|
|
|
11
11
|
* `bookmark_collections` (0.7.68; absent on older servers): this server serves bookmark collections
|
|
12
12
|
* (`bookmarks.collections`). Show collections only when it is true, and still treat a 503
|
|
13
13
|
* `lists_unavailable` as off.
|
|
14
|
+
*
|
|
15
|
+
* `child_safety_report_category` (absent on older servers): this server files the child-safety report category's
|
|
16
|
+
* sub-types (`child_safety:minor_in_danger`, `:sexualised_minor`, `:grooming_enticement`, `:trafficking`) as
|
|
17
|
+
* urgent: top severity, an immediate staff alert, decided by a person. Offer them only when it is true; otherwise
|
|
18
|
+
* report child sexual abuse material as `csam`, which every server files as urgent.
|
|
14
19
|
*/
|
|
15
20
|
capabilities: {
|
|
16
21
|
ws_token: boolean;
|
|
17
22
|
bookmark_collections?: boolean;
|
|
23
|
+
child_safety_report_category?: boolean;
|
|
18
24
|
};
|
|
19
25
|
auth_methods: string;
|
|
20
26
|
default_template_name: string | null;
|
|
@@ -1721,8 +1721,16 @@ export interface SocialConversation {
|
|
|
1721
1721
|
focus: SocialPost;
|
|
1722
1722
|
/** Oldest available ancestor first, ending with the immediate parent. */
|
|
1723
1723
|
ancestors: SocialPost[];
|
|
1724
|
-
/** Available direct replies to focus, not all descendants. */
|
|
1724
|
+
/** Available direct replies to focus, not all descendants (from 0.7.81, without `thread`'s first part). */
|
|
1725
1725
|
replies: SocialPost[];
|
|
1726
|
+
/**
|
|
1727
|
+
* The focus author's own thread below the post, oldest first (0.7.81): a "1/" post's 2/, 3/, 4/ ..., at each level
|
|
1728
|
+
* the earliest reply by the same author, at most 20 parts. Its first part is a direct reply, served here and not in
|
|
1729
|
+
* `replies`, so render `focus`, then `thread`, then `replies`. Each part passes the same app moderation, member
|
|
1730
|
+
* controls and network labels as a reply. It stops at a part this reader does not show (`limited`). Empty when the
|
|
1731
|
+
* author wrote none and on a read of the post alone (`replies: 'none'`); absent from APIs before 0.7.81.
|
|
1732
|
+
*/
|
|
1733
|
+
thread?: SocialPost[];
|
|
1726
1734
|
coverage: 'public_provider_window';
|
|
1727
1735
|
limited: boolean;
|
|
1728
1736
|
/**
|
|
@@ -57,4 +57,28 @@ export declare class ReportsResource {
|
|
|
57
57
|
dismiss(reportId: string): Promise<SingleResponse<{
|
|
58
58
|
success: true;
|
|
59
59
|
}>>;
|
|
60
|
+
/**
|
|
61
|
+
* Short-lived URLs for the private chat photos and voice notes a reported message carries (0.7.82), for the staff
|
|
62
|
+
* person reviewing the report. A live admin's own key only: an AI account or an agent key gets 403
|
|
63
|
+
* `human_required`. Only the reported message's own attachments are signed; each URL works for about five minutes
|
|
64
|
+
* and is served `private, no-store`, so ask again rather than keeping it. A report of anything but a message is
|
|
65
|
+
* 400 `not_a_message_report`. Only a recipient's report opens anything: a report you filed yourself is 403
|
|
66
|
+
* `own_report`, and one not filed by a person in that chat (other than the message's sender) is 403
|
|
67
|
+
* `reporter_not_a_recipient`.
|
|
68
|
+
*/
|
|
69
|
+
signAttachments(reportId: string): Promise<SingleResponse<ReportedMessageAttachments>>;
|
|
70
|
+
}
|
|
71
|
+
export interface ReportedMessageAttachment {
|
|
72
|
+
key: string;
|
|
73
|
+
kind: 'image' | 'audio';
|
|
74
|
+
content_type: string;
|
|
75
|
+
/** A GET URL valid until `expires_at`. */
|
|
76
|
+
url: string;
|
|
77
|
+
expires_at: string;
|
|
78
|
+
}
|
|
79
|
+
export interface ReportedMessageAttachments {
|
|
80
|
+
report_id: string;
|
|
81
|
+
message_id: string;
|
|
82
|
+
conversation_id: string;
|
|
83
|
+
attachments: ReportedMessageAttachment[];
|
|
60
84
|
}
|
|
@@ -27,4 +27,16 @@ export class ReportsResource {
|
|
|
27
27
|
dismiss(reportId) {
|
|
28
28
|
return this.client.post(`/reports/${encodeURIComponent(reportId)}/dismiss`);
|
|
29
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Short-lived URLs for the private chat photos and voice notes a reported message carries (0.7.82), for the staff
|
|
32
|
+
* person reviewing the report. A live admin's own key only: an AI account or an agent key gets 403
|
|
33
|
+
* `human_required`. Only the reported message's own attachments are signed; each URL works for about five minutes
|
|
34
|
+
* and is served `private, no-store`, so ask again rather than keeping it. A report of anything but a message is
|
|
35
|
+
* 400 `not_a_message_report`. Only a recipient's report opens anything: a report you filed yourself is 403
|
|
36
|
+
* `own_report`, and one not filed by a person in that chat (other than the message's sender) is 403
|
|
37
|
+
* `reporter_not_a_recipient`.
|
|
38
|
+
*/
|
|
39
|
+
signAttachments(reportId) {
|
|
40
|
+
return this.client.post(`/reports/${encodeURIComponent(reportId)}/attachments/sign`);
|
|
41
|
+
}
|
|
30
42
|
}
|
|
@@ -1,28 +1,52 @@
|
|
|
1
1
|
import type { HttpClient } from '../client.js';
|
|
2
2
|
/**
|
|
3
|
-
* Sealed mode's config and transport: where the app sends a sealed request, which
|
|
4
|
-
* the member's allowance. No crypto lives here, so the SDK stays dependency-free: the app
|
|
5
|
-
* attestation and seals the body with Recursiv's verifier
|
|
6
|
-
* bytes. The gateway relays them to the
|
|
3
|
+
* Sealed mode's config and transport: where the app sends a sealed request, which targets and models it may use, and
|
|
4
|
+
* the member's allowance. No crypto lives here, so the SDK stays dependency-free: the app fetches a target's
|
|
5
|
+
* attestation document through the gateway (`attestation()`), verifies it and seals the body with Recursiv's verifier
|
|
6
|
+
* (packages/e2ee attest), then hands this resource the sealed bytes. The gateway relays them to the target without
|
|
7
|
+
* opening them.
|
|
7
8
|
*
|
|
8
9
|
* Off unless the API runs with SEALED_GATEWAY_ENABLED and SEALED_GATEWAY_PROJECTS names the app the key is bound to:
|
|
9
|
-
* until then `config()` and `send()` throw NotFoundError.
|
|
10
|
+
* until then `config()`, `attestation()` and `send()` throw NotFoundError.
|
|
10
11
|
*
|
|
11
12
|
* React Native: the global fetch there (whatwg-fetch) cannot stream. It has no `response.body`, so a streamed answer
|
|
12
13
|
* cannot be read as it arrives, and it posts a ReadableStream body as the text "[object ReadableStream]". Pass a fetch
|
|
13
14
|
* that streams, such as expo/fetch's, as `send({ fetch })` for streamed answers or a ReadableStream body.
|
|
14
15
|
*/
|
|
16
|
+
/**
|
|
17
|
+
* What a target is, which decides how the app attests and seals to it. `router`: Tinfoil's router, which opens the
|
|
18
|
+
* request inside its enclave and passes it to a model server it picks. The only kind today. A later kind (Tinfoil's
|
|
19
|
+
* gateway that cannot open bodies, sealing to one model server) may be added: an app must skip a target whose kind it
|
|
20
|
+
* does not know rather than seal to it.
|
|
21
|
+
*/
|
|
22
|
+
export type SealedTargetKind = 'router';
|
|
23
|
+
/** A server the gateway forwards to. */
|
|
24
|
+
export interface SealedTarget {
|
|
25
|
+
/** Exact origin, e.g. `https://router-0.tinfoil.sh`. */
|
|
26
|
+
origin: string;
|
|
27
|
+
kind: SealedTargetKind;
|
|
28
|
+
}
|
|
29
|
+
/** A model the app offers in Sealed. */
|
|
30
|
+
export interface SealedModel {
|
|
31
|
+
/** e.g. `gemma4-31b`. */
|
|
32
|
+
id: string;
|
|
33
|
+
/**
|
|
34
|
+
* The targets that serve this model, a subset of `SealedConfig.targets`. The app attests one of these, seals a
|
|
35
|
+
* request for this model to the key that attestation proves, and names it in `send()`. Lists differ between models
|
|
36
|
+
* when one has moved to another target, so read it per model.
|
|
37
|
+
*/
|
|
38
|
+
targets: SealedTarget[];
|
|
39
|
+
}
|
|
15
40
|
/** What `config()` returns. */
|
|
16
41
|
export interface SealedConfig {
|
|
17
42
|
/** The chat route, relative to the API base: `/sealed/v1/chat/completions`. */
|
|
18
43
|
chat_completions_path: string;
|
|
19
|
-
/**
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
models: string[];
|
|
44
|
+
/** The attestation relay, relative to the API base: `/sealed/attestation`. */
|
|
45
|
+
attestation_path: string;
|
|
46
|
+
/** Every target the gateway forwards to, chat and attestation alike. */
|
|
47
|
+
targets: SealedTarget[];
|
|
48
|
+
/** The models the app offers in Sealed, each with the targets that serve it. */
|
|
49
|
+
models: SealedModel[];
|
|
26
50
|
/** The attestation policy version the app's verifier must pin; an older pinned policy means the app needs updating. */
|
|
27
51
|
verifier_policy_version: number;
|
|
28
52
|
allowance: SealedAllowance;
|
|
@@ -35,7 +59,7 @@ export interface SealedAllowance {
|
|
|
35
59
|
max_request_bytes: number;
|
|
36
60
|
}
|
|
37
61
|
export interface SealedSendInput {
|
|
38
|
-
/** The router the app verified and sealed to:
|
|
62
|
+
/** The router the app verified and sealed to: the origin of a `router` target from `config()`, exactly. */
|
|
39
63
|
enclaveOrigin: string;
|
|
40
64
|
/** The EHBP encapsulated key from sealing the body: 64 hex characters. */
|
|
41
65
|
encapsulatedKey: string;
|
|
@@ -43,8 +67,8 @@ export interface SealedSendInput {
|
|
|
43
67
|
body: Uint8Array | ArrayBuffer | ReadableStream<Uint8Array>;
|
|
44
68
|
/** Ask for a streamed (server-sent events) answer. */
|
|
45
69
|
stream?: boolean;
|
|
46
|
-
/** The
|
|
47
|
-
|
|
70
|
+
/** The targets from a config already read. When omitted, `send()` reads the config first. */
|
|
71
|
+
targets?: readonly SealedTarget[];
|
|
48
72
|
signal?: AbortSignal;
|
|
49
73
|
/**
|
|
50
74
|
* The fetch to send with; the global fetch when omitted. On React Native pass one that streams (expo/fetch's): the
|
|
@@ -52,12 +76,23 @@ export interface SealedSendInput {
|
|
|
52
76
|
*/
|
|
53
77
|
fetch?: typeof fetch;
|
|
54
78
|
}
|
|
55
|
-
|
|
79
|
+
export interface SealedAttestationInput {
|
|
80
|
+
/** The target whose document to fetch: the origin of one of `config().targets`, exactly. */
|
|
81
|
+
target: string;
|
|
82
|
+
/** The verifier's fresh challenge: 32 bytes as 64 hex characters. The document must answer it. */
|
|
83
|
+
nonce: string;
|
|
84
|
+
/** The targets from a config already read. When omitted, `attestation()` reads the config first. */
|
|
85
|
+
targets?: readonly SealedTarget[];
|
|
86
|
+
signal?: AbortSignal;
|
|
87
|
+
}
|
|
88
|
+
/** Why `send()` or `attestation()` refused before or after the request, with nothing shown as the model's words. */
|
|
56
89
|
export type SealedTransportErrorCode =
|
|
57
|
-
/** The router is not one the gateway lists: nothing was sent. */
|
|
90
|
+
/** The router or target is not one the gateway lists: nothing was sent. */
|
|
58
91
|
'enclave_not_allowed'
|
|
59
92
|
/** The encapsulated key is not 32 bytes of hex: nothing was sent. */
|
|
60
93
|
| 'bad_encapsulated_key'
|
|
94
|
+
/** The attestation nonce is not 32 bytes of hex: nothing was sent. */
|
|
95
|
+
| 'bad_nonce'
|
|
61
96
|
/** A 2xx answer without a valid Ehbp-Response-Nonce: its body was discarded unread. */
|
|
62
97
|
| 'unsealed_answer';
|
|
63
98
|
export declare class SealedTransportError extends Error {
|
|
@@ -74,8 +109,20 @@ export declare const SEALED_ALLOWANCE_REMAINING_HEADER = "x-sealed-allowance-rem
|
|
|
74
109
|
export declare class SealedResource {
|
|
75
110
|
private client;
|
|
76
111
|
constructor(client: HttpClient);
|
|
77
|
-
/** The
|
|
112
|
+
/** The targets, models, verifier policy version and allowance the app needs before it seals anything. */
|
|
78
113
|
config(): Promise<SealedConfig>;
|
|
114
|
+
/**
|
|
115
|
+
* A target's attestation document, fetched through the gateway so Tinfoil sees the gateway's address rather than the
|
|
116
|
+
* phone's. Returns the document's bytes exactly as the target served them, for the verifier (packages/e2ee attest),
|
|
117
|
+
* which checks that they answer `nonce`; nothing here reads or trusts them. Asked once, never retried: to try again,
|
|
118
|
+
* call it again with a fresh nonce.
|
|
119
|
+
*
|
|
120
|
+
* Throws SealedTransportError before sending anything when the target is not listed or the nonce is malformed, and a
|
|
121
|
+
* RecursivError for every answer that is not a document: RateLimitError past the per-minute check limit,
|
|
122
|
+
* NotFoundError while Sealed is off or not on for this app, and a 502 when the target could not be reached or did not
|
|
123
|
+
* answer with a document.
|
|
124
|
+
*/
|
|
125
|
+
attestation(input: SealedAttestationInput): Promise<Uint8Array>;
|
|
79
126
|
/** The URL `send()` posts to. */
|
|
80
127
|
chatCompletionsUrl(): string;
|
|
81
128
|
/**
|
package/dist/resources/sealed.js
CHANGED
|
@@ -31,10 +31,56 @@ export class SealedResource {
|
|
|
31
31
|
constructor(client) {
|
|
32
32
|
this.client = client;
|
|
33
33
|
}
|
|
34
|
-
/** The
|
|
34
|
+
/** The targets, models, verifier policy version and allowance the app needs before it seals anything. */
|
|
35
35
|
async config() {
|
|
36
36
|
return this.client.get('/sealed/config');
|
|
37
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* A target's attestation document, fetched through the gateway so Tinfoil sees the gateway's address rather than the
|
|
40
|
+
* phone's. Returns the document's bytes exactly as the target served them, for the verifier (packages/e2ee attest),
|
|
41
|
+
* which checks that they answer `nonce`; nothing here reads or trusts them. Asked once, never retried: to try again,
|
|
42
|
+
* call it again with a fresh nonce.
|
|
43
|
+
*
|
|
44
|
+
* Throws SealedTransportError before sending anything when the target is not listed or the nonce is malformed, and a
|
|
45
|
+
* RecursivError for every answer that is not a document: RateLimitError past the per-minute check limit,
|
|
46
|
+
* NotFoundError while Sealed is off or not on for this app, and a 502 when the target could not be reached or did not
|
|
47
|
+
* answer with a document.
|
|
48
|
+
*/
|
|
49
|
+
async attestation(input) {
|
|
50
|
+
// Malformed input is refused here, before the config is read, so it never turns into a network error.
|
|
51
|
+
const origin = exactOrigin(input.target);
|
|
52
|
+
if (!origin) {
|
|
53
|
+
throw new SealedTransportError('enclave_not_allowed', 'That server is not one the gateway forwards to. Nothing was sent.');
|
|
54
|
+
}
|
|
55
|
+
if (!HEX_32_BYTES.test(input.nonce)) {
|
|
56
|
+
throw new SealedTransportError('bad_nonce', 'The nonce must be 32 bytes of hex. Nothing was sent.');
|
|
57
|
+
}
|
|
58
|
+
const targets = input.targets ?? (await this.config()).targets;
|
|
59
|
+
if (!targets.some((target) => target.origin === origin)) {
|
|
60
|
+
throw new SealedTransportError('enclave_not_allowed', 'That server is not one the gateway forwards to. Nothing was sent.');
|
|
61
|
+
}
|
|
62
|
+
const controller = new AbortController();
|
|
63
|
+
const onCallerAbort = () => controller.abort(input.signal?.reason);
|
|
64
|
+
if (input.signal?.aborted)
|
|
65
|
+
controller.abort(input.signal.reason);
|
|
66
|
+
else
|
|
67
|
+
input.signal?.addEventListener('abort', onCallerAbort, { once: true });
|
|
68
|
+
const timer = setTimeout(() => controller.abort(), this.client.requestTimeoutMs);
|
|
69
|
+
try {
|
|
70
|
+
const response = await fetch(this.client.buildUrl('/sealed/attestation', { nonce: input.nonce, target: origin }), {
|
|
71
|
+
method: 'GET',
|
|
72
|
+
headers: { ...this.client.apiKeyAuthHeader(), Accept: 'application/json' },
|
|
73
|
+
signal: controller.signal,
|
|
74
|
+
});
|
|
75
|
+
if (!response.ok)
|
|
76
|
+
throw await gatewayRefusal(response);
|
|
77
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
clearTimeout(timer);
|
|
81
|
+
input.signal?.removeEventListener('abort', onCallerAbort);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
38
84
|
/** The URL `send()` posts to. */
|
|
39
85
|
chatCompletionsUrl() {
|
|
40
86
|
return this.client.buildUrl('/sealed/v1/chat/completions');
|
|
@@ -55,14 +101,18 @@ export class SealedResource {
|
|
|
55
101
|
* is not listed, the key is malformed, or a 2xx answer came back unsealed.
|
|
56
102
|
*/
|
|
57
103
|
async send(input) {
|
|
104
|
+
// Malformed input is refused here, before the config is read, so it never turns into a network error.
|
|
58
105
|
const origin = exactOrigin(input.enclaveOrigin);
|
|
59
|
-
|
|
60
|
-
if (!origin || !routers.includes(origin)) {
|
|
106
|
+
if (!origin) {
|
|
61
107
|
throw new SealedTransportError('enclave_not_allowed', 'That router is not one the gateway forwards to. Nothing was sent.');
|
|
62
108
|
}
|
|
63
109
|
if (!HEX_32_BYTES.test(input.encapsulatedKey)) {
|
|
64
110
|
throw new SealedTransportError('bad_encapsulated_key', 'The encapsulated key must be 32 bytes of hex. Nothing was sent.');
|
|
65
111
|
}
|
|
112
|
+
const targets = input.targets ?? (await this.config()).targets;
|
|
113
|
+
if (!targets.some((target) => target.origin === origin && target.kind === 'router')) {
|
|
114
|
+
throw new SealedTransportError('enclave_not_allowed', 'That router is not one the gateway forwards to. Nothing was sent.');
|
|
115
|
+
}
|
|
66
116
|
// Time out waiting for the answer to start; once it has, a long stream runs until it ends or the caller aborts.
|
|
67
117
|
const controller = new AbortController();
|
|
68
118
|
const onCallerAbort = () => controller.abort(input.signal?.reason);
|
package/dist/types.d.ts
CHANGED
|
@@ -723,6 +723,12 @@ export type ConversationEncryptionPolicy = {
|
|
|
723
723
|
export interface Conversation {
|
|
724
724
|
/** Missing on older APIs: treat as unknown, never as plaintext permission. */
|
|
725
725
|
encryption?: ConversationEncryptionPolicy;
|
|
726
|
+
/**
|
|
727
|
+
* Who can read it now, computed by the server from its current members: see
|
|
728
|
+
* PrivacyTier. Null when no member is an AI agent now; an agent that has left
|
|
729
|
+
* may have read earlier messages. Omitted by older APIs.
|
|
730
|
+
*/
|
|
731
|
+
privacy_tier?: PrivacyTier | null;
|
|
726
732
|
id: string;
|
|
727
733
|
type: string;
|
|
728
734
|
name: string | null;
|
|
@@ -773,6 +779,8 @@ export interface LeaveConversationResult {
|
|
|
773
779
|
export interface ConversationDetail {
|
|
774
780
|
/** Missing on older APIs: treat as unknown, never as plaintext permission. */
|
|
775
781
|
encryption?: ConversationEncryptionPolicy;
|
|
782
|
+
/** As `Conversation.privacy_tier`. Omitted by `createGroup()` and by older APIs. */
|
|
783
|
+
privacy_tier?: PrivacyTier | null;
|
|
776
784
|
id: string;
|
|
777
785
|
type: string;
|
|
778
786
|
name: string | null;
|
|
@@ -914,10 +922,86 @@ export interface MessageMedia {
|
|
|
914
922
|
* not encrypted; a withdrawn one leaves the history. The person who did not
|
|
915
923
|
* ask answers it by starting encryption as usual, from their own device.
|
|
916
924
|
*/
|
|
917
|
-
export interface
|
|
925
|
+
export interface EncryptionRequestMessageMetadata {
|
|
918
926
|
kind: 'e2ee_start_request';
|
|
919
927
|
v: number;
|
|
928
|
+
ai?: undefined;
|
|
929
|
+
}
|
|
930
|
+
/**
|
|
931
|
+
* Who can read a conversation or message now, as the server computes it from
|
|
932
|
+
* the conversation's current members. Render a label from this value, never
|
|
933
|
+
* from what the member picked. It is not a history: null says no AI agent is a
|
|
934
|
+
* member now, not that none ever read the messages, so never show null as
|
|
935
|
+
* "only people saw this".
|
|
936
|
+
* - `standard`: an AI agent is in the conversation. The app, Recursiv and the
|
|
937
|
+
* AI companies that route and answer each turn can read what is sent there.
|
|
938
|
+
* - `e2ee`: the conversation carries the MLS transport (`encryption.mode`
|
|
939
|
+
* `mls_v2`). Served on the conversation; a `Message` row is never MLS
|
|
940
|
+
* content, so a message is never `e2ee`.
|
|
941
|
+
* - `members_only`, `sealed`, `on_device`: reserved; no API serves them yet.
|
|
942
|
+
*/
|
|
943
|
+
export type PrivacyTier = 'standard' | 'members_only' | 'e2ee' | 'sealed' | 'on_device';
|
|
944
|
+
/** Where a tool's data can go (the same values as `AgentCapabilityExportBoundary`). */
|
|
945
|
+
export type ExportBoundary = 'none' | 'org_internal' | 'third_party_processor' | 'public_network';
|
|
946
|
+
/** One tool the model called on a turn. */
|
|
947
|
+
export interface AiTurnStampTool {
|
|
948
|
+
name: string;
|
|
949
|
+
export_boundary: ExportBoundary;
|
|
950
|
+
}
|
|
951
|
+
/**
|
|
952
|
+
* What answered an agent's reply, written by the server when it saves a reply
|
|
953
|
+
* a model wrote through its provider chain (`Message.metadata.ai`). Label
|
|
954
|
+
* replies from this, not from the model the member picked. Some agent
|
|
955
|
+
* messages carry none (server-written notices and limit replies, messages an
|
|
956
|
+
* agent sends outside a model turn): read a missing stamp as unknown.
|
|
957
|
+
*/
|
|
958
|
+
export interface AiTurnStamp {
|
|
959
|
+
/** `standard` for every reply the server runs. */
|
|
960
|
+
tier: PrivacyTier;
|
|
961
|
+
/** The model that answered, as `maker/model`, e.g. `google/gemini-3.5-flash`. */
|
|
962
|
+
model: string;
|
|
963
|
+
/** The model's maker as its id names it (the part before `/`), or `unknown`. */
|
|
964
|
+
maker: string;
|
|
965
|
+
/**
|
|
966
|
+
* `openrouter`: through OpenRouter, a routing service. `direct`: straight to
|
|
967
|
+
* the model's maker (Google, Anthropic or OpenAI) at its own API. `other`:
|
|
968
|
+
* through a service the stamp cannot name, such as an address the app's
|
|
969
|
+
* operator configured or a host that did not make the model.
|
|
970
|
+
*/
|
|
971
|
+
route: 'openrouter' | 'direct' | 'other';
|
|
972
|
+
/** The agent's usage tier on the turn. */
|
|
973
|
+
budget_tier: 'full' | 'standard' | 'economy' | 'hibernation';
|
|
974
|
+
/**
|
|
975
|
+
* Set when a different model answered than the one picked: `budget`, a
|
|
976
|
+
* smaller one because of usage limits (`model` is that smaller one);
|
|
977
|
+
* `fallback`, because `from` was not available (the picked model, or the
|
|
978
|
+
* smaller one a usage limit chose). Null when the picked model answered.
|
|
979
|
+
*/
|
|
980
|
+
substituted: null | {
|
|
981
|
+
from: string;
|
|
982
|
+
reason: 'budget' | 'fallback';
|
|
983
|
+
};
|
|
984
|
+
/** Something Minds AI saved (facts, or parts of other chats) went with the turn. */
|
|
985
|
+
memory_used: boolean;
|
|
986
|
+
/** Parts of the member's other chats, their summaries or decisions went with the turn. */
|
|
987
|
+
recall_used: boolean;
|
|
988
|
+
/** The member's activity (follows, votes, groups, interests) went with the turn. */
|
|
989
|
+
activity_used: boolean;
|
|
990
|
+
/**
|
|
991
|
+
* Every tool the model called on the turn up to this reply, in call order:
|
|
992
|
+
* page tools and any others, by name, with no arguments (so no addresses). A
|
|
993
|
+
* tool the server has not classified reads `public_network`.
|
|
994
|
+
*/
|
|
995
|
+
tools: AiTurnStampTool[];
|
|
996
|
+
}
|
|
997
|
+
/** An agent's reply: what answered it. Served only on a message from an agent (`sender.is_ai`). */
|
|
998
|
+
export interface AgentReplyMessageMetadata {
|
|
999
|
+
ai: AiTurnStamp;
|
|
1000
|
+
kind?: undefined;
|
|
1001
|
+
v?: undefined;
|
|
920
1002
|
}
|
|
1003
|
+
/** A message's `metadata`: an encryption request's, or an agent reply's turn stamp. */
|
|
1004
|
+
export type MessageMetadata = EncryptionRequestMessageMetadata | AgentReplyMessageMetadata;
|
|
921
1005
|
export interface Message {
|
|
922
1006
|
id: string;
|
|
923
1007
|
content: string;
|
|
@@ -928,6 +1012,14 @@ export interface Message {
|
|
|
928
1012
|
created_at: string;
|
|
929
1013
|
/** Absent on a person's own message, and from older APIs: see MessageMetadata. */
|
|
930
1014
|
metadata?: MessageMetadata;
|
|
1015
|
+
/**
|
|
1016
|
+
* Who can read it now: see PrivacyTier. `standard` on an agent's message, or
|
|
1017
|
+
* while an AI agent is a member of the conversation. Null otherwise, which
|
|
1018
|
+
* does not mean only people read it: a message sent while an agent was a
|
|
1019
|
+
* member went to the AI companies with that agent's turns, and reads null
|
|
1020
|
+
* once the agent leaves. Omitted by older APIs.
|
|
1021
|
+
*/
|
|
1022
|
+
privacy_tier?: PrivacyTier | null;
|
|
931
1023
|
}
|
|
932
1024
|
export interface SendMessageInput {
|
|
933
1025
|
conversation_id: string;
|
|
@@ -947,7 +1039,7 @@ export interface ChatMessageEvent {
|
|
|
947
1039
|
isAi?: boolean;
|
|
948
1040
|
};
|
|
949
1041
|
createdAt: string;
|
|
950
|
-
/** The message's metadata as stored; `kind: 'e2ee_start_request'` is an encryption request (MessageMetadata). */
|
|
1042
|
+
/** The message's metadata as stored; `kind: 'e2ee_start_request'` is an encryption request, and an agent's reply carries `ai`, its AiTurnStamp (MessageMetadata). */
|
|
951
1043
|
metadata?: Record<string, unknown>;
|
|
952
1044
|
}
|
|
953
1045
|
export interface ChatMessageEditEvent {
|
package/llm.md
CHANGED
|
@@ -504,7 +504,9 @@ const { data: group } = await r.chat.createGroup({ name: 'Team', member_ids: ['u
|
|
|
504
504
|
const { data: msgs } = await r.chat.messages(dm.id, { limit: 50 })
|
|
505
505
|
```
|
|
506
506
|
|
|
507
|
-
Methods: `conversations`, `communityConversation`, `conversation`, `messages`, `send`, `dm`, `createGroup`, `addMembers`, `removeMember`, `updateGroup`, `deleteConversation`, `leave`, `acceptRequest`, `declineRequest`, `reactToMessage`, `editMessage`, `deleteMessage`, `markAsRead`, `unreadCount`
|
|
507
|
+
Methods: `conversations`, `communityConversation`, `conversation`, `messages`, `send`, `dm`, `createGroup`, `addMembers`, `removeMember`, `updateGroup`, `deleteConversation`, `leave`, `acceptRequest`, `declineRequest`, `reactToMessage`, `editMessage`, `deleteMessage`, `markAsRead`, `unreadCount`, `attachmentUploadUrl`, `uploadAttachment`, `signAttachments`
|
|
508
|
+
|
|
509
|
+
Chat photos and voice notes (0.7.82): `const { ref } = await r.chat.uploadAttachment(dm.id, { blob })`, then send `ref`. With the server's private chat storage on, `ref` is `chat-attachment:<key>`: render it with `(await r.chat.signAttachments(dm.id, chatAttachmentKeysIn(message.content))).data.attachments` (URLs live about five minutes, `private, no-store`; `url: null` for a key the caller may not open). `editMessage` changes a message's words only: an edit that drops, adds or swaps a `chat-attachment:` reference gets 400 `attachment_refs_changed` (delete the message instead). Otherwise `ref` is a public URL, as before. A group's chat tied to a community always gets a public URL. Staff reviewing a member's report of a message open its private attachments with `r.reports.signAttachments(reportId)` (a live admin's own key; an AI account or agent key gets 403 `human_required`; only a report filed by a person in that chat, other than the sender and the caller, opens anything: 403 `own_report` or `reporter_not_a_recipient` otherwise).
|
|
508
510
|
|
|
509
511
|
Scroll back with `messages(id, { cursor: page.meta.next_cursor })`, not a growing `offset`. Conversations carry `can_delete` / `can_leave`; check them before offering Delete or Leave.
|
|
510
512
|
|
|
@@ -512,6 +514,8 @@ Private groups (0.7.59): each member has `role` (`admin` or `member`) and, in a
|
|
|
512
514
|
|
|
513
515
|
Activity time (0.7.67): conversation rows, `communityConversation()` and `conversation(id)` carry `last_activity_at`, the time `conversations()` sorts by: the newest message, or, in an end-to-end encrypted (`mls_v2`) conversation, the relay's receive time of its newest encrypted message when that is later (membership and key changes and quiet events such as reactions do not move it); its creation when it has no messages. In any other conversation, deleting the newest message moves it back to the previous message (or the creation), so do not keep the later of a cached and a served value. A time only, never content: show it on a row the device cannot decrypt. Omitted by older APIs, and by `conversation(id)` to a member who declined the request.
|
|
514
516
|
|
|
517
|
+
Who can read it now: conversation rows, `communityConversation()`, `conversation(id)` and each of `messages()` carry `privacy_tier` (`PrivacyTier`), computed by the server from the current members: `standard` where an AI agent is a member, and on an agent's own message (the app, Recursiv and the AI companies that route and answer each turn can read it), `e2ee` for an `mls_v2` conversation (never on a message row), and null when no AI agent is a member now. Null is not a history: messages sent while an agent was a member went to the AI companies with its turns and read null once it leaves, so never show null as "only people saw this". `members_only`, `sealed` and `on_device` are reserved. An agent's reply written by a model through the server's provider chain carries `metadata.ai`, its `AiTurnStamp`: the model that answered and its maker, the route (`openrouter`, `direct` to the maker, or `other`, a service the stamp cannot name), the usage tier, `substituted` (a smaller model for usage limits, or a fallback), whether saved memory, other chats or activity went with the turn, and each tool called by name with its `export_boundary` (no arguments or addresses; an unclassified tool reads `public_network`). Some agent messages carry no stamp (server-written notices, limit replies, messages an agent sends outside a model turn): read a missing stamp as unknown. Label a reply from the stamp, never from the model the member picked. Omitted by older APIs.
|
|
518
|
+
|
|
515
519
|
### r.e2ee — End-to-end encryption transport (flag-gated)
|
|
516
520
|
|
|
517
521
|
Relays opaque MLS envelopes, public device credentials and one-time key packages for the shared E2EE core; the server never sees keys or plaintext. Requires the `e2eeV2` network feature flag (otherwise `feature_disabled`) and a key acting as the account owner (agent-scoped keys get `agent_key_not_allowed`). The realm is the network id. Only a direct conversation between two people with an accepted request can start encryption (`conversation_not_eligible` otherwise), and every later append to it is checked the same way again (both people still accepted, still in the pilot, still only the two of them), so a client need not read the policy before each send; a resend of an event already stored still returns `duplicate: true`, even once a Welcome's target device is gone; blocks apply (`blocked`), to resends too; commits must send `expected_commit_seq`.
|
|
@@ -829,7 +833,7 @@ await r.appSubscriptions.changeTier({ tier: 'pro', proration_date: quote.prorati
|
|
|
829
833
|
- 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.
|
|
830
834
|
- 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.
|
|
831
835
|
- 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.
|
|
832
|
-
- `r.protocols` — Private app safety: `sourceControls` returns current `enabled` plus paged saved choices; `setSourceControl`, `onSourceControlChange` and `reportSource` share app scope. Example: `const choices = new Map(); const stop = r.protocols.onSourceControlChange(change => choices.set(JSON.stringify([change.source, change.control]), change.enabled));` then `await r.protocols.setSourceControl({ source: { protocol: "bluesky", kind: "actor", nativeId: "did:plc:aaaaaaaaaaaaaaaaaaaaaaaa" }, control: "mute", enabled: true, scope: "minds" });` and call `stop(); choices.clear()` on account change/unmount. Notifications follow an exact server confirmation, never an uncertain response. These preferences and reports apply in Minds, not on the remote network. Keyless post links: `const visitor = new Recursiv({ anonymous: true, 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`. 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. 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`.
|
|
836
|
+
- `r.protocols` — Private app safety: `sourceControls` returns current `enabled` plus paged saved choices; `setSourceControl`, `onSourceControlChange` and `reportSource` share app scope. Example: `const choices = new Map(); const stop = r.protocols.onSourceControlChange(change => choices.set(JSON.stringify([change.source, change.control]), change.enabled));` then `await r.protocols.setSourceControl({ source: { protocol: "bluesky", kind: "actor", nativeId: "did:plc:aaaaaaaaaaaaaaaaaaaaaaaa" }, control: "mute", enabled: true, scope: "minds" });` and call `stop(); choices.clear()` on account change/unmount. Notifications follow an exact server confirmation, never an uncertain response. These preferences and reports apply in Minds, not on the remote network. Keyless post links: `const visitor = new Recursiv({ anonymous: true, 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`. 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`.
|
|
833
837
|
- 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.
|
|
834
838
|
- 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.
|
|
835
839
|
- 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).
|