@recursiv/sdk 0.7.55 → 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 +4 -4
- package/dist/client.js +9 -2
- package/dist/errors.d.ts +28 -1
- package/dist/errors.js +60 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1 -1
- package/dist/realtime.d.ts +24 -1
- package/dist/realtime.js +29 -1
- package/dist/resources/e2ee.d.ts +169 -0
- package/dist/resources/e2ee.js +57 -0
- package/dist/resources/protocols.d.ts +34 -3
- package/dist/resources/protocols.js +5 -2
- package/llm.md +19 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -75,7 +75,7 @@ for await (const chunk of r.agents.chatStream(agent.id, { message: 'Hello!' }))
|
|
|
75
75
|
| Resource | Key methods |
|
|
76
76
|
|----------|------------|
|
|
77
77
|
| `r.agents` | `create`, `update`, `chat`, `chatStream`, `retryChatStream`, `grantProjectAccess`, `conversations` |
|
|
78
|
-
| `r.e2ee` | `status`, `registerDevice`, `devices`, `revokeDevice`, `publishKeyPackages`, `keyPackageStatus`, `claimKeyPackage`, `appendEvent`, `events`, `acknowledge` (flag-gated E2EE v2 transport) |
|
|
78
|
+
| `r.e2ee` | `status`, `registerDevice`, `devices`, `revokeDevice`, `publishKeyPackages`, `keyPackageStatus`, `claimKeyPackage`, `appendEvent`, `events`, `acknowledge`, `conversations`; device linking by scan: `createLink`, `link`, `scannedLink`, `approveLink`, `completeLink`, `linkProgress`, `cancelLink` (flag-gated E2EE v2 transport; its doorbell is `r.realtime.onE2eeEvent`, which carries a small message's stored ciphertext to the owner's own sockets; link and device changes arrive on `r.realtime.onE2eeLink` and `onE2eeDevices`) |
|
|
79
79
|
| `r.brain` | `sendMessage` |
|
|
80
80
|
| `r.commands` | `execute`, `gatePrompt` |
|
|
81
81
|
| `r.projectBrain` | `tasks`, `decisions`, `milestones`, `completeTask`, `usage` |
|
|
@@ -115,7 +115,7 @@ for await (const chunk of r.agents.chatStream(agent.id, { message: 'Hello!' }))
|
|
|
115
115
|
| `r.admin` | User management, stats, content reports (`listReports`, `resolveReport`, `dismissReport`) |
|
|
116
116
|
| `r.dispatcher` | Task queue, claims, signals, outcomes, webhooks |
|
|
117
117
|
| `r.goals` | Org goals with task rollups (group dispatcher tasks) |
|
|
118
|
-
| `r.protocols` | Private Minds safety: `sourceControls` (current enablement and saved choices; filter with `control`, each item may carry a `display` label), `setSourceControl`, `onSourceControlChange`, `reportSource`. Delegated accounts: `nativeSocialAccounts`, `startNativeSocialConnection`, `finishNativeSocialConnection`, `disconnectNativeSocialAccount`. Native follows: `nativeFollowStates` (every connected account's state for one source, one read), `nativeFollowState`, `prepareNativeFollow`, `submitNativeFollow`, `nativeFollowStatus` (native following or pending approval, distinct from a Minds feed subscription; preserve the complete signed Nostr list). Native reposts and undo: `nativeRepostState`, `prepareNativeRepost`, `submitNativeRepost`, `nativeRepostStatus` (public posts, explicit approval, connected account, no uncertain retries). Native likes and unlikes: `nativeLikeState`, `prepareNativeLike`, `submitNativeLike`, `nativeLikeStatus` (connected authority, app enablement and explicit approval; resume pending actions instead of duplicating them). Reviewed replies and optional linked posts: `prepareNativeReply`, `submitNativeReply`, `nativeReplyStatus`, `nativeReplyHistory`, `prepareNativeReplyRemoval`, `removeNativeReply` (require deployed API support and app enablement). Keyless shared post links: `sharedNetworks(projectId)`, `getSharedConversation(projectId, { protocol, post, readRef? })`. Member public reading: `searchPublicPosts` (selected account required for Bluesky/ActivityPub; public Nostr search), `getConnectedSocialConversation` (preserve result readerAccountId/readRef), `publicNetworks`, `searchPublicProfiles`, `getPublicProfile`, `getPublicAuthorFeed`, `getFollowingFeed`, `getPublicConversation`, `getPublicTrends`, `getPublicTrendFeed`; radar stories: `getPublicRadarStory` (one story by id with its posts; one that has left the radar comes with its archived copy and the live story it continues as), keyless `getSharedRadarStory(projectId, { cluster })` and `getSharedRadarPosts(projectId, { cluster })` (keyless reads carry images and videos only from outlets and members marked `visitorMedia`; signed-in reads keep all media, so a share card uses only those); private app subscriptions: `listSubscriptions`, `subscribeToProfile`, `unsubscribeFromProfile`, `onSubscriptionChange`. `list` returns bounded adapter `maturity`, `limitations`, and exhaustive `supported`/`planned`/`unsupported` implementation capabilities (not provider health or enablement); settings, candidate refresh/status, search, and read-only quarantined candidate review via `listCandidates` / `getCandidate` |
|
|
118
|
+
| `r.protocols` | Private Minds safety: `sourceControls` (current enablement and saved choices; filter with `control`, each item may carry a `display` label), `setSourceControl`, `onSourceControlChange`, `reportSource`. Delegated accounts: `nativeSocialAccounts`, `startNativeSocialConnection`, `finishNativeSocialConnection`, `disconnectNativeSocialAccount`. Native follows: `nativeFollowStates` (every connected account's state for one source, one read), `nativeFollowState`, `prepareNativeFollow`, `submitNativeFollow`, `nativeFollowStatus` (native following or pending approval, distinct from a Minds feed subscription; preserve the complete signed Nostr list). Native reposts and undo: `nativeRepostState`, `prepareNativeRepost`, `submitNativeRepost`, `nativeRepostStatus` (public posts, explicit approval, connected account, no uncertain retries). Native likes and unlikes: `nativeLikeState`, `prepareNativeLike`, `submitNativeLike`, `nativeLikeStatus` (connected authority, app enablement and explicit approval; resume pending actions instead of duplicating them). Reviewed replies and optional linked posts: `prepareNativeReply`, `submitNativeReply`, `nativeReplyStatus`, `nativeReplyHistory`, `prepareNativeReplyRemoval`, `removeNativeReply` (require deployed API support and app enablement). Keyless shared post links: `sharedNetworks(projectId)`, `getSharedConversation(projectId, { protocol, post, readRef? })`. Member public reading: `searchPublicPosts` (selected account required for Bluesky/ActivityPub; public Nostr search), `getConnectedSocialConversation` (preserve result readerAccountId/readRef), `publicNetworks`, `searchPublicProfiles`, `getPublicProfile`, `getPublicAuthorFeed`, `getFollowingFeed`, `getPublicConversation`, `getPublicTrends`, `getPublicTrendFeed`; radar stories: `getPublicRadarStory` (one story by id with its posts; one that has left the radar comes with its archived copy and the live story it continues as; the id may be a World Today edition story's, and the answer then names it in `editionStory` under the story's radar id), keyless `getSharedRadarStory(projectId, { cluster })` and `getSharedRadarPosts(projectId, { cluster })` (keyless reads carry images and videos only from outlets and members marked `visitorMedia`; signed-in reads keep all media, so a share card uses only those); private app subscriptions: `listSubscriptions`, `subscribeToProfile`, `unsubscribeFromProfile`, `onSubscriptionChange`. `list` returns bounded adapter `maturity`, `limitations`, and exhaustive `supported`/`planned`/`unsupported` implementation capabilities (not provider health or enablement); settings, candidate refresh/status, search, and read-only quarantined candidate review via `listCandidates` / `getCandidate` |
|
|
119
119
|
|
|
120
120
|
Full reference: see [llm.md](./llm.md) (ships with this package) or the type definitions in `dist/`.
|
|
121
121
|
|
|
@@ -351,7 +351,7 @@ import {
|
|
|
351
351
|
AuthorizationError, // 403 — API key lacks required scope
|
|
352
352
|
NotFoundError, // 404
|
|
353
353
|
ValidationError, // 400 — check error.details
|
|
354
|
-
RateLimitError, // 429 — includes retryAfter, upgradeUrl
|
|
354
|
+
RateLimitError, // 429 — includes retryAfter, resetAt, scope, upgradeUrl
|
|
355
355
|
ConflictError, // 409
|
|
356
356
|
} from '@recursiv/sdk'
|
|
357
357
|
|
|
@@ -364,7 +364,7 @@ try {
|
|
|
364
364
|
}
|
|
365
365
|
```
|
|
366
366
|
|
|
367
|
-
Auto-retry is built in. 429 and 5xx responses retry with exponential backoff (configurable via `maxRetries`, default 2).
|
|
367
|
+
Auto-retry is built in. 429 and 5xx responses retry with exponential backoff (configurable via `maxRetries`, default 2). A 429 with `X-RateLimit-Scope: action` refuses one member action past its budget and is never retried: its `RateLimitError` has `scope: 'action'` and `resetAt` (a `Date`, when that action's budget reopens).
|
|
368
368
|
|
|
369
369
|
## Pagination
|
|
370
370
|
|
package/dist/client.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { RecursivError, AuthenticationError, AuthorizationError, NotFoundError, ValidationError, InsufficientCreditsError, RateLimitError, ConflictError, errorBodyFromResponse, httpStatusMessage, } from './errors.js';
|
|
1
|
+
import { RecursivError, AuthenticationError, AuthorizationError, NotFoundError, ValidationError, InsufficientCreditsError, RateLimitError, ConflictError, errorBodyFromResponse, httpStatusMessage, isActionRateLimitScope, rateLimitDetailsFromResponse, } from './errors.js';
|
|
2
2
|
const DEFAULT_BASE_URL = 'https://api.recursiv.io/api/v1';
|
|
3
3
|
export class HttpClient {
|
|
4
4
|
#apiKey;
|
|
@@ -59,6 +59,13 @@ export class HttpClient {
|
|
|
59
59
|
if (response.status === 429 || response.status >= 500) {
|
|
60
60
|
if (attempt === this.maxRetries)
|
|
61
61
|
return response;
|
|
62
|
+
// X-RateLimit-Scope: action refuses this one action (a post, a follow,
|
|
63
|
+
// a report) past the member's budget. The API is not busy: the same
|
|
64
|
+
// request is refused again until the budget reopens, or, sent late,
|
|
65
|
+
// becomes a write the member saw refused. The caller gets the 429 now,
|
|
66
|
+
// with RateLimitError.scope and resetAt.
|
|
67
|
+
if (response.status === 429 && isActionRateLimitScope(response.headers))
|
|
68
|
+
return response;
|
|
62
69
|
const retryAfter = response.headers.get('retry-after');
|
|
63
70
|
const delay = retryAfter
|
|
64
71
|
? parseInt(retryAfter, 10) * 1000
|
|
@@ -163,7 +170,7 @@ export class HttpClient {
|
|
|
163
170
|
case 409:
|
|
164
171
|
throw new ConflictError(err);
|
|
165
172
|
case 429:
|
|
166
|
-
throw new RateLimitError(err);
|
|
173
|
+
throw new RateLimitError(err, rateLimitDetailsFromResponse(response.headers, err));
|
|
167
174
|
default:
|
|
168
175
|
throw new RecursivError(response.status, err);
|
|
169
176
|
}
|
package/dist/errors.d.ts
CHANGED
|
@@ -50,9 +50,36 @@ export declare class InsufficientCreditsError extends RecursivError {
|
|
|
50
50
|
readonly upgradeUrl: string;
|
|
51
51
|
constructor(body: ApiErrorBody['error']);
|
|
52
52
|
}
|
|
53
|
+
/** What a 429 says beyond its body: which budget refused it, and when that budget reopens. */
|
|
54
|
+
export interface RateLimitDetails {
|
|
55
|
+
/**
|
|
56
|
+
* `'action'` when the server refused one member action past its budget
|
|
57
|
+
* (a post, a follow, a report: `X-RateLimit-Scope: action`). Only that
|
|
58
|
+
* action is refused, the rest of the API is not busy, and sending it again
|
|
59
|
+
* before `resetAt` is refused again. `undefined` for any other 429.
|
|
60
|
+
*/
|
|
61
|
+
scope?: 'action';
|
|
62
|
+
/**
|
|
63
|
+
* When the refused budget reopens, for showing in the member's local time.
|
|
64
|
+
* From `X-RateLimit-Reset`, else `Retry-After`, else the body's
|
|
65
|
+
* `retry_after`; `undefined` when none of them says.
|
|
66
|
+
*/
|
|
67
|
+
resetAt?: Date;
|
|
68
|
+
}
|
|
69
|
+
/** Whether a response's headers mark its 429 as refusing one action (`X-RateLimit-Scope: action`). */
|
|
70
|
+
export declare function isActionRateLimitScope(headers: Pick<Headers, 'get'> | null | undefined): boolean;
|
|
71
|
+
/**
|
|
72
|
+
* The scope and reset of a 429, read from its headers, and from the body's
|
|
73
|
+
* `retry_after` (seconds) when neither reset header was sent.
|
|
74
|
+
*/
|
|
75
|
+
export declare function rateLimitDetailsFromResponse(headers: Pick<Headers, 'get'> | null | undefined, body?: Pick<ApiErrorBody['error'], 'retry_after'>, now?: number): RateLimitDetails;
|
|
53
76
|
export declare class RateLimitError extends RecursivError {
|
|
54
77
|
readonly upgradeUrl: string;
|
|
55
|
-
|
|
78
|
+
/** `'action'` when only this one action was refused (see RateLimitDetails.scope); otherwise undefined. */
|
|
79
|
+
readonly scope?: 'action';
|
|
80
|
+
/** When the refused budget reopens (see RateLimitDetails.resetAt); undefined when the server did not say. */
|
|
81
|
+
readonly resetAt?: Date;
|
|
82
|
+
constructor(body: ApiErrorBody['error'], details?: RateLimitDetails);
|
|
56
83
|
}
|
|
57
84
|
export declare class ConflictError extends RecursivError {
|
|
58
85
|
constructor(body: ApiErrorBody['error']);
|
package/dist/errors.js
CHANGED
|
@@ -110,11 +110,70 @@ export class InsufficientCreditsError extends RecursivError {
|
|
|
110
110
|
this.message += `\n Upgrade: ${this.upgradeUrl}`;
|
|
111
111
|
}
|
|
112
112
|
}
|
|
113
|
+
/** Whether a response's headers mark its 429 as refusing one action (`X-RateLimit-Scope: action`). */
|
|
114
|
+
export function isActionRateLimitScope(headers) {
|
|
115
|
+
return headers?.get?.('x-ratelimit-scope')?.trim().toLowerCase() === 'action';
|
|
116
|
+
}
|
|
117
|
+
// A bare number below this is seconds from now, not an epoch (1e9 s is 2001-09-09).
|
|
118
|
+
const EPOCH_SECONDS_FLOOR = 1e9;
|
|
119
|
+
// A bare number at or above this is epoch milliseconds (1e12 ms is 2001-09-09).
|
|
120
|
+
const EPOCH_MS_FLOOR = 1e12;
|
|
121
|
+
function dateOrUndefined(ms) {
|
|
122
|
+
return Number.isFinite(ms) ? new Date(ms) : undefined;
|
|
123
|
+
}
|
|
124
|
+
/** X-RateLimit-Reset: epoch seconds from Recursiv; epoch ms, seconds from now or an ISO/HTTP date elsewhere. */
|
|
125
|
+
function resetFromRateLimitReset(raw, now) {
|
|
126
|
+
const value = raw?.trim();
|
|
127
|
+
if (!value)
|
|
128
|
+
return undefined;
|
|
129
|
+
if (/^\d+(\.\d+)?$/.test(value)) {
|
|
130
|
+
const n = Number(value);
|
|
131
|
+
if (n >= EPOCH_MS_FLOOR)
|
|
132
|
+
return dateOrUndefined(n);
|
|
133
|
+
if (n >= EPOCH_SECONDS_FLOOR)
|
|
134
|
+
return dateOrUndefined(n * 1000);
|
|
135
|
+
return dateOrUndefined(now + n * 1000);
|
|
136
|
+
}
|
|
137
|
+
return dateOrUndefined(Date.parse(value));
|
|
138
|
+
}
|
|
139
|
+
/** Retry-After: seconds from now, or an HTTP date. */
|
|
140
|
+
function resetFromRetryAfter(raw, now) {
|
|
141
|
+
const value = raw?.trim();
|
|
142
|
+
if (!value)
|
|
143
|
+
return undefined;
|
|
144
|
+
if (/^\d+$/.test(value))
|
|
145
|
+
return dateOrUndefined(now + Number(value) * 1000);
|
|
146
|
+
return dateOrUndefined(Date.parse(value));
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* The scope and reset of a 429, read from its headers, and from the body's
|
|
150
|
+
* `retry_after` (seconds) when neither reset header was sent.
|
|
151
|
+
*/
|
|
152
|
+
export function rateLimitDetailsFromResponse(headers, body, now = Date.now()) {
|
|
153
|
+
const retryAfterSeconds = body?.retry_after;
|
|
154
|
+
const resetAt = resetFromRateLimitReset(headers?.get?.('x-ratelimit-reset'), now)
|
|
155
|
+
?? resetFromRetryAfter(headers?.get?.('retry-after'), now)
|
|
156
|
+
?? (typeof retryAfterSeconds === 'number' && retryAfterSeconds >= 0
|
|
157
|
+
? dateOrUndefined(now + retryAfterSeconds * 1000)
|
|
158
|
+
: undefined);
|
|
159
|
+
const details = {};
|
|
160
|
+
if (isActionRateLimitScope(headers))
|
|
161
|
+
details.scope = 'action';
|
|
162
|
+
if (resetAt)
|
|
163
|
+
details.resetAt = resetAt;
|
|
164
|
+
return details;
|
|
165
|
+
}
|
|
113
166
|
export class RateLimitError extends RecursivError {
|
|
114
167
|
upgradeUrl;
|
|
115
|
-
|
|
168
|
+
/** `'action'` when only this one action was refused (see RateLimitDetails.scope); otherwise undefined. */
|
|
169
|
+
scope;
|
|
170
|
+
/** When the refused budget reopens (see RateLimitDetails.resetAt); undefined when the server did not say. */
|
|
171
|
+
resetAt;
|
|
172
|
+
constructor(body, details = {}) {
|
|
116
173
|
super(429, body);
|
|
117
174
|
this.name = 'RateLimitError';
|
|
175
|
+
this.scope = details.scope;
|
|
176
|
+
this.resetAt = details.resetAt;
|
|
118
177
|
this.upgradeUrl = body?.upgrade_url || body.details?.upgrade_url || 'https://recursiv.io/pricing';
|
|
119
178
|
// Append upgrade hint for free tier users
|
|
120
179
|
if (body.message?.includes('free tier') ||
|
package/dist/index.d.ts
CHANGED
|
@@ -154,7 +154,7 @@ export { UsersResource } from './resources/users.js';
|
|
|
154
154
|
export { CommunitiesResource } from './resources/communities.js';
|
|
155
155
|
export { ChatResource } from './resources/chat.js';
|
|
156
156
|
export { E2eeResource } from './resources/e2ee.js';
|
|
157
|
-
export type { E2eeDevice, E2eeEnvelope, E2eeEvent, E2eeKeyPackageStatus, E2eeObjectDownload, E2eeObjectUpload, E2eeStatus, E2eeStatusReason } from './resources/e2ee.js';
|
|
157
|
+
export type { E2eeDevice, E2eeDevicesRealtimeEvent, E2eeEnvelope, E2eeEvent, E2eeKeyPackageStatus, E2eeLink, E2eeLinkCancelledBy, E2eeLinkProgress, E2eeLinkRealtimeEvent, E2eeLinkState, E2eeObjectDownload, E2eeObjectUpload, E2eeRealtimeEvent, E2eeStatus, E2eeStatusReason, } from './resources/e2ee.js';
|
|
158
158
|
export { ProjectsResource } from './resources/projects.js';
|
|
159
159
|
export { AgentsResource, SUPPORTED_MODELS } from './resources/agents.js';
|
|
160
160
|
export { EvidenceResource } from './resources/evidence.js';
|
|
@@ -240,7 +240,8 @@ export { TemplatesResource } from './resources/templates.js';
|
|
|
240
240
|
export type { Template, CreateTemplateInput, UpdateTemplateInput, ListTemplatesParams } from './resources/templates.js';
|
|
241
241
|
export { MediaResource } from './resources/media.js';
|
|
242
242
|
export type { GenerateImageInput, GeneratedImage, ImageProvider } from './resources/media.js';
|
|
243
|
-
export { RecursivError, AuthenticationError, AuthorizationError, LegacyHandoffError, NotFoundError, ValidationError, InsufficientCreditsError, RateLimitError, ConflictError, } from './errors.js';
|
|
243
|
+
export { RecursivError, AuthenticationError, AuthorizationError, LegacyHandoffError, NotFoundError, ValidationError, InsufficientCreditsError, RateLimitError, ConflictError, isActionRateLimitScope, } from './errors.js';
|
|
244
|
+
export type { RateLimitDetails } from './errors.js';
|
|
244
245
|
export { parseSSE } from './lib/sse.js';
|
|
245
246
|
export * from './lib/webhooks.js';
|
|
246
247
|
export type * from './types.js';
|
package/dist/index.js
CHANGED
|
@@ -277,6 +277,6 @@ export { HumanInputResource } from './resources/human-input.js';
|
|
|
277
277
|
export { SwarmsResource } from './resources/swarms.js';
|
|
278
278
|
export { TemplatesResource } from './resources/templates.js';
|
|
279
279
|
export { MediaResource } from './resources/media.js';
|
|
280
|
-
export { RecursivError, AuthenticationError, AuthorizationError, LegacyHandoffError, NotFoundError, ValidationError, InsufficientCreditsError, RateLimitError, ConflictError, } from './errors.js';
|
|
280
|
+
export { RecursivError, AuthenticationError, AuthorizationError, LegacyHandoffError, NotFoundError, ValidationError, InsufficientCreditsError, RateLimitError, ConflictError, isActionRateLimitScope, } from './errors.js';
|
|
281
281
|
export { parseSSE } from './lib/sse.js';
|
|
282
282
|
export * from './lib/webhooks.js';
|
package/dist/realtime.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Socket } from 'socket.io-client';
|
|
2
2
|
import { HttpClient } from './client.js';
|
|
3
|
+
import type { E2eeDevicesRealtimeEvent, E2eeLinkRealtimeEvent, E2eeRealtimeEvent } from './resources/e2ee.js';
|
|
3
4
|
export interface ChatMessageEvent {
|
|
4
5
|
id: string;
|
|
5
6
|
conversationId: string;
|
|
@@ -145,7 +146,8 @@ export declare class RealtimeClient {
|
|
|
145
146
|
* replacement. Returns an unsubscribe function.
|
|
146
147
|
*
|
|
147
148
|
* Server events include `notification`, `feed_update`,
|
|
148
|
-
* `conversation_created`, `chat_message`, `chat_typing
|
|
149
|
+
* `conversation_created`, `chat_message`, `chat_typing`, `agent_thinking`,
|
|
150
|
+
* `e2ee:event`, `e2ee:link` and `e2ee:devices`.
|
|
149
151
|
*/
|
|
150
152
|
on<T = unknown>(event: string, listener: (payload: T) => void): () => void;
|
|
151
153
|
/** Join a conversation room now, and again after every reconnect, until you leave it. */
|
|
@@ -157,4 +159,25 @@ export declare class RealtimeClient {
|
|
|
157
159
|
onMessageDelete(callback: (msg: ChatMessageDeleteEvent) => void): (() => void) | undefined;
|
|
158
160
|
onTyping(callback: (event: TypingEvent) => void): (() => void) | undefined;
|
|
159
161
|
onAgentThinking(callback: (event: AgentThinkingEvent) => void): (() => void) | undefined;
|
|
162
|
+
/**
|
|
163
|
+
* The E2EE doorbell after each append to an encrypted conversation this
|
|
164
|
+
* account belongs to: `{ conversationId, seq }`, plus the stored `event`
|
|
165
|
+
* when this socket may carry it (see E2eeRealtimeEvent). Pass `event` to the
|
|
166
|
+
* engine's `applyPushed`; without one, sync the conversation.
|
|
167
|
+
*/
|
|
168
|
+
onE2eeEvent(callback: (event: E2eeRealtimeEvent) => void): () => void;
|
|
169
|
+
/**
|
|
170
|
+
* A device link of this account changed: `{ linkId, state }`, state
|
|
171
|
+
* `scanned`, `approved`, `completed`, `progress` (at most once a second) or
|
|
172
|
+
* `cancelled`. Only on sockets opened by a sign-in with the link's exact
|
|
173
|
+
* binding; there is never an event when a link opens. Read the link with
|
|
174
|
+
* `r.e2ee.link(linkId)`; poll as well, since an event can be missed.
|
|
175
|
+
*/
|
|
176
|
+
onE2eeLink(callback: (event: E2eeLinkRealtimeEvent) => void): () => void;
|
|
177
|
+
/**
|
|
178
|
+
* One of this account's devices changed in a way the others act on:
|
|
179
|
+
* `{ deviceId, reason: 'packages' }` when it can now be claimed, or
|
|
180
|
+
* `{ deviceId, reason: 'revoked' }`.
|
|
181
|
+
*/
|
|
182
|
+
onE2eeDevices(callback: (event: E2eeDevicesRealtimeEvent) => void): () => void;
|
|
160
183
|
}
|
package/dist/realtime.js
CHANGED
|
@@ -368,7 +368,8 @@ export class RealtimeClient {
|
|
|
368
368
|
* replacement. Returns an unsubscribe function.
|
|
369
369
|
*
|
|
370
370
|
* Server events include `notification`, `feed_update`,
|
|
371
|
-
* `conversation_created`, `chat_message`, `chat_typing
|
|
371
|
+
* `conversation_created`, `chat_message`, `chat_typing`, `agent_thinking`,
|
|
372
|
+
* `e2ee:event`, `e2ee:link` and `e2ee:devices`.
|
|
372
373
|
*/
|
|
373
374
|
on(event, listener) {
|
|
374
375
|
// Every call is its own subscription, even for a function that is already
|
|
@@ -425,4 +426,31 @@ export class RealtimeClient {
|
|
|
425
426
|
onAgentThinking(callback) {
|
|
426
427
|
return this.on('agent_thinking', callback);
|
|
427
428
|
}
|
|
429
|
+
/**
|
|
430
|
+
* The E2EE doorbell after each append to an encrypted conversation this
|
|
431
|
+
* account belongs to: `{ conversationId, seq }`, plus the stored `event`
|
|
432
|
+
* when this socket may carry it (see E2eeRealtimeEvent). Pass `event` to the
|
|
433
|
+
* engine's `applyPushed`; without one, sync the conversation.
|
|
434
|
+
*/
|
|
435
|
+
onE2eeEvent(callback) {
|
|
436
|
+
return this.on('e2ee:event', callback);
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* A device link of this account changed: `{ linkId, state }`, state
|
|
440
|
+
* `scanned`, `approved`, `completed`, `progress` (at most once a second) or
|
|
441
|
+
* `cancelled`. Only on sockets opened by a sign-in with the link's exact
|
|
442
|
+
* binding; there is never an event when a link opens. Read the link with
|
|
443
|
+
* `r.e2ee.link(linkId)`; poll as well, since an event can be missed.
|
|
444
|
+
*/
|
|
445
|
+
onE2eeLink(callback) {
|
|
446
|
+
return this.on('e2ee:link', callback);
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* One of this account's devices changed in a way the others act on:
|
|
450
|
+
* `{ deviceId, reason: 'packages' }` when it can now be claimed, or
|
|
451
|
+
* `{ deviceId, reason: 'revoked' }`.
|
|
452
|
+
*/
|
|
453
|
+
onE2eeDevices(callback) {
|
|
454
|
+
return this.on('e2ee:devices', callback);
|
|
455
|
+
}
|
|
428
456
|
}
|
package/dist/resources/e2ee.d.ts
CHANGED
|
@@ -11,6 +11,15 @@ export interface E2eeDevice {
|
|
|
11
11
|
isRoot: boolean;
|
|
12
12
|
status: 'active' | 'revoked' | string;
|
|
13
13
|
revokedAt: string | null;
|
|
14
|
+
/** When the relay registered the device (the relay's clock). Absent on relays before 0.7.57. */
|
|
15
|
+
createdAt?: string;
|
|
16
|
+
/**
|
|
17
|
+
* When the device last fetched events, to the hour. Only in your own
|
|
18
|
+
* directory (`devices()` without `user_id`); null for another account's
|
|
19
|
+
* devices, when unknown, or when the relay cannot say. Absent on relays
|
|
20
|
+
* before 0.7.57.
|
|
21
|
+
*/
|
|
22
|
+
lastSeenAt?: string | null;
|
|
14
23
|
}
|
|
15
24
|
/** Opaque `recursiv-mls-v2-draft1` envelope with base64 MLS bytes. */
|
|
16
25
|
export interface E2eeEnvelope {
|
|
@@ -30,6 +39,33 @@ export interface E2eeEvent {
|
|
|
30
39
|
welcome: string | null;
|
|
31
40
|
createdAt: string;
|
|
32
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* The realtime `e2ee:event` doorbell (`r.realtime.onE2eeEvent`), sent after
|
|
44
|
+
* every append to an encrypted conversation to each member account's sockets.
|
|
45
|
+
* It never carries plaintext and has no ordering authority.
|
|
46
|
+
*
|
|
47
|
+
* `event` is present only for a small application message (an ordinary or a
|
|
48
|
+
* quiet one; never a commit, never a Welcome, never one whose envelope is over
|
|
49
|
+
* 16 KB), and only on a socket whose credential could read it with `events()`:
|
|
50
|
+
* one authenticated by the account owner's session or by an owner key whose
|
|
51
|
+
* app or organization binding reaches the conversation. A socket opened with
|
|
52
|
+
* an agent-scoped key gets the content-free form only. When present it is the
|
|
53
|
+
* stored event exactly as `events()` returns it, JSON byte for byte.
|
|
54
|
+
*
|
|
55
|
+
* With the shared engine, hand `event` to `applyPushed(conversationId, event)`
|
|
56
|
+
* on the device session: it applies the event without a relay read when it is
|
|
57
|
+
* the next one this device expects, and syncs otherwise. Without `event`, sync.
|
|
58
|
+
*/
|
|
59
|
+
export interface E2eeRealtimeEvent {
|
|
60
|
+
conversationId: string;
|
|
61
|
+
/** The relay sequence the append was stored at. */
|
|
62
|
+
seq: number;
|
|
63
|
+
/** The stored event, when this socket may carry it (see above). */
|
|
64
|
+
event?: E2eeEvent & {
|
|
65
|
+
kind: 'application';
|
|
66
|
+
welcome: null;
|
|
67
|
+
};
|
|
68
|
+
}
|
|
33
69
|
/** One-time key packages a device still has to hand out. */
|
|
34
70
|
export interface E2eeKeyPackageStatus {
|
|
35
71
|
/** Unclaimed packages that have not expired. Once this reaches 0 nobody can add the device to a new conversation. */
|
|
@@ -69,6 +105,72 @@ export interface E2eeStatus {
|
|
|
69
105
|
enabled: boolean;
|
|
70
106
|
/** Present only when `enabled` is false. Treat any unrecognised reason as off. */
|
|
71
107
|
reason?: E2eeStatusReason;
|
|
108
|
+
/**
|
|
109
|
+
* True when this relay can link a new device by scanning (`createLink`).
|
|
110
|
+
* False or absent: offer the codes fallback only as a choice the person
|
|
111
|
+
* makes, never switch to it on their behalf.
|
|
112
|
+
*/
|
|
113
|
+
links?: boolean;
|
|
114
|
+
}
|
|
115
|
+
/** A device link's state. An open link past `expiresAt` reads as `cancelled` with `cancelledBy: 'expired'`. */
|
|
116
|
+
export type E2eeLinkState = 'open' | 'approved' | 'completed' | 'cancelled';
|
|
117
|
+
/** Why a link was cancelled. */
|
|
118
|
+
export type E2eeLinkCancelledBy = 'creator' | 'superseded' | 'declined' | 'expired';
|
|
119
|
+
/** How far the approving device has got adding the new device to the account's encrypted chats. */
|
|
120
|
+
export interface E2eeLinkProgress {
|
|
121
|
+
done: number;
|
|
122
|
+
total: number;
|
|
123
|
+
joined: number;
|
|
124
|
+
notHeld: number;
|
|
125
|
+
failed: number;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* A device link (spec RCV1): the relay's short-lived mailbox between a new
|
|
129
|
+
* device, which shows a QR code, and an approved device of the same account,
|
|
130
|
+
* which scans it and posts an approval only the new device can verify. The
|
|
131
|
+
* relay stores public data only and is not trusted for integrity: the engine
|
|
132
|
+
* checks the approval's tag, the chain and the directory itself.
|
|
133
|
+
*/
|
|
134
|
+
export interface E2eeLink {
|
|
135
|
+
/** 128 random bits as unpadded base64url (22 characters); the QR carries these bytes. */
|
|
136
|
+
id: string;
|
|
137
|
+
state: E2eeLinkState;
|
|
138
|
+
deviceName: string;
|
|
139
|
+
signingKey: string;
|
|
140
|
+
/** The new device's self-reported label. Chosen by whoever made the link: never show it as the app's own words. */
|
|
141
|
+
label: string | null;
|
|
142
|
+
createdAt: string;
|
|
143
|
+
expiresAt: string;
|
|
144
|
+
/** The relay's clock when it answered: count down with `expiresAt - now`, never the device's own clock. */
|
|
145
|
+
now: string;
|
|
146
|
+
scannedAt: string | null;
|
|
147
|
+
/** Another link of this account is open (another sign-in is linking too). */
|
|
148
|
+
otherOpen: boolean;
|
|
149
|
+
cancelledBy: E2eeLinkCancelledBy | null;
|
|
150
|
+
/** The approval body and its tag: only for the sign-in that created the link, null for everyone else. */
|
|
151
|
+
approval: string | null;
|
|
152
|
+
tag: string | null;
|
|
153
|
+
approverDeviceId: string | null;
|
|
154
|
+
progress: E2eeLinkProgress | null;
|
|
155
|
+
lastConversationId: string | null;
|
|
156
|
+
needPackages: boolean;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* The realtime `e2ee:link` event (`r.realtime.onE2eeLink`): a link changed.
|
|
160
|
+
* Content-free; read the link with `link(id)`. There is never an `open` event.
|
|
161
|
+
*/
|
|
162
|
+
export interface E2eeLinkRealtimeEvent {
|
|
163
|
+
linkId: string;
|
|
164
|
+
state: 'scanned' | 'approved' | 'completed' | 'progress' | 'cancelled';
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* The realtime `e2ee:devices` event (`r.realtime.onE2eeDevices`): `packages`
|
|
168
|
+
* when one of the account's devices can now be claimed (it published after
|
|
169
|
+
* having none, or after a claim for it failed); `revoked` when one was revoked.
|
|
170
|
+
*/
|
|
171
|
+
export interface E2eeDevicesRealtimeEvent {
|
|
172
|
+
deviceId: string;
|
|
173
|
+
reason: 'packages' | 'revoked';
|
|
72
174
|
}
|
|
73
175
|
/**
|
|
74
176
|
* E2EE v2 ciphertext transport. The server relays envelopes and public
|
|
@@ -195,4 +297,71 @@ export declare class E2eeResource {
|
|
|
195
297
|
reference: string;
|
|
196
298
|
deleted: boolean;
|
|
197
299
|
}>>;
|
|
300
|
+
/**
|
|
301
|
+
* Start linking this new, not yet registered device: `POST /e2ee/links`.
|
|
302
|
+
* Answers the open link (180 s on the relay's clock). A new link cancels
|
|
303
|
+
* this sign-in's previous open link. `label` is 1-48 letters, numbers,
|
|
304
|
+
* spaces and `. , ' ( ) + -`; reserved words are refused (`invalid_label`).
|
|
305
|
+
* Not retried: a 5xx is answered at once, and the caller decides.
|
|
306
|
+
*/
|
|
307
|
+
createLink(input: {
|
|
308
|
+
device_name: string;
|
|
309
|
+
signing_key: string;
|
|
310
|
+
label?: string;
|
|
311
|
+
}): Promise<SingleResponse<E2eeLink>>;
|
|
312
|
+
/** Read a link: `GET /e2ee/links/:id`. `approval` and `tag` only for the sign-in that created it. Not retried: poll instead. */
|
|
313
|
+
link(id: string): Promise<SingleResponse<E2eeLink>>;
|
|
314
|
+
/** The approving device scanned the code: once per link, it keeps the link open 120 s past the scan. */
|
|
315
|
+
scannedLink(id: string, input: {
|
|
316
|
+
device_id: string;
|
|
317
|
+
}): Promise<SingleResponse<E2eeLink>>;
|
|
318
|
+
/**
|
|
319
|
+
* Post the approval body and its tag, once. Retry-safe: the same approving
|
|
320
|
+
* device is answered 200 on an approved or completed link. The answer never
|
|
321
|
+
* carries the approval or tag.
|
|
322
|
+
*/
|
|
323
|
+
approveLink(id: string, input: {
|
|
324
|
+
device_id: string;
|
|
325
|
+
approval: string;
|
|
326
|
+
tag: string;
|
|
327
|
+
}): Promise<SingleResponse<E2eeLink>>;
|
|
328
|
+
/** The new device, now registered, finished: the sign-in that created the link only. */
|
|
329
|
+
completeLink(id: string, input: {
|
|
330
|
+
device_id: string;
|
|
331
|
+
}): Promise<SingleResponse<E2eeLink>>;
|
|
332
|
+
/** The approving device, from the sign-in that approved, reports how many chats it has added the new device to. */
|
|
333
|
+
linkProgress(id: string, input: {
|
|
334
|
+
device_id: string;
|
|
335
|
+
done: number;
|
|
336
|
+
total: number;
|
|
337
|
+
joined: number;
|
|
338
|
+
not_held: number;
|
|
339
|
+
failed: number;
|
|
340
|
+
last_conversation_id?: string;
|
|
341
|
+
need_packages?: boolean;
|
|
342
|
+
}): Promise<SingleResponse<E2eeLink>>;
|
|
343
|
+
/**
|
|
344
|
+
* Cancel a link. The sign-in that created it cancels it while open or
|
|
345
|
+
* approved; an active device of the account declines an open one with
|
|
346
|
+
* `{ device_id, reason: 'declined' }`, and once the link is scanned only
|
|
347
|
+
* from the sign-in that scanned it.
|
|
348
|
+
*/
|
|
349
|
+
cancelLink(id: string, input?: {
|
|
350
|
+
device_id?: string;
|
|
351
|
+
reason?: 'cancelled' | 'declined';
|
|
352
|
+
}): Promise<SingleResponse<E2eeLink>>;
|
|
353
|
+
/**
|
|
354
|
+
* The ids of the encrypted conversations this account belongs to, in this
|
|
355
|
+
* key's scope, ordered by id: pass `next` as `after` for the next page
|
|
356
|
+
* (`limit` 1-200, default 100).
|
|
357
|
+
*/
|
|
358
|
+
conversations(params?: {
|
|
359
|
+
after?: string;
|
|
360
|
+
limit?: number;
|
|
361
|
+
}): Promise<{
|
|
362
|
+
data: {
|
|
363
|
+
conversationId: string;
|
|
364
|
+
}[];
|
|
365
|
+
next: string | null;
|
|
366
|
+
}>;
|
|
198
367
|
}
|
package/dist/resources/e2ee.js
CHANGED
|
@@ -91,4 +91,61 @@ export class E2eeResource {
|
|
|
91
91
|
deleteObject(objectId, params) {
|
|
92
92
|
return this.client.delete(`/e2ee/objects/${encodeURIComponent(objectId)}?device_id=${encodeURIComponent(params.device_id)}`);
|
|
93
93
|
}
|
|
94
|
+
// ── Device links (spec RCV1 §6) ──
|
|
95
|
+
// Names match the engine's optional transport methods, so `r.e2ee` can be
|
|
96
|
+
// passed to the engine as its transport unchanged. Link refusals are 409
|
|
97
|
+
// (`link_limit`, `no_root`, `already_registered`, `conflict`), never 429;
|
|
98
|
+
// `invalid_label` is 400; `storage_unavailable` is a temporary 503: keep
|
|
99
|
+
// polling until the link's `expiresAt`.
|
|
100
|
+
/**
|
|
101
|
+
* Start linking this new, not yet registered device: `POST /e2ee/links`.
|
|
102
|
+
* Answers the open link (180 s on the relay's clock). A new link cancels
|
|
103
|
+
* this sign-in's previous open link. `label` is 1-48 letters, numbers,
|
|
104
|
+
* spaces and `. , ' ( ) + -`; reserved words are refused (`invalid_label`).
|
|
105
|
+
* Not retried: a 5xx is answered at once, and the caller decides.
|
|
106
|
+
*/
|
|
107
|
+
createLink(input) {
|
|
108
|
+
return this.client.post('/e2ee/links', input, { retry: false });
|
|
109
|
+
}
|
|
110
|
+
/** Read a link: `GET /e2ee/links/:id`. `approval` and `tag` only for the sign-in that created it. Not retried: poll instead. */
|
|
111
|
+
link(id) {
|
|
112
|
+
return this.client.get(`/e2ee/links/${encodeURIComponent(id)}`, undefined, { retry: false });
|
|
113
|
+
}
|
|
114
|
+
/** The approving device scanned the code: once per link, it keeps the link open 120 s past the scan. */
|
|
115
|
+
scannedLink(id, input) {
|
|
116
|
+
return this.client.post(`/e2ee/links/${encodeURIComponent(id)}/scanned`, input);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Post the approval body and its tag, once. Retry-safe: the same approving
|
|
120
|
+
* device is answered 200 on an approved or completed link. The answer never
|
|
121
|
+
* carries the approval or tag.
|
|
122
|
+
*/
|
|
123
|
+
approveLink(id, input) {
|
|
124
|
+
return this.client.post(`/e2ee/links/${encodeURIComponent(id)}/approve`, input, { retry: true });
|
|
125
|
+
}
|
|
126
|
+
/** The new device, now registered, finished: the sign-in that created the link only. */
|
|
127
|
+
completeLink(id, input) {
|
|
128
|
+
return this.client.post(`/e2ee/links/${encodeURIComponent(id)}/complete`, input);
|
|
129
|
+
}
|
|
130
|
+
/** The approving device, from the sign-in that approved, reports how many chats it has added the new device to. */
|
|
131
|
+
linkProgress(id, input) {
|
|
132
|
+
return this.client.post(`/e2ee/links/${encodeURIComponent(id)}/progress`, input);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Cancel a link. The sign-in that created it cancels it while open or
|
|
136
|
+
* approved; an active device of the account declines an open one with
|
|
137
|
+
* `{ device_id, reason: 'declined' }`, and once the link is scanned only
|
|
138
|
+
* from the sign-in that scanned it.
|
|
139
|
+
*/
|
|
140
|
+
cancelLink(id, input) {
|
|
141
|
+
return this.client.post(`/e2ee/links/${encodeURIComponent(id)}/cancel`, input ?? {});
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* The ids of the encrypted conversations this account belongs to, in this
|
|
145
|
+
* key's scope, ordered by id: pass `next` as `after` for the next page
|
|
146
|
+
* (`limit` 1-200, default 100).
|
|
147
|
+
*/
|
|
148
|
+
conversations(params) {
|
|
149
|
+
return this.client.get('/e2ee/conversations', params);
|
|
150
|
+
}
|
|
94
151
|
}
|
|
@@ -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[];
|
|
@@ -1243,6 +1266,11 @@ export interface SocialRadarStory {
|
|
|
1243
1266
|
id: string;
|
|
1244
1267
|
label: string;
|
|
1245
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;
|
|
1246
1274
|
}
|
|
1247
1275
|
/** Read options for callers that hold their own deadline, such as a discovery page. */
|
|
1248
1276
|
export interface SocialReadOptions {
|
|
@@ -1352,7 +1380,7 @@ export declare class ProtocolsResource {
|
|
|
1352
1380
|
getSharedRadarPosts(projectId: string, params: {
|
|
1353
1381
|
cluster: string;
|
|
1354
1382
|
}): Promise<SingleResponse<SocialRadarPosts>>;
|
|
1355
|
-
/** 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. */
|
|
1356
1384
|
getSharedRadarStory(projectId: string, params: {
|
|
1357
1385
|
cluster: string;
|
|
1358
1386
|
}): Promise<SingleResponse<SocialRadarStory>>;
|
|
@@ -1417,7 +1445,10 @@ export declare class ProtocolsResource {
|
|
|
1417
1445
|
getPublicRadarPosts(params: {
|
|
1418
1446
|
cluster: string;
|
|
1419
1447
|
}, options?: SocialReadOptions): Promise<SingleResponse<SocialRadarPosts>>;
|
|
1420
|
-
/**
|
|
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
|
+
*/
|
|
1421
1452
|
getPublicRadarStory(params: {
|
|
1422
1453
|
cluster: string;
|
|
1423
1454
|
}, options?: SocialReadOptions): Promise<SingleResponse<SocialRadarStory>>;
|
|
@@ -113,7 +113,7 @@ export class ProtocolsResource {
|
|
|
113
113
|
getSharedRadarPosts(projectId, params) {
|
|
114
114
|
return this.client.get(`/protocols/public/${encodeURIComponent(projectId)}/radar/posts`, { cluster: params.cluster });
|
|
115
115
|
}
|
|
116
|
-
/** 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. */
|
|
117
117
|
getSharedRadarStory(projectId, params) {
|
|
118
118
|
return this.client.get(`/protocols/public/${encodeURIComponent(projectId)}/radar/story`, { cluster: params.cluster });
|
|
119
119
|
}
|
|
@@ -181,7 +181,10 @@ export class ProtocolsResource {
|
|
|
181
181
|
getPublicRadarPosts(params, options) {
|
|
182
182
|
return this.client.get('/protocols/social/radar/posts', { cluster: params.cluster }, ...readOptions(options));
|
|
183
183
|
}
|
|
184
|
-
/**
|
|
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
|
+
*/
|
|
185
188
|
getPublicRadarStory(params, options) {
|
|
186
189
|
return this.client.get('/protocols/social/radar/story', { cluster: params.cluster }, ...readOptions(options));
|
|
187
190
|
}
|
package/llm.md
CHANGED
|
@@ -516,6 +516,22 @@ await r.e2ee.appendEvent(conversationId, { device_id: device.id, envelope }); //
|
|
|
516
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
|
|
517
517
|
const { data: page } = await r.e2ee.events(conversationId, { device_id: device.id, after_seq: 0 });
|
|
518
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'
|
|
519
535
|
```
|
|
520
536
|
|
|
521
537
|
### r.realtime — WebSocket Chat Client
|
|
@@ -551,7 +567,7 @@ unsub()
|
|
|
551
567
|
const off = r.realtime.on('notification', (n) => refreshBadge(n))
|
|
552
568
|
```
|
|
553
569
|
|
|
554
|
-
Events: `onMessage`, `onMessageEdit`, `onMessageDelete`, `onTyping`, `onAgentThinking`, `on(event, fn)`
|
|
570
|
+
Events: `onMessage`, `onMessageEdit`, `onMessageDelete`, `onTyping`, `onAgentThinking`, `onE2eeEvent`, `onE2eeLink`, `onE2eeDevices`, `on(event, fn)`
|
|
555
571
|
|
|
556
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.
|
|
557
573
|
|
|
@@ -809,10 +825,10 @@ Pagination: pass `limit` and `offset`. Check `meta.has_more`.
|
|
|
809
825
|
| `AuthorizationError` | 403 | API key lacks required scope |
|
|
810
826
|
| `NotFoundError` | 404 | Resource doesn't exist or no access |
|
|
811
827
|
| `ValidationError` | 400 | Invalid input (check `error.details`) |
|
|
812
|
-
| `RateLimitError` | 429 | Rate limit hit (`
|
|
828
|
+
| `RateLimitError` | 429 | Rate limit hit (`retryAfter` seconds, `resetAt` Date, `scope`) |
|
|
813
829
|
| `ConflictError` | 409 | Resource already exists |
|
|
814
830
|
|
|
815
|
-
Auto-retry on 429/5xx built in (exponential backoff, default `maxRetries: 2`). 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).
|
|
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).
|
|
816
832
|
|
|
817
833
|
### Idempotency
|
|
818
834
|
|