@recursiv/sdk 0.7.55 → 0.7.58
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 +198 -1
- package/dist/resources/e2ee.js +68 -2
- package/dist/resources/protocols.d.ts +62 -6
- package/dist/resources/protocols.js +5 -2
- package/dist/resources/search.d.ts +9 -1
- package/dist/types.d.ts +39 -0
- package/llm.md +21 -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
|
|
78
|
+
| `r.e2ee` | `status`, `registerDevice`, `devices`, `revokeDevice` (`{ cascade }` to also revoke devices approved through it; 409 `has_dependents` otherwise), `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
|
|
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 left the radar within its minimum lifetime, or within the last half hour, still answers live with `liveUntil` and the story that continues it; one that has left for longer comes with its archived copy and the live story it continues as; the id may be a World Today edition story's, and the answer then names it in `editionStory` under the story's radar id), 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, E2eeHasDependentsDetails, E2eeKeyPackageStatus, E2eeLink, E2eeLinkCancelledBy, E2eeLinkProgress, E2eeLinkRealtimeEvent, E2eeLinkState, E2eeObjectDownload, E2eeObjectUpload, E2eeRealtimeEvent, E2eeRevokeResponse, 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,89 @@ 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 `details` of a 409 `has_dependents` refusal from `revokeDevice` (read
|
|
168
|
+
* them from the thrown `ConflictError`'s `details`): the ids of the account's
|
|
169
|
+
* active devices that were approved through the device and would stop
|
|
170
|
+
* working with it, and whether it is the account's root (main) device, which
|
|
171
|
+
* every device chains to; once the root is revoked the account cannot link a
|
|
172
|
+
* new device.
|
|
173
|
+
*/
|
|
174
|
+
export interface E2eeHasDependentsDetails {
|
|
175
|
+
dependents: string[];
|
|
176
|
+
root: boolean;
|
|
177
|
+
}
|
|
178
|
+
/** `revokeDevice`'s answer: the device, and every device the call revoked (the target first). */
|
|
179
|
+
export interface E2eeRevokeResponse extends SingleResponse<E2eeDevice> {
|
|
180
|
+
/** Absent on relays before 0.7.58. Empty when the device was already revoked and nothing else was. */
|
|
181
|
+
revoked?: string[];
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* The realtime `e2ee:devices` event (`r.realtime.onE2eeDevices`): `packages`
|
|
185
|
+
* when one of the account's devices can now be claimed (it published after
|
|
186
|
+
* having none, or after a claim for it failed); `revoked` when one was revoked.
|
|
187
|
+
*/
|
|
188
|
+
export interface E2eeDevicesRealtimeEvent {
|
|
189
|
+
deviceId: string;
|
|
190
|
+
reason: 'packages' | 'revoked';
|
|
72
191
|
}
|
|
73
192
|
/**
|
|
74
193
|
* E2EE v2 ciphertext transport. The server relays envelopes and public
|
|
@@ -101,7 +220,18 @@ export declare class E2eeResource {
|
|
|
101
220
|
}): Promise<{
|
|
102
221
|
data: E2eeDevice[];
|
|
103
222
|
}>;
|
|
104
|
-
|
|
223
|
+
/**
|
|
224
|
+
* Revoke one of your devices. When other active devices were approved
|
|
225
|
+
* through it, or it is the account's root (main) device, the relay changes
|
|
226
|
+
* nothing and throws a `ConflictError` with code `has_dependents` and
|
|
227
|
+
* `details: E2eeHasDependentsDetails`, so the app can say which devices
|
|
228
|
+
* would be signed out. Call again with `{ cascade: true }` to revoke it and
|
|
229
|
+
* every device chained through it (relays from 0.7.58; older relays ignore
|
|
230
|
+
* the body and revoke only the device).
|
|
231
|
+
*/
|
|
232
|
+
revokeDevice(deviceId: string, input?: {
|
|
233
|
+
cascade?: boolean;
|
|
234
|
+
}): Promise<E2eeRevokeResponse>;
|
|
105
235
|
/** Publish one-time key packages for one of your devices; `available` is how many it now has. */
|
|
106
236
|
publishKeyPackages(input: {
|
|
107
237
|
device_id: string;
|
|
@@ -195,4 +325,71 @@ export declare class E2eeResource {
|
|
|
195
325
|
reference: string;
|
|
196
326
|
deleted: boolean;
|
|
197
327
|
}>>;
|
|
328
|
+
/**
|
|
329
|
+
* Start linking this new, not yet registered device: `POST /e2ee/links`.
|
|
330
|
+
* Answers the open link (180 s on the relay's clock). A new link cancels
|
|
331
|
+
* this sign-in's previous open link. `label` is 1-48 letters, numbers,
|
|
332
|
+
* spaces and `. , ' ( ) + -`; reserved words are refused (`invalid_label`).
|
|
333
|
+
* Not retried: a 5xx is answered at once, and the caller decides.
|
|
334
|
+
*/
|
|
335
|
+
createLink(input: {
|
|
336
|
+
device_name: string;
|
|
337
|
+
signing_key: string;
|
|
338
|
+
label?: string;
|
|
339
|
+
}): Promise<SingleResponse<E2eeLink>>;
|
|
340
|
+
/** Read a link: `GET /e2ee/links/:id`. `approval` and `tag` only for the sign-in that created it. Not retried: poll instead. */
|
|
341
|
+
link(id: string): Promise<SingleResponse<E2eeLink>>;
|
|
342
|
+
/** The approving device scanned the code: once per link, it keeps the link open 120 s past the scan. */
|
|
343
|
+
scannedLink(id: string, input: {
|
|
344
|
+
device_id: string;
|
|
345
|
+
}): Promise<SingleResponse<E2eeLink>>;
|
|
346
|
+
/**
|
|
347
|
+
* Post the approval body and its tag, once. Retry-safe: the same approving
|
|
348
|
+
* device is answered 200 on an approved or completed link. The answer never
|
|
349
|
+
* carries the approval or tag.
|
|
350
|
+
*/
|
|
351
|
+
approveLink(id: string, input: {
|
|
352
|
+
device_id: string;
|
|
353
|
+
approval: string;
|
|
354
|
+
tag: string;
|
|
355
|
+
}): Promise<SingleResponse<E2eeLink>>;
|
|
356
|
+
/** The new device, now registered, finished: the sign-in that created the link only. */
|
|
357
|
+
completeLink(id: string, input: {
|
|
358
|
+
device_id: string;
|
|
359
|
+
}): Promise<SingleResponse<E2eeLink>>;
|
|
360
|
+
/** The approving device, from the sign-in that approved, reports how many chats it has added the new device to. */
|
|
361
|
+
linkProgress(id: string, input: {
|
|
362
|
+
device_id: string;
|
|
363
|
+
done: number;
|
|
364
|
+
total: number;
|
|
365
|
+
joined: number;
|
|
366
|
+
not_held: number;
|
|
367
|
+
failed: number;
|
|
368
|
+
last_conversation_id?: string;
|
|
369
|
+
need_packages?: boolean;
|
|
370
|
+
}): Promise<SingleResponse<E2eeLink>>;
|
|
371
|
+
/**
|
|
372
|
+
* Cancel a link. The sign-in that created it cancels it while open or
|
|
373
|
+
* approved; an active device of the account declines an open one with
|
|
374
|
+
* `{ device_id, reason: 'declined' }`, and once the link is scanned only
|
|
375
|
+
* from the sign-in that scanned it.
|
|
376
|
+
*/
|
|
377
|
+
cancelLink(id: string, input?: {
|
|
378
|
+
device_id?: string;
|
|
379
|
+
reason?: 'cancelled' | 'declined';
|
|
380
|
+
}): Promise<SingleResponse<E2eeLink>>;
|
|
381
|
+
/**
|
|
382
|
+
* The ids of the encrypted conversations this account belongs to, in this
|
|
383
|
+
* key's scope, ordered by id: pass `next` as `after` for the next page
|
|
384
|
+
* (`limit` 1-200, default 100).
|
|
385
|
+
*/
|
|
386
|
+
conversations(params?: {
|
|
387
|
+
after?: string;
|
|
388
|
+
limit?: number;
|
|
389
|
+
}): Promise<{
|
|
390
|
+
data: {
|
|
391
|
+
conversationId: string;
|
|
392
|
+
}[];
|
|
393
|
+
next: string | null;
|
|
394
|
+
}>;
|
|
198
395
|
}
|
package/dist/resources/e2ee.js
CHANGED
|
@@ -28,8 +28,17 @@ export class E2eeResource {
|
|
|
28
28
|
devices(params) {
|
|
29
29
|
return this.client.get('/e2ee/devices', params);
|
|
30
30
|
}
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Revoke one of your devices. When other active devices were approved
|
|
33
|
+
* through it, or it is the account's root (main) device, the relay changes
|
|
34
|
+
* nothing and throws a `ConflictError` with code `has_dependents` and
|
|
35
|
+
* `details: E2eeHasDependentsDetails`, so the app can say which devices
|
|
36
|
+
* would be signed out. Call again with `{ cascade: true }` to revoke it and
|
|
37
|
+
* every device chained through it (relays from 0.7.58; older relays ignore
|
|
38
|
+
* the body and revoke only the device).
|
|
39
|
+
*/
|
|
40
|
+
revokeDevice(deviceId, input) {
|
|
41
|
+
return this.client.post(`/e2ee/devices/${encodeURIComponent(deviceId)}/revoke`, input ?? {});
|
|
33
42
|
}
|
|
34
43
|
/** Publish one-time key packages for one of your devices; `available` is how many it now has. */
|
|
35
44
|
publishKeyPackages(input) {
|
|
@@ -91,4 +100,61 @@ export class E2eeResource {
|
|
|
91
100
|
deleteObject(objectId, params) {
|
|
92
101
|
return this.client.delete(`/e2ee/objects/${encodeURIComponent(objectId)}?device_id=${encodeURIComponent(params.device_id)}`);
|
|
93
102
|
}
|
|
103
|
+
// ── Device links (spec RCV1 §6) ──
|
|
104
|
+
// Names match the engine's optional transport methods, so `r.e2ee` can be
|
|
105
|
+
// passed to the engine as its transport unchanged. Link refusals are 409
|
|
106
|
+
// (`link_limit`, `no_root`, `already_registered`, `conflict`), never 429;
|
|
107
|
+
// `invalid_label` is 400; `storage_unavailable` is a temporary 503: keep
|
|
108
|
+
// polling until the link's `expiresAt`.
|
|
109
|
+
/**
|
|
110
|
+
* Start linking this new, not yet registered device: `POST /e2ee/links`.
|
|
111
|
+
* Answers the open link (180 s on the relay's clock). A new link cancels
|
|
112
|
+
* this sign-in's previous open link. `label` is 1-48 letters, numbers,
|
|
113
|
+
* spaces and `. , ' ( ) + -`; reserved words are refused (`invalid_label`).
|
|
114
|
+
* Not retried: a 5xx is answered at once, and the caller decides.
|
|
115
|
+
*/
|
|
116
|
+
createLink(input) {
|
|
117
|
+
return this.client.post('/e2ee/links', input, { retry: false });
|
|
118
|
+
}
|
|
119
|
+
/** Read a link: `GET /e2ee/links/:id`. `approval` and `tag` only for the sign-in that created it. Not retried: poll instead. */
|
|
120
|
+
link(id) {
|
|
121
|
+
return this.client.get(`/e2ee/links/${encodeURIComponent(id)}`, undefined, { retry: false });
|
|
122
|
+
}
|
|
123
|
+
/** The approving device scanned the code: once per link, it keeps the link open 120 s past the scan. */
|
|
124
|
+
scannedLink(id, input) {
|
|
125
|
+
return this.client.post(`/e2ee/links/${encodeURIComponent(id)}/scanned`, input);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Post the approval body and its tag, once. Retry-safe: the same approving
|
|
129
|
+
* device is answered 200 on an approved or completed link. The answer never
|
|
130
|
+
* carries the approval or tag.
|
|
131
|
+
*/
|
|
132
|
+
approveLink(id, input) {
|
|
133
|
+
return this.client.post(`/e2ee/links/${encodeURIComponent(id)}/approve`, input, { retry: true });
|
|
134
|
+
}
|
|
135
|
+
/** The new device, now registered, finished: the sign-in that created the link only. */
|
|
136
|
+
completeLink(id, input) {
|
|
137
|
+
return this.client.post(`/e2ee/links/${encodeURIComponent(id)}/complete`, input);
|
|
138
|
+
}
|
|
139
|
+
/** The approving device, from the sign-in that approved, reports how many chats it has added the new device to. */
|
|
140
|
+
linkProgress(id, input) {
|
|
141
|
+
return this.client.post(`/e2ee/links/${encodeURIComponent(id)}/progress`, input);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Cancel a link. The sign-in that created it cancels it while open or
|
|
145
|
+
* approved; an active device of the account declines an open one with
|
|
146
|
+
* `{ device_id, reason: 'declined' }`, and once the link is scanned only
|
|
147
|
+
* from the sign-in that scanned it.
|
|
148
|
+
*/
|
|
149
|
+
cancelLink(id, input) {
|
|
150
|
+
return this.client.post(`/e2ee/links/${encodeURIComponent(id)}/cancel`, input ?? {});
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* The ids of the encrypted conversations this account belongs to, in this
|
|
154
|
+
* key's scope, ordered by id: pass `next` as `after` for the next page
|
|
155
|
+
* (`limit` 1-200, default 100).
|
|
156
|
+
*/
|
|
157
|
+
conversations(params) {
|
|
158
|
+
return this.client.get('/e2ee/conversations', params);
|
|
159
|
+
}
|
|
94
160
|
}
|
|
@@ -970,13 +970,30 @@ 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
|
*/
|
|
978
992
|
countsObservedAt?: string;
|
|
979
|
-
/**
|
|
993
|
+
/**
|
|
994
|
+
* Pixel dimensions when the source declares them (Bluesky aspect ratio, Mastodon attachment meta, Nostr NIP-92 `dim`), so a
|
|
995
|
+
* card can take the clip's shape before it loads. Each entry of a sensitive post carries `sensitive: true`.
|
|
996
|
+
*/
|
|
980
997
|
media: Array<{
|
|
981
998
|
type: 'image' | 'video';
|
|
982
999
|
url: string;
|
|
@@ -984,8 +1001,15 @@ export interface SocialPost {
|
|
|
984
1001
|
poster?: string;
|
|
985
1002
|
width?: number;
|
|
986
1003
|
height?: number;
|
|
1004
|
+
sensitive?: true;
|
|
987
1005
|
}>;
|
|
988
1006
|
hasAdditionalContent: boolean;
|
|
1007
|
+
/**
|
|
1008
|
+
* The author marked the post sensitive with one of Minds' sensitive tags (#nsfw, #nudity and the rest), in its text or
|
|
1009
|
+
* among the network's own tags: cover its media as a Minds post flagged NSFW is covered. A post the network itself marks
|
|
1010
|
+
* (a Mastodon sensitive flag or content warning, a Bluesky label, a Nostr content-warning) is never returned at all.
|
|
1011
|
+
*/
|
|
1012
|
+
sensitive?: boolean;
|
|
989
1013
|
/** At most one independently filtered quoted post. */
|
|
990
1014
|
quote?: {
|
|
991
1015
|
state: 'available';
|
|
@@ -995,6 +1019,15 @@ export interface SocialPost {
|
|
|
995
1019
|
} | {
|
|
996
1020
|
state: 'unavailable';
|
|
997
1021
|
};
|
|
1022
|
+
/**
|
|
1023
|
+
* Present, and only then, when the ActivityPub author opted out of indexing (`noindex`,
|
|
1024
|
+
* `indexable: false`, or #nobot, #noindex or #noai in their bio). The keyless trends, trend-feed and
|
|
1025
|
+
* radar reads leave such posts out, and no post served from the source index is one. The keyless
|
|
1026
|
+
* author-feed and conversation reads (a profile or a post someone opened) and member reads of a
|
|
1027
|
+
* provider window still return them with this mark, so an app should never re-share or promote one
|
|
1028
|
+
* to signed-out visitors.
|
|
1029
|
+
*/
|
|
1030
|
+
authorOptOut?: true;
|
|
998
1031
|
}
|
|
999
1032
|
export interface SocialPostSearch {
|
|
1000
1033
|
items: SocialPost[];
|
|
@@ -1129,7 +1162,11 @@ export interface SocialRadarMember {
|
|
|
1129
1162
|
*/
|
|
1130
1163
|
visitorMedia?: true;
|
|
1131
1164
|
}
|
|
1132
|
-
/**
|
|
1165
|
+
/**
|
|
1166
|
+
* One story on the radar. `breaking` is outlet corroboration inside a short window; `viral` is social velocity above its
|
|
1167
|
+
* network's norm, from two or more people, on a story still gaining engagement, reported by no outlet or by two or more.
|
|
1168
|
+
* `velocity.perHour` is the engagement its posts gained per hour since the last build, counting only posts read on both.
|
|
1169
|
+
*/
|
|
1133
1170
|
export interface SocialRadarCluster {
|
|
1134
1171
|
id: string;
|
|
1135
1172
|
/** Verbatim: a World Today headline, a provider topic name or the medoid outlet title. Never generated. */
|
|
@@ -1226,7 +1263,9 @@ export interface SocialRadarStoryArchive {
|
|
|
1226
1263
|
}
|
|
1227
1264
|
/**
|
|
1228
1265
|
* One radar story by id, with its social member posts.
|
|
1229
|
-
* `live`: on the current radar (`cluster` and `items` present)
|
|
1266
|
+
* `live`: on the current radar (`cluster` and `items` present), or left it sooner than its minimum lifetime after it was
|
|
1267
|
+
* first published (six hours by default), or within the last half hour: then `liveUntil` says until when, and `successor`
|
|
1268
|
+
* names the story on the radar that continues it, if one does. `aged_out`: not on this radar, but
|
|
1230
1269
|
* held by the story archive (`archive`, and the live story it continues as in `successor`) or seen on
|
|
1231
1270
|
* one of the last six radars (`archive` null). `unknown`: in neither (never existed, or left longer ago
|
|
1232
1271
|
* than the archive keeps stories). A server without the story archive leaves `archive` and `successor` out.
|
|
@@ -1243,6 +1282,13 @@ export interface SocialRadarStory {
|
|
|
1243
1282
|
id: string;
|
|
1244
1283
|
label: string;
|
|
1245
1284
|
} | null;
|
|
1285
|
+
/** On a `live` answer for a story no longer on the radar list: when it stops answering `live` and ages out. */
|
|
1286
|
+
liveUntil?: string;
|
|
1287
|
+
/**
|
|
1288
|
+
* Present when the story was asked for by a World Today edition story's id: the radar story that edition story
|
|
1289
|
+
* seeded, and `id` is its radar id, the address to link to. A server that cannot resolve edition story ids answers 400.
|
|
1290
|
+
*/
|
|
1291
|
+
editionStory?: string;
|
|
1246
1292
|
}
|
|
1247
1293
|
/** Read options for callers that hold their own deadline, such as a discovery page. */
|
|
1248
1294
|
export interface SocialReadOptions {
|
|
@@ -1274,6 +1320,13 @@ export interface SocialConversation {
|
|
|
1274
1320
|
replies: SocialPost[];
|
|
1275
1321
|
coverage: 'public_provider_window';
|
|
1276
1322
|
limited: boolean;
|
|
1323
|
+
/**
|
|
1324
|
+
* The member hid this post, or blocks or mutes its author: `focus` is the post, so the app can show it on request,
|
|
1325
|
+
* `replies` is empty, and `viewerControls` are the member's controls to undo. Absent otherwise (a post the app's
|
|
1326
|
+
* moderation withholds is `post_unavailable`).
|
|
1327
|
+
*/
|
|
1328
|
+
hiddenByViewer?: true;
|
|
1329
|
+
viewerControls?: Array<Pick<SocialSourceControl, 'source' | 'control'>>;
|
|
1277
1330
|
}
|
|
1278
1331
|
export interface SocialFollowingSource {
|
|
1279
1332
|
subscriptionId: string;
|
|
@@ -1352,7 +1405,7 @@ export declare class ProtocolsResource {
|
|
|
1352
1405
|
getSharedRadarPosts(projectId: string, params: {
|
|
1353
1406
|
cluster: string;
|
|
1354
1407
|
}): Promise<SingleResponse<SocialRadarPosts>>;
|
|
1355
|
-
/** One radar story by id with its posts, without a key: a story page or shared link in one call. */
|
|
1408
|
+
/** 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
1409
|
getSharedRadarStory(projectId: string, params: {
|
|
1357
1410
|
cluster: string;
|
|
1358
1411
|
}): Promise<SingleResponse<SocialRadarStory>>;
|
|
@@ -1417,7 +1470,10 @@ export declare class ProtocolsResource {
|
|
|
1417
1470
|
getPublicRadarPosts(params: {
|
|
1418
1471
|
cluster: string;
|
|
1419
1472
|
}, options?: SocialReadOptions): Promise<SingleResponse<SocialRadarPosts>>;
|
|
1420
|
-
/**
|
|
1473
|
+
/**
|
|
1474
|
+
* One radar story by id with its posts under the app's own key; `status` tells an aged-out story from an unknown one.
|
|
1475
|
+
* `cluster` may be a World Today edition story's id (see SocialRadarStory.editionStory).
|
|
1476
|
+
*/
|
|
1421
1477
|
getPublicRadarStory(params: {
|
|
1422
1478
|
cluster: string;
|
|
1423
1479
|
}, 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
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { HttpClient } from '../client.js';
|
|
2
|
-
import type { Post } from '../types.js';
|
|
2
|
+
import type { Post, ViewerModeration } from '../types.js';
|
|
3
3
|
import type { SocialPost } from './protocols.js';
|
|
4
4
|
/** The sections a blended search can answer, in no particular order (see `SearchAnswer.order`). */
|
|
5
5
|
export declare const SEARCH_SECTIONS: readonly ["top_posts", "latest_posts", "open_network", "people", "groups", "topics", "stories", "related", "trending"];
|
|
@@ -34,6 +34,14 @@ export interface SearchPerson {
|
|
|
34
34
|
plus: boolean;
|
|
35
35
|
pro: boolean;
|
|
36
36
|
founder: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* The account wears the display name of a verified or founder account it is not: it is listed after every account
|
|
39
|
+
* that is not such a copy. Label it as not the account at `name_matches_verified_handle`. Absent from an older API.
|
|
40
|
+
*/
|
|
41
|
+
name_matches_verified?: boolean;
|
|
42
|
+
name_matches_verified_handle?: string | null;
|
|
43
|
+
/** Whether the caller mutes this account (people search never lists an account in a block with the caller). */
|
|
44
|
+
viewer?: ViewerModeration;
|
|
37
45
|
}
|
|
38
46
|
export interface SearchGroup {
|
|
39
47
|
id: string;
|
package/dist/types.d.ts
CHANGED
|
@@ -118,6 +118,12 @@ export interface UserSummary {
|
|
|
118
118
|
username: string;
|
|
119
119
|
image: string | null;
|
|
120
120
|
is_ai: boolean;
|
|
121
|
+
/**
|
|
122
|
+
* The account labels itself adult by its own handle, display name or bio:
|
|
123
|
+
* cover its avatar unless the member turned Show NSFW on. Absent from
|
|
124
|
+
* servers older than this field and from reads that do not label accounts.
|
|
125
|
+
*/
|
|
126
|
+
is_nsfw?: boolean;
|
|
121
127
|
}
|
|
122
128
|
export interface User extends UserSummary {
|
|
123
129
|
bio: string | null;
|
|
@@ -254,11 +260,24 @@ export interface RepostedPost {
|
|
|
254
260
|
media: MediaItem[];
|
|
255
261
|
created_at: string;
|
|
256
262
|
}
|
|
263
|
+
/**
|
|
264
|
+
* What the signed-in caller has done about an account: they block it, they mute it. Never what the account did about
|
|
265
|
+
* the caller. Absent when the server could not read it, and from an older API.
|
|
266
|
+
*/
|
|
267
|
+
export interface ViewerModeration {
|
|
268
|
+
blocking: boolean;
|
|
269
|
+
muting: boolean;
|
|
270
|
+
}
|
|
257
271
|
export interface PostDetail extends Post {
|
|
258
272
|
/** The newest replies (at most 50). */
|
|
259
273
|
replies: PostReply[];
|
|
260
274
|
/** Present when the thread has more replies; pass it to `posts.replies(id, { cursor })` for the next, older page. */
|
|
261
275
|
replies_next_cursor?: string | null;
|
|
276
|
+
/**
|
|
277
|
+
* Whether the caller blocks or mutes the author, so a page can say "You blocked @x" (with View and Unblock) instead
|
|
278
|
+
* of offering a reply or a vote: reacting to or replying to a post across a block answers 403.
|
|
279
|
+
*/
|
|
280
|
+
viewer?: ViewerModeration;
|
|
262
281
|
}
|
|
263
282
|
export interface PostReply {
|
|
264
283
|
id: string;
|
|
@@ -514,6 +533,12 @@ export interface CommunityMember {
|
|
|
514
533
|
image: string | null;
|
|
515
534
|
bio: string | null;
|
|
516
535
|
is_ai: boolean;
|
|
536
|
+
/**
|
|
537
|
+
* The account labels itself adult by its own handle, display name or bio:
|
|
538
|
+
* cover its avatar unless the member turned Show NSFW on. Absent from
|
|
539
|
+
* servers older than this field and from reads that do not label accounts.
|
|
540
|
+
*/
|
|
541
|
+
is_nsfw?: boolean;
|
|
517
542
|
role: string;
|
|
518
543
|
joined_at: string;
|
|
519
544
|
}
|
|
@@ -1017,6 +1042,12 @@ export interface Profile {
|
|
|
1017
1042
|
location: string | null;
|
|
1018
1043
|
website: string | null;
|
|
1019
1044
|
is_ai: boolean;
|
|
1045
|
+
/**
|
|
1046
|
+
* The account labels itself adult by its own handle, display name or bio:
|
|
1047
|
+
* cover its avatar unless the member turned Show NSFW on. Absent from
|
|
1048
|
+
* servers older than this field and from reads that do not label accounts.
|
|
1049
|
+
*/
|
|
1050
|
+
is_nsfw?: boolean;
|
|
1020
1051
|
created_at: string;
|
|
1021
1052
|
}
|
|
1022
1053
|
/** A row of a followers/following list: the profile plus the caller's relationship with it. */
|
|
@@ -1086,6 +1117,14 @@ export interface ProfileSearchResult extends Profile {
|
|
|
1086
1117
|
plus: boolean;
|
|
1087
1118
|
pro: boolean;
|
|
1088
1119
|
founder: boolean;
|
|
1120
|
+
/**
|
|
1121
|
+
* The account wears the display name of a verified or founder account it is not: it is listed after every account
|
|
1122
|
+
* that is not such a copy. Label it as not the account at `name_matches_verified_handle`. Absent from an older API.
|
|
1123
|
+
*/
|
|
1124
|
+
name_matches_verified?: boolean;
|
|
1125
|
+
name_matches_verified_handle?: string | null;
|
|
1126
|
+
/** Whether the caller mutes this account (a search never lists an account in a block with the caller). */
|
|
1127
|
+
viewer?: ViewerModeration;
|
|
1089
1128
|
}
|
|
1090
1129
|
export interface SearchMentionsParams {
|
|
1091
1130
|
/** What the member typed after `@` (a leading `@` is ignored). */
|
package/llm.md
CHANGED
|
@@ -516,8 +516,26 @@ 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)));
|
|
519
521
|
```
|
|
520
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'
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
Revoking a device other active devices were approved through, or the account's root (main) device, throws a 409 `ConflictError` with code `has_dependents` and `details: { dependents: string[], root: boolean }`; nothing changes (0.7.58). `revokeDevice(id, { cascade: true })` revokes it and every device chained through it, answering `revoked` (ids). Once the root is revoked the account cannot link a new device.
|
|
538
|
+
|
|
521
539
|
### r.realtime — WebSocket Chat Client
|
|
522
540
|
|
|
523
541
|
Provides persistent real-time streaming and chat presence, useful for building frontends (e.g. React Native, Web).
|
|
@@ -551,7 +569,7 @@ unsub()
|
|
|
551
569
|
const off = r.realtime.on('notification', (n) => refreshBadge(n))
|
|
552
570
|
```
|
|
553
571
|
|
|
554
|
-
Events: `onMessage`, `onMessageEdit`, `onMessageDelete`, `onTyping`, `onAgentThinking`, `on(event, fn)`
|
|
572
|
+
Events: `onMessage`, `onMessageEdit`, `onMessageDelete`, `onTyping`, `onAgentThinking`, `onE2eeEvent`, `onE2eeLink`, `onE2eeDevices`, `on(event, fn)`
|
|
555
573
|
|
|
556
574
|
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
575
|
|
|
@@ -809,10 +827,10 @@ Pagination: pass `limit` and `offset`. Check `meta.has_more`.
|
|
|
809
827
|
| `AuthorizationError` | 403 | API key lacks required scope |
|
|
810
828
|
| `NotFoundError` | 404 | Resource doesn't exist or no access |
|
|
811
829
|
| `ValidationError` | 400 | Invalid input (check `error.details`) |
|
|
812
|
-
| `RateLimitError` | 429 | Rate limit hit (`
|
|
830
|
+
| `RateLimitError` | 429 | Rate limit hit (`retryAfter` seconds, `resetAt` Date, `scope`) |
|
|
813
831
|
| `ConflictError` | 409 | Resource already exists |
|
|
814
832
|
|
|
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).
|
|
833
|
+
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
834
|
|
|
817
835
|
### Idempotency
|
|
818
836
|
|