@recursiv/sdk 0.7.54 → 0.7.57
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 +28 -19
- package/dist/client.d.ts +1 -0
- package/dist/client.js +10 -2
- package/dist/errors.d.ts +41 -1
- package/dist/errors.js +77 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.js +1 -1
- package/dist/realtime.d.ts +24 -1
- package/dist/realtime.js +29 -1
- package/dist/resources/auth.d.ts +46 -10
- package/dist/resources/auth.js +99 -38
- package/dist/resources/commands.d.ts +14 -1
- package/dist/resources/commands.js +7 -1
- package/dist/resources/e2ee.d.ts +221 -6
- package/dist/resources/e2ee.js +90 -0
- package/dist/resources/human-input.d.ts +4 -1
- package/dist/resources/human-input.js +7 -3
- package/dist/resources/moderation.d.ts +5 -1
- package/dist/resources/moderation.js +6 -2
- package/dist/resources/notifications.d.ts +19 -0
- package/dist/resources/protocols.d.ts +52 -5
- package/dist/resources/protocols.js +13 -4
- package/dist/resources/reports.d.ts +4 -0
- package/dist/resources/reports.js +5 -1
- package/dist/types.d.ts +6 -0
- package/llm.md +26 -6
- package/package.json +1 -1
|
@@ -81,7 +81,11 @@ export declare class ModerationResource {
|
|
|
81
81
|
* reachable through a valid key while the account is suspended or banned.
|
|
82
82
|
*/
|
|
83
83
|
mine(): Promise<SingleResponse<OwnModerationLog>>;
|
|
84
|
-
/**
|
|
84
|
+
/**
|
|
85
|
+
* Appeal a decision affecting the caller or content they own. Sent once,
|
|
86
|
+
* never retried automatically: a 429 past the member's appeal budget
|
|
87
|
+
* (`RateLimitError`, `retryAfter`) is for the member to see.
|
|
88
|
+
*/
|
|
85
89
|
appeal(actionId: string, statement: string): Promise<SingleResponse<{
|
|
86
90
|
success: true;
|
|
87
91
|
id: string;
|
|
@@ -22,9 +22,13 @@ export class ModerationResource {
|
|
|
22
22
|
mine() {
|
|
23
23
|
return this.client.get('/appeals/moderation-actions');
|
|
24
24
|
}
|
|
25
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* Appeal a decision affecting the caller or content they own. Sent once,
|
|
27
|
+
* never retried automatically: a 429 past the member's appeal budget
|
|
28
|
+
* (`RateLimitError`, `retryAfter`) is for the member to see.
|
|
29
|
+
*/
|
|
26
30
|
appeal(actionId, statement) {
|
|
27
|
-
return this.client.post(`/appeals/${encodeURIComponent(actionId)}`, { statement });
|
|
31
|
+
return this.client.post(`/appeals/${encodeURIComponent(actionId)}`, { statement }, { retry: false });
|
|
28
32
|
}
|
|
29
33
|
/** Admin review: uphold or overturn an appeal. */
|
|
30
34
|
resolveAppeal(appealId, input) {
|
|
@@ -19,6 +19,11 @@ export interface NotificationActor {
|
|
|
19
19
|
* state so inline Upvote and Repost can start in the right position. `null`
|
|
20
20
|
* when the post is gone or no longer visible to the viewer.
|
|
21
21
|
*/
|
|
22
|
+
/** A post's tag as a notification's post preview carries it. */
|
|
23
|
+
export interface NotificationPostTag {
|
|
24
|
+
name: string;
|
|
25
|
+
slug: string;
|
|
26
|
+
}
|
|
22
27
|
export interface NotificationPostPreview {
|
|
23
28
|
id: string;
|
|
24
29
|
/**
|
|
@@ -35,6 +40,20 @@ export interface NotificationPostPreview {
|
|
|
35
40
|
excerpt_truncated?: boolean;
|
|
36
41
|
media_url: string | null;
|
|
37
42
|
media_type: string | null;
|
|
43
|
+
/**
|
|
44
|
+
* The post's own NSFW flag. With `tags`, it says whether the post is
|
|
45
|
+
* sensitive: a client that masks a sensitive post's card masks its excerpt
|
|
46
|
+
* and picture here too, and holds the picture back while this is absent
|
|
47
|
+
* (servers older than this field).
|
|
48
|
+
*/
|
|
49
|
+
is_nsfw?: boolean;
|
|
50
|
+
/**
|
|
51
|
+
* The post's tags, each with its name as written and the slug the tag API
|
|
52
|
+
* derived from it ("nsfw", "nude", "porn" and the like mark it sensitive).
|
|
53
|
+
* Match both: a tag created as "NSFW!" is slugged "nsfw". Absent from
|
|
54
|
+
* servers older than this field.
|
|
55
|
+
*/
|
|
56
|
+
tags?: NotificationPostTag[];
|
|
38
57
|
reactions_count?: number;
|
|
39
58
|
score?: number;
|
|
40
59
|
upvote_count?: number;
|
|
@@ -970,8 +970,22 @@ export interface SocialPost {
|
|
|
970
970
|
replyCount: number | null;
|
|
971
971
|
likeCount: number | null;
|
|
972
972
|
repostCount: number | null;
|
|
973
|
+
/** Posts quoting this one, only where the provider reports it (Bluesky's AppView, Mastodon 4.5 and later). Absent when unknown. */
|
|
974
|
+
quoteCount?: number;
|
|
973
975
|
/**
|
|
974
|
-
*
|
|
976
|
+
* Who counted the numbers above: `appview` (Bluesky's AppView, across the network), `instance_local`
|
|
977
|
+
* (one Fediverse server's view of the post) or `provider_reported` (a third party's unsigned figures,
|
|
978
|
+
* Primal's for Nostr: show them as reported by Primal, never as verified). Absent when the post has no counts.
|
|
979
|
+
*/
|
|
980
|
+
countsOrigin?: 'appview' | 'instance_local' | 'provider_reported';
|
|
981
|
+
/**
|
|
982
|
+
* Nostr: distinct people, never the author, behind the signed zap receipts the provider returned for
|
|
983
|
+
* the note (each receipt and its zap request signature-checked). A lower bound; sats are not counted.
|
|
984
|
+
*/
|
|
985
|
+
distinctZappers?: number;
|
|
986
|
+
/**
|
|
987
|
+
* When the counts were read from the source network, at the earliest (they can be up to one index
|
|
988
|
+
* snapshot interval newer, never older). Present only on a post a category feed served
|
|
975
989
|
* from the open-network index, whose copy can be older than the page (the server refreshes counts
|
|
976
990
|
* older than 15 minutes when the provider answers in time). Absent: the counts are from this read.
|
|
977
991
|
*/
|
|
@@ -995,6 +1009,15 @@ export interface SocialPost {
|
|
|
995
1009
|
} | {
|
|
996
1010
|
state: 'unavailable';
|
|
997
1011
|
};
|
|
1012
|
+
/**
|
|
1013
|
+
* Present, and only then, when the ActivityPub author opted out of indexing (`noindex`,
|
|
1014
|
+
* `indexable: false`, or #nobot, #noindex or #noai in their bio). The keyless trends, trend-feed and
|
|
1015
|
+
* radar reads leave such posts out, and no post served from the source index is one. The keyless
|
|
1016
|
+
* author-feed and conversation reads (a profile or a post someone opened) and member reads of a
|
|
1017
|
+
* provider window still return them with this mark, so an app should never re-share or promote one
|
|
1018
|
+
* to signed-out visitors.
|
|
1019
|
+
*/
|
|
1020
|
+
authorOptOut?: true;
|
|
998
1021
|
}
|
|
999
1022
|
export interface SocialPostSearch {
|
|
1000
1023
|
items: SocialPost[];
|
|
@@ -1119,6 +1142,15 @@ export interface SocialRadarMember {
|
|
|
1119
1142
|
height?: number;
|
|
1120
1143
|
};
|
|
1121
1144
|
providerRank?: number;
|
|
1145
|
+
/**
|
|
1146
|
+
* Present on a post member whose image or video a signed-out visitor may see (an open-network post with a cached
|
|
1147
|
+
* safe verdict, or a Minds post that passes the visitor's floor); absent on every other post member, and on members
|
|
1148
|
+
* of a radar built before the server recorded it. An outlet's media needs no mark. The keyless routes
|
|
1149
|
+
* (`getSharedRadar`, `getSharedRadarPosts`, `getSharedRadarStory`) serve media only from marked members and outlets.
|
|
1150
|
+
* A signed-in read (`getPublicRadar` and friends) keeps every member's media, so use only an outlet's or a marked
|
|
1151
|
+
* member's media on a signed-out page or a share card.
|
|
1152
|
+
*/
|
|
1153
|
+
visitorMedia?: true;
|
|
1122
1154
|
}
|
|
1123
1155
|
/** One story on the radar. `breaking` is outlet corroboration inside a short window; `viral` is social velocity above its network's norm. */
|
|
1124
1156
|
export interface SocialRadarCluster {
|
|
@@ -1143,6 +1175,7 @@ export interface SocialRadarCluster {
|
|
|
1143
1175
|
normalized: number;
|
|
1144
1176
|
samples: number;
|
|
1145
1177
|
};
|
|
1178
|
+
/** On a signed-in read the lead may belong to a member without `visitorMedia`; find it by `memberKey` before a share card uses it. */
|
|
1146
1179
|
media: {
|
|
1147
1180
|
images: number;
|
|
1148
1181
|
videos: number;
|
|
@@ -1233,6 +1266,11 @@ export interface SocialRadarStory {
|
|
|
1233
1266
|
id: string;
|
|
1234
1267
|
label: string;
|
|
1235
1268
|
} | null;
|
|
1269
|
+
/**
|
|
1270
|
+
* Present when the story was asked for by a World Today edition story's id: the radar story that edition story
|
|
1271
|
+
* seeded, and `id` is its radar id, the address to link to. A server that cannot resolve edition story ids answers 400.
|
|
1272
|
+
*/
|
|
1273
|
+
editionStory?: string;
|
|
1236
1274
|
}
|
|
1237
1275
|
/** Read options for callers that hold their own deadline, such as a discovery page. */
|
|
1238
1276
|
export interface SocialReadOptions {
|
|
@@ -1333,13 +1371,16 @@ export declare class ProtocolsResource {
|
|
|
1333
1371
|
category?: string;
|
|
1334
1372
|
discovery?: boolean;
|
|
1335
1373
|
}): Promise<SingleResponse<SocialTrendFeed>>;
|
|
1336
|
-
/**
|
|
1374
|
+
/**
|
|
1375
|
+
* Breaking and viral stories clustered across outlets, open networks and Minds, through the same public-only app boundary.
|
|
1376
|
+
* Images and videos come only from outlets and from members marked `visitorMedia`; every other member is text.
|
|
1377
|
+
*/
|
|
1337
1378
|
getSharedRadar(projectId: string, params?: SocialRadarParams): Promise<SingleResponse<SocialRadar>>;
|
|
1338
1379
|
/** One radar story's social member posts without a key, so a signed-out story card can show the posts it counts. */
|
|
1339
1380
|
getSharedRadarPosts(projectId: string, params: {
|
|
1340
1381
|
cluster: string;
|
|
1341
1382
|
}): Promise<SingleResponse<SocialRadarPosts>>;
|
|
1342
|
-
/** One radar story by id with its posts, without a key: a story page or shared link in one call. */
|
|
1383
|
+
/** One radar story by id with its posts, without a key: a story page or shared link in one call. `cluster` may be a World Today edition story's id. */
|
|
1343
1384
|
getSharedRadarStory(projectId: string, params: {
|
|
1344
1385
|
cluster: string;
|
|
1345
1386
|
}): Promise<SingleResponse<SocialRadarStory>>;
|
|
@@ -1395,13 +1436,19 @@ export declare class ProtocolsResource {
|
|
|
1395
1436
|
category?: string;
|
|
1396
1437
|
discovery?: boolean;
|
|
1397
1438
|
}, options?: SocialReadOptions): Promise<SingleResponse<SocialTrendFeed>>;
|
|
1398
|
-
/**
|
|
1439
|
+
/**
|
|
1440
|
+
* The app's topic radar under its own key: the same snapshot the keyless route serves, for a signed-in reader, so
|
|
1441
|
+
* every member keeps its media (the keyless route serves only outlets' and `visitorMedia` members'). Never triggers a build.
|
|
1442
|
+
*/
|
|
1399
1443
|
getPublicRadar(params?: SocialRadarParams, options?: SocialReadOptions): Promise<SingleResponse<SocialRadar>>;
|
|
1400
1444
|
/** One radar story's social member posts, so its card can show the posts it counts. Minds members are read with `posts.get`. */
|
|
1401
1445
|
getPublicRadarPosts(params: {
|
|
1402
1446
|
cluster: string;
|
|
1403
1447
|
}, options?: SocialReadOptions): Promise<SingleResponse<SocialRadarPosts>>;
|
|
1404
|
-
/**
|
|
1448
|
+
/**
|
|
1449
|
+
* One radar story by id with its posts under the app's own key; `status` tells an aged-out story from an unknown one.
|
|
1450
|
+
* `cluster` may be a World Today edition story's id (see SocialRadarStory.editionStory).
|
|
1451
|
+
*/
|
|
1405
1452
|
getPublicRadarStory(params: {
|
|
1406
1453
|
cluster: string;
|
|
1407
1454
|
}, options?: SocialReadOptions): Promise<SingleResponse<SocialRadarStory>>;
|
|
@@ -102,7 +102,10 @@ export class ProtocolsResource {
|
|
|
102
102
|
return this.client.get(`/protocols/public/${encodeURIComponent(projectId)}/trend-feed`, { protocol: params.protocol, topic: params.topic,
|
|
103
103
|
limit: params.limit, cursor: params.cursor, category: params.category, discovery: params.discovery });
|
|
104
104
|
}
|
|
105
|
-
/**
|
|
105
|
+
/**
|
|
106
|
+
* Breaking and viral stories clustered across outlets, open networks and Minds, through the same public-only app boundary.
|
|
107
|
+
* Images and videos come only from outlets and from members marked `visitorMedia`; every other member is text.
|
|
108
|
+
*/
|
|
106
109
|
getSharedRadar(projectId, params = {}) {
|
|
107
110
|
return this.client.get(`/protocols/public/${encodeURIComponent(projectId)}/radar`, radarQuery(params));
|
|
108
111
|
}
|
|
@@ -110,7 +113,7 @@ export class ProtocolsResource {
|
|
|
110
113
|
getSharedRadarPosts(projectId, params) {
|
|
111
114
|
return this.client.get(`/protocols/public/${encodeURIComponent(projectId)}/radar/posts`, { cluster: params.cluster });
|
|
112
115
|
}
|
|
113
|
-
/** One radar story by id with its posts, without a key: a story page or shared link in one call. */
|
|
116
|
+
/** One radar story by id with its posts, without a key: a story page or shared link in one call. `cluster` may be a World Today edition story's id. */
|
|
114
117
|
getSharedRadarStory(projectId, params) {
|
|
115
118
|
return this.client.get(`/protocols/public/${encodeURIComponent(projectId)}/radar/story`, { cluster: params.cluster });
|
|
116
119
|
}
|
|
@@ -167,7 +170,10 @@ export class ProtocolsResource {
|
|
|
167
170
|
...(params.category !== undefined ? { category: params.category } : {}),
|
|
168
171
|
...(params.discovery !== undefined ? { discovery: params.discovery } : {}) }, ...readOptions(options));
|
|
169
172
|
}
|
|
170
|
-
/**
|
|
173
|
+
/**
|
|
174
|
+
* The app's topic radar under its own key: the same snapshot the keyless route serves, for a signed-in reader, so
|
|
175
|
+
* every member keeps its media (the keyless route serves only outlets' and `visitorMedia` members'). Never triggers a build.
|
|
176
|
+
*/
|
|
171
177
|
getPublicRadar(params = {}, options) {
|
|
172
178
|
return this.client.get('/protocols/social/radar', radarQuery(params), ...readOptions(options));
|
|
173
179
|
}
|
|
@@ -175,7 +181,10 @@ export class ProtocolsResource {
|
|
|
175
181
|
getPublicRadarPosts(params, options) {
|
|
176
182
|
return this.client.get('/protocols/social/radar/posts', { cluster: params.cluster }, ...readOptions(options));
|
|
177
183
|
}
|
|
178
|
-
/**
|
|
184
|
+
/**
|
|
185
|
+
* One radar story by id with its posts under the app's own key; `status` tells an aged-out story from an unknown one.
|
|
186
|
+
* `cluster` may be a World Today edition story's id (see SocialRadarStory.editionStory).
|
|
187
|
+
*/
|
|
179
188
|
getPublicRadarStory(params, options) {
|
|
180
189
|
return this.client.get('/protocols/social/radar/story', { cluster: params.cluster }, ...readOptions(options));
|
|
181
190
|
}
|
|
@@ -35,6 +35,10 @@ export declare class ReportsResource {
|
|
|
35
35
|
* Submit a content report. Targets a post, user, community, or message
|
|
36
36
|
* by ID, with a short reason and optional free-text details. Reporter
|
|
37
37
|
* is the authenticated user.
|
|
38
|
+
*
|
|
39
|
+
* Sent once, never retried automatically: past the member's report budget
|
|
40
|
+
* the server answers 429 (`RateLimitError`, `retryAfter` = seconds until
|
|
41
|
+
* the window opens), and that refusal is for the member to see.
|
|
38
42
|
*/
|
|
39
43
|
create(input: CreateReportInput): Promise<SingleResponse<{
|
|
40
44
|
success: true;
|
|
@@ -7,9 +7,13 @@ export class ReportsResource {
|
|
|
7
7
|
* Submit a content report. Targets a post, user, community, or message
|
|
8
8
|
* by ID, with a short reason and optional free-text details. Reporter
|
|
9
9
|
* is the authenticated user.
|
|
10
|
+
*
|
|
11
|
+
* Sent once, never retried automatically: past the member's report budget
|
|
12
|
+
* the server answers 429 (`RateLimitError`, `retryAfter` = seconds until
|
|
13
|
+
* the window opens), and that refusal is for the member to see.
|
|
10
14
|
*/
|
|
11
15
|
create(input) {
|
|
12
|
-
return this.client.post('/reports', input);
|
|
16
|
+
return this.client.post('/reports', input, { retry: false });
|
|
13
17
|
}
|
|
14
18
|
/** List recent reports in the caller's project/network (admin only). */
|
|
15
19
|
list() {
|
package/dist/types.d.ts
CHANGED
|
@@ -229,6 +229,12 @@ export interface RepostedPost {
|
|
|
229
229
|
/** The group the original is in (null when it is in none): it decides what the repost card offers. */
|
|
230
230
|
community?: PostCommunity | null;
|
|
231
231
|
organization_id: string | null;
|
|
232
|
+
/**
|
|
233
|
+
* The original's own NSFW flag. A repost or quote card shows the original's
|
|
234
|
+
* words and media: mask them when this is set or `tags` marks it sensitive.
|
|
235
|
+
* Absent from servers older than this field.
|
|
236
|
+
*/
|
|
237
|
+
is_nsfw?: boolean;
|
|
232
238
|
tags: Tag[];
|
|
233
239
|
reactions_count: number;
|
|
234
240
|
score: number;
|
package/llm.md
CHANGED
|
@@ -190,7 +190,7 @@ const { data } = await r.commands.execute({
|
|
|
190
190
|
// data: { text, actions }
|
|
191
191
|
```
|
|
192
192
|
|
|
193
|
-
Methods: `
|
|
193
|
+
Methods: `execute({ prompt, current_screen?, org_slug?, model?, locale?, attachments? })`, `gatePrompt({ messages })`. `gatePrompt` is Recursiv's own signup gate, not a moderation API: it answers only for keys on the platform's networks (404 anywhere else, including an app's own network), ignores `model`, and shares one daily LLM budget with Recursiv's signup page.
|
|
194
194
|
|
|
195
195
|
### r.projectBrain — Per-project knowledge base & task management
|
|
196
196
|
|
|
@@ -389,7 +389,7 @@ await r.auth.requestTwoFactorEmailCode(live, { projectId }) // app context brand
|
|
|
389
389
|
await r.auth.disableTwoFactor({ emailCode: '482913' }, live)
|
|
390
390
|
```
|
|
391
391
|
|
|
392
|
-
Two-factor methods: `enableTwoFactor`, `verifyTwoFactor`, `disableTwoFactor`, `regenerateTwoFactorBackupCodes`, `requestTwoFactorEmailCode`, `completeTwoFactorSignIn`, `completeTwoFactorSignInAndCreateKey`. OAuth consent screens: `exchangeSessionForCookie(token)` (browser, sets the API-origin cookie so `/api/auth/mcp/authorize` continues without a second sign-in), `getOAuthClientInfo({ clientId, redirectUri?, scope? })` (public client name, redirect hosts, `redirect_uri_registered`, granted scopes).
|
|
392
|
+
Two-factor methods: `enableTwoFactor`, `verifyTwoFactor`, `disableTwoFactor`, `regenerateTwoFactorBackupCodes`, `requestTwoFactorEmailCode`, `completeTwoFactorSignIn`, `completeTwoFactorSignInAndCreateKey`. Minds cutover: `legacyHandoff(legacyToken, { attemptId, refreshToken, projectId, timeoutMs })` exchanges the legacy app's session once; it throws `LegacyHandoffError` (`status` 0 when no response came, `code`, `retryAfterSeconds`), and only `legacy_token_used` / `legacy_token_invalid` mean forget the legacy session. With an `attemptId` a success carries `apiKeyDescription`; give the API key made with that session that `description`, so replays of the attempt count only that key. `legacyHandoffAndCreateKey(token, keyInput, { attemptId, refreshToken })` does both; pass the same `attemptId` on every retry. OAuth consent screens: `exchangeSessionForCookie(token)` (browser, sets the API-origin cookie so `/api/auth/mcp/authorize` continues without a second sign-in), `getOAuthClientInfo({ clientId, redirectUri?, scope? })` (public client name, redirect hosts, `redirect_uri_registered`, granted scopes).
|
|
393
393
|
|
|
394
394
|
**Mobile/React Native auth** (recommended — one call instead of three):
|
|
395
395
|
```typescript
|
|
@@ -502,16 +502,36 @@ Scroll back with `messages(id, { cursor: page.meta.next_cursor })`, not a growin
|
|
|
502
502
|
|
|
503
503
|
### r.e2ee — End-to-end encryption transport (flag-gated)
|
|
504
504
|
|
|
505
|
-
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); blocks apply (`blocked`); commits must send `expected_commit_seq`.
|
|
505
|
+
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`.
|
|
506
|
+
|
|
507
|
+
Ask first instead of trying a call: `r.e2ee.status()` returns `{ enabled: true }` or `{ enabled: false, reason: 'network_off' | 'not_allowlisted' }` for the calling account only. `enabled: false` means frozen: already-encrypted conversations stay readable, nothing new can be sent or enrolled.
|
|
506
508
|
|
|
507
509
|
```ts
|
|
510
|
+
const { data: access } = await r.e2ee.status(); // offer encryption only when access.enabled
|
|
508
511
|
const { data: device } = await r.e2ee.registerDevice({ device_name: 'phone-1', realm: networkId, signing_key, credential });
|
|
509
512
|
await r.e2ee.publishKeyPackages({ device_id: device.id, packages: [keyPackageBase64] }); // → { published, available }
|
|
510
513
|
const { data: stock } = await r.e2ee.keyPackageStatus({ device_id: device.id }); // { available, expiringSoon }; refill when low
|
|
511
514
|
const { data: claimed } = await r.e2ee.claimKeyPackage({ device_id: device.id, target_device_id: peerDeviceId });
|
|
512
515
|
await r.e2ee.appendEvent(conversationId, { device_id: device.id, envelope }); // idempotent per event_id
|
|
516
|
+
await r.e2ee.appendEvent(conversationId, { device_id: device.id, envelope: reaction, silent: true }); // no push, no move up the chat list; every member's doorbell still rings
|
|
513
517
|
const { data: page } = await r.e2ee.events(conversationId, { device_id: device.id, after_seq: 0 });
|
|
514
518
|
await r.e2ee.acknowledge(conversationId, { device_id: device.id, seq: page.headSeq });
|
|
519
|
+
// Doorbell after every append: { conversationId, seq, event? }. `event` (a small application message, as events() returns it) comes only to a socket of the owner's session or an owner key, never an agent-scoped key.
|
|
520
|
+
r.realtime.onE2eeEvent(({ conversationId, event }) => (event ? controller.applyPushed(conversationId, event) : controller.sync(conversationId)));
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
Link a new device by scanning (0.7.57; `status()` reports `links: true` when the relay can). The new device N shows the link id in a QR code; an approved device A of the same account scans it. The relay is a short-lived mailbox (180 s, 120 s more once scanned; one open link per sign-in, at most 3 per account; 10 creates per sign-in and 20 per account an hour, refused with 409 `link_limit`, never 429). `approval` and `tag` go only to the sign-in that created the link. Reserved labels get 400 `invalid_label`; a 503 `storage_unavailable` is temporary, so keep polling until `expiresAt`. Own devices carry `createdAt` and `lastSeenAt`.
|
|
524
|
+
|
|
525
|
+
```ts
|
|
526
|
+
const { data: link } = await r.e2ee.createLink({ device_name, signing_key, label: 'Pixel 9' }); // on N; count down with expiresAt - now
|
|
527
|
+
await r.e2ee.scannedLink(link.id, { device_id: a.id }); // on A, after the scan
|
|
528
|
+
await r.e2ee.approveLink(link.id, { device_id: a.id, approval, tag }); // retry-safe
|
|
529
|
+
const { data: mine } = await r.e2ee.link(link.id); // N polls, or on r.realtime.onE2eeLink(({ linkId, state }) => ...)
|
|
530
|
+
await r.e2ee.completeLink(link.id, { device_id: n.id }); // N, once registered
|
|
531
|
+
await r.e2ee.linkProgress(link.id, { device_id: a.id, done: 3, total: 7, joined: 3, not_held: 0, failed: 0, last_conversation_id });
|
|
532
|
+
await r.e2ee.cancelLink(link.id, { device_id: a.id, reason: 'declined' }); // "I didn't start this"
|
|
533
|
+
const { data: ids, next } = await r.e2ee.conversations({ limit: 200 }); // encrypted chat ids, keyset-paged
|
|
534
|
+
r.realtime.onE2eeDevices(({ deviceId, reason }) => {}); // 'packages': it can be claimed now; 'revoked'
|
|
515
535
|
```
|
|
516
536
|
|
|
517
537
|
### r.realtime — WebSocket Chat Client
|
|
@@ -547,7 +567,7 @@ unsub()
|
|
|
547
567
|
const off = r.realtime.on('notification', (n) => refreshBadge(n))
|
|
548
568
|
```
|
|
549
569
|
|
|
550
|
-
Events: `onMessage`, `onMessageEdit`, `onMessageDelete`, `onTyping`, `onAgentThinking`, `on(event, fn)`
|
|
570
|
+
Events: `onMessage`, `onMessageEdit`, `onMessageDelete`, `onTyping`, `onAgentThinking`, `onE2eeEvent`, `onE2eeLink`, `onE2eeDevices`, `on(event, fn)`
|
|
551
571
|
|
|
552
572
|
Reconnects on its own (transport drops, token expiry, refused handshakes) with jittered backoff and a fresh single-use token per attempt; listeners and joined rooms carry over, so no app-side reconnect shim is needed. It stops only on `disconnect()` or a 401/403 from the token source. `connect()` is safe to call repeatedly.
|
|
553
573
|
|
|
@@ -805,10 +825,10 @@ Pagination: pass `limit` and `offset`. Check `meta.has_more`.
|
|
|
805
825
|
| `AuthorizationError` | 403 | API key lacks required scope |
|
|
806
826
|
| `NotFoundError` | 404 | Resource doesn't exist or no access |
|
|
807
827
|
| `ValidationError` | 400 | Invalid input (check `error.details`) |
|
|
808
|
-
| `RateLimitError` | 429 | Rate limit hit (`
|
|
828
|
+
| `RateLimitError` | 429 | Rate limit hit (`retryAfter` seconds, `resetAt` Date, `scope`) |
|
|
809
829
|
| `ConflictError` | 409 | Resource already exists |
|
|
810
830
|
|
|
811
|
-
Auto-retry on 429/5xx built in (exponential backoff, default `maxRetries: 2`).
|
|
831
|
+
Auto-retry on 429/5xx built in (exponential backoff, default `maxRetries: 2`), except a 429 with `X-RateLimit-Scope: action` (one member action past its budget): never retried, `RateLimitError.scope === 'action'`, `resetAt` = when it reopens; show it on that action, do not pause the app. Member actions with a per-member budget are sent once and never auto-retried: `reports.create`, `moderation.appeal` and `protocols.reportSource` throw the 429 at once (`RateLimitError`, `message` is member-facing, `retryAfter` = seconds until the window opens; a budget refusal carries `X-RateLimit-Scope: action` and no `Retry-After` header).
|
|
812
832
|
|
|
813
833
|
### Idempotency
|
|
814
834
|
|