@recursiv/sdk 0.7.53 → 0.7.55
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 +41 -3
- package/dist/client.d.ts +1 -0
- package/dist/client.js +1 -0
- package/dist/errors.d.ts +13 -0
- package/dist/errors.js +17 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.js +1 -1
- package/dist/resources/auth.d.ts +63 -13
- package/dist/resources/auth.js +106 -40
- package/dist/resources/commands.d.ts +14 -1
- package/dist/resources/commands.js +7 -1
- package/dist/resources/curator.d.ts +1 -1
- package/dist/resources/discovery.d.ts +78 -1
- package/dist/resources/discovery.js +55 -6
- package/dist/resources/e2ee.d.ts +52 -6
- package/dist/resources/e2ee.js +33 -0
- package/dist/resources/human-input.d.ts +4 -1
- package/dist/resources/human-input.js +7 -2
- package/dist/resources/moderation.d.ts +5 -1
- package/dist/resources/moderation.js +6 -2
- package/dist/resources/notifications.d.ts +19 -0
- package/dist/resources/profiles.d.ts +7 -3
- package/dist/resources/profiles.js +5 -1
- package/dist/resources/protocols.d.ts +34 -2
- package/dist/resources/protocols.js +8 -2
- package/dist/resources/reports.d.ts +4 -0
- package/dist/resources/reports.js +5 -1
- package/dist/types.d.ts +54 -0
- package/llm.md +13 -7
- package/package.json +1 -1
package/dist/types.d.ts
CHANGED
|
@@ -22,6 +22,38 @@ export interface DiscoveryLayout {
|
|
|
22
22
|
theme?: string;
|
|
23
23
|
windows?: DiscoveryLayoutWindow[];
|
|
24
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* How to ask for the next page of the same feed: the next offset while the page order is a ranking,
|
|
27
|
+
* the cursor once the feed pages by keyset (a category feed past its window of newest judged posts), or null when the
|
|
28
|
+
* feed has no more. On a category feed, `fallover` then says where to go.
|
|
29
|
+
*/
|
|
30
|
+
export type FeedNext = {
|
|
31
|
+
offset: number;
|
|
32
|
+
} | {
|
|
33
|
+
cursor: string;
|
|
34
|
+
} | null;
|
|
35
|
+
/**
|
|
36
|
+
* One step of a category feed's fall-through, in order: the same category's older posts (the keyset
|
|
37
|
+
* cursor; the same continuation `next` carries), the same category on the open networks, the member's
|
|
38
|
+
* next categories (`via`: the order they came from, the one `posts.categories()` serves), For you
|
|
39
|
+
* (signed in), All.
|
|
40
|
+
*/
|
|
41
|
+
export type FalloverStep = {
|
|
42
|
+
kind: 'category_older';
|
|
43
|
+
category: string;
|
|
44
|
+
cursor: string;
|
|
45
|
+
} | {
|
|
46
|
+
kind: 'category_network';
|
|
47
|
+
category: string;
|
|
48
|
+
} | {
|
|
49
|
+
kind: 'category';
|
|
50
|
+
category: string;
|
|
51
|
+
via: 'personal' | 'popular' | 'static';
|
|
52
|
+
} | {
|
|
53
|
+
kind: 'for_you';
|
|
54
|
+
} | {
|
|
55
|
+
kind: 'all';
|
|
56
|
+
};
|
|
25
57
|
export interface PaginationMeta {
|
|
26
58
|
discovery?: DiscoveryLayout;
|
|
27
59
|
scanned?: number;
|
|
@@ -36,6 +68,10 @@ export interface PaginationMeta {
|
|
|
36
68
|
has_more: boolean;
|
|
37
69
|
/** Pass this as `cursor` for the next page. Absent on offset-only endpoints. */
|
|
38
70
|
next_cursor?: string | null;
|
|
71
|
+
/** The next page of this feed (offset or cursor), or null at its end. Absent on older servers. */
|
|
72
|
+
next?: FeedNext;
|
|
73
|
+
/** Present when a stage of a category feed ends: the ordered steps to follow so scrolling never stops. */
|
|
74
|
+
fallover?: FalloverStep[];
|
|
39
75
|
}
|
|
40
76
|
export interface PaginatedResponse<T> {
|
|
41
77
|
data: T[];
|
|
@@ -99,6 +135,18 @@ export interface ViewerRelationship {
|
|
|
99
135
|
/** This account follows the caller. */
|
|
100
136
|
follows_you: boolean;
|
|
101
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* The caller's relationship with one account (`profiles.isFollowing`): follows
|
|
140
|
+
* in both directions, and whether the caller blocks or mutes it. A server
|
|
141
|
+
* from before the moderation fields, or one whose read of them failed, leaves
|
|
142
|
+
* them out: absent means unknown, never "not blocked".
|
|
143
|
+
*/
|
|
144
|
+
export interface ProfileRelationship extends ViewerRelationship {
|
|
145
|
+
/** The caller blocks this account. */
|
|
146
|
+
is_blocking?: boolean;
|
|
147
|
+
/** The caller mutes this account. */
|
|
148
|
+
is_muting?: boolean;
|
|
149
|
+
}
|
|
102
150
|
export interface FollowUser extends ViewerRelationship {
|
|
103
151
|
id: string;
|
|
104
152
|
name: string;
|
|
@@ -181,6 +229,12 @@ export interface RepostedPost {
|
|
|
181
229
|
/** The group the original is in (null when it is in none): it decides what the repost card offers. */
|
|
182
230
|
community?: PostCommunity | null;
|
|
183
231
|
organization_id: string | null;
|
|
232
|
+
/**
|
|
233
|
+
* The original's own NSFW flag. A repost or quote card shows the original's
|
|
234
|
+
* words and media: mask them when this is set or `tags` marks it sensitive.
|
|
235
|
+
* Absent from servers older than this field.
|
|
236
|
+
*/
|
|
237
|
+
is_nsfw?: boolean;
|
|
184
238
|
tags: Tag[];
|
|
185
239
|
reactions_count: number;
|
|
186
240
|
score: number;
|
package/llm.md
CHANGED
|
@@ -190,7 +190,7 @@ const { data } = await r.commands.execute({
|
|
|
190
190
|
// data: { text, actions }
|
|
191
191
|
```
|
|
192
192
|
|
|
193
|
-
Methods: `
|
|
193
|
+
Methods: `execute({ prompt, current_screen?, org_slug?, model?, locale?, attachments? })`, `gatePrompt({ messages })`. `gatePrompt` is Recursiv's own signup gate, not a moderation API: it answers only for keys on the platform's networks (404 anywhere else, including an app's own network), ignores `model`, and shares one daily LLM budget with Recursiv's signup page.
|
|
194
194
|
|
|
195
195
|
### r.projectBrain — Per-project knowledge base & task management
|
|
196
196
|
|
|
@@ -367,7 +367,9 @@ await r.auth.signOut(session.token)
|
|
|
367
367
|
```
|
|
368
368
|
|
|
369
369
|
`getSession(token)` and `signOut(token)` send the explicit session token as
|
|
370
|
-
Bearer auth, with a Cookie fallback for native/server runtimes.
|
|
370
|
+
Bearer auth, with a Cookie fallback for native/server runtimes. `getSession`
|
|
371
|
+
also answers `session: { id, expiresAt }`; an API-key client lists sessions with
|
|
372
|
+
`is_current: false`, so it matches `session.id` to mark its own device.
|
|
371
373
|
|
|
372
374
|
Methods: `signUp`, `signIn`, `getSession`, `signOut`, `createApiKey`, `signInAndCreateKey`, `signUpAndCreateKey`, `verifyOtpAndCreateKey`
|
|
373
375
|
|
|
@@ -387,7 +389,7 @@ await r.auth.requestTwoFactorEmailCode(live, { projectId }) // app context brand
|
|
|
387
389
|
await r.auth.disableTwoFactor({ emailCode: '482913' }, live)
|
|
388
390
|
```
|
|
389
391
|
|
|
390
|
-
Two-factor methods: `enableTwoFactor`, `verifyTwoFactor`, `disableTwoFactor`, `regenerateTwoFactorBackupCodes`, `requestTwoFactorEmailCode`, `completeTwoFactorSignIn`, `completeTwoFactorSignInAndCreateKey`. OAuth consent screens: `exchangeSessionForCookie(token)` (browser, sets the API-origin cookie so `/api/auth/mcp/authorize` continues without a second sign-in), `getOAuthClientInfo({ clientId, redirectUri?, scope? })` (public client name, redirect hosts, `redirect_uri_registered`, granted scopes).
|
|
392
|
+
Two-factor methods: `enableTwoFactor`, `verifyTwoFactor`, `disableTwoFactor`, `regenerateTwoFactorBackupCodes`, `requestTwoFactorEmailCode`, `completeTwoFactorSignIn`, `completeTwoFactorSignInAndCreateKey`. Minds cutover: `legacyHandoff(legacyToken, { attemptId, refreshToken, projectId, timeoutMs })` exchanges the legacy app's session once; it throws `LegacyHandoffError` (`status` 0 when no response came, `code`, `retryAfterSeconds`), and only `legacy_token_used` / `legacy_token_invalid` mean forget the legacy session. With an `attemptId` a success carries `apiKeyDescription`; give the API key made with that session that `description`, so replays of the attempt count only that key. `legacyHandoffAndCreateKey(token, keyInput, { attemptId, refreshToken })` does both; pass the same `attemptId` on every retry. OAuth consent screens: `exchangeSessionForCookie(token)` (browser, sets the API-origin cookie so `/api/auth/mcp/authorize` continues without a second sign-in), `getOAuthClientInfo({ clientId, redirectUri?, scope? })` (public client name, redirect hosts, `redirect_uri_registered`, granted scopes).
|
|
391
393
|
|
|
392
394
|
**Mobile/React Native auth** (recommended — one call instead of three):
|
|
393
395
|
```typescript
|
|
@@ -479,7 +481,7 @@ const { data: rows } = await r.profiles.followers('user_xxx') // each row: is_fo
|
|
|
479
481
|
await r.profiles.mute('user_xxx') // server hides their posts and reposts of them from your feeds
|
|
480
482
|
```
|
|
481
483
|
|
|
482
|
-
Methods: `list`, `me`, `get`, `getByUsername`, `search` (ranked by match x ln(1 + followers + min(posts, 100)) over one pool of 200, the same for every page; each row (`ProfileSearchResult`) carries `followers_count`, `posts_count`, `empty` (fold empties under "more handles") and `verified` (the accounts the operator names)), `mentions` (people + agents for an @-picker, follows first), `update`, `follow`, `unfollow`, `followers`, `following`, `followingIds` (your follow set as ids, cursor-paged), `isFollowing
|
|
484
|
+
Methods: `list`, `me`, `get`, `getByUsername`, `search` (ranked by match x ln(1 + followers + min(posts, 100)) over one pool of 200, the same for every page; each row (`ProfileSearchResult`) carries `followers_count`, `posts_count`, `empty` (fold empties under "more handles") and `verified` (the accounts the operator names)), `mentions` (people + agents for an @-picker, follows first), `update`, `follow`, `unfollow`, `followers`, `following`, `followingIds` (your follow set as ids, cursor-paged), `isFollowing` (follows both ways, plus `is_blocking`/`is_muting` when the server knows), `related`, `leaderboard`, `block`, `unblock`, `blocked`, `mute`, `unmute`, `muted`
|
|
483
485
|
|
|
484
486
|
---
|
|
485
487
|
|
|
@@ -500,14 +502,18 @@ Scroll back with `messages(id, { cursor: page.meta.next_cursor })`, not a growin
|
|
|
500
502
|
|
|
501
503
|
### r.e2ee — End-to-end encryption transport (flag-gated)
|
|
502
504
|
|
|
503
|
-
Relays opaque MLS envelopes, public device credentials and one-time key packages for the shared E2EE core; the server never sees keys or plaintext. Requires the `e2eeV2` network feature flag (otherwise `feature_disabled`) and a key acting as the account owner (agent-scoped keys get `agent_key_not_allowed`). The realm is the network id. Only a direct conversation between two people with an accepted request can start encryption (`conversation_not_eligible` otherwise); blocks apply (`blocked`); commits must send `expected_commit_seq`.
|
|
505
|
+
Relays opaque MLS envelopes, public device credentials and one-time key packages for the shared E2EE core; the server never sees keys or plaintext. Requires the `e2eeV2` network feature flag (otherwise `feature_disabled`) and a key acting as the account owner (agent-scoped keys get `agent_key_not_allowed`). The realm is the network id. Only a direct conversation between two people with an accepted request can start encryption (`conversation_not_eligible` otherwise), and every later append to it is checked the same way again (both people still accepted, still in the pilot, still only the two of them), so a client need not read the policy before each send; a resend of an event already stored still returns `duplicate: true`, even once a Welcome's target device is gone; blocks apply (`blocked`), to resends too; commits must send `expected_commit_seq`.
|
|
506
|
+
|
|
507
|
+
Ask first instead of trying a call: `r.e2ee.status()` returns `{ enabled: true }` or `{ enabled: false, reason: 'network_off' | 'not_allowlisted' }` for the calling account only. `enabled: false` means frozen: already-encrypted conversations stay readable, nothing new can be sent or enrolled.
|
|
504
508
|
|
|
505
509
|
```ts
|
|
510
|
+
const { data: access } = await r.e2ee.status(); // offer encryption only when access.enabled
|
|
506
511
|
const { data: device } = await r.e2ee.registerDevice({ device_name: 'phone-1', realm: networkId, signing_key, credential });
|
|
507
512
|
await r.e2ee.publishKeyPackages({ device_id: device.id, packages: [keyPackageBase64] }); // → { published, available }
|
|
508
513
|
const { data: stock } = await r.e2ee.keyPackageStatus({ device_id: device.id }); // { available, expiringSoon }; refill when low
|
|
509
514
|
const { data: claimed } = await r.e2ee.claimKeyPackage({ device_id: device.id, target_device_id: peerDeviceId });
|
|
510
515
|
await r.e2ee.appendEvent(conversationId, { device_id: device.id, envelope }); // idempotent per event_id
|
|
516
|
+
await r.e2ee.appendEvent(conversationId, { device_id: device.id, envelope: reaction, silent: true }); // no push, no move up the chat list; every member's doorbell still rings
|
|
511
517
|
const { data: page } = await r.e2ee.events(conversationId, { device_id: device.id, after_seq: 0 });
|
|
512
518
|
await r.e2ee.acknowledge(conversationId, { device_id: device.id, seq: page.headSeq });
|
|
513
519
|
```
|
|
@@ -708,7 +714,7 @@ await r.appSubscriptions.changeTier({ tier: 'pro', proration_date: quote.prorati
|
|
|
708
714
|
- `r.protocols` — Private app safety: `sourceControls` returns current `enabled` plus paged saved choices; `setSourceControl`, `onSourceControlChange` and `reportSource` share app scope. Example: `const choices = new Map(); const stop = r.protocols.onSourceControlChange(change => choices.set(JSON.stringify([change.source, change.control]), change.enabled));` then `await r.protocols.setSourceControl({ source: { protocol: "bluesky", kind: "actor", nativeId: "did:plc:aaaaaaaaaaaaaaaaaaaaaaaa" }, control: "mute", enabled: true, scope: "minds" });` and call `stop(); choices.clear()` on account change/unmount. Notifications follow an exact server confirmation, never an uncertain response. These preferences and reports apply in Minds, not on the remote network. Keyless post links: `const visitor = new Recursiv({ anonymous: true, inheritEnvironmentApiKey: false }); const { data: thread } = await visitor.protocols.getSharedConversation(projectId, { protocol: 'nostr', post: publicEventId })`; `sharedNetworks(projectId)` lists public conversation capabilities. Only configured apps; app moderation and rate limits apply, no member state or write authority. Member public reading: `publicNetworks`, `searchPublicProfiles`, `getPublicProfile`, `getPublicAuthorFeed`, `getFollowingFeed`, `getPublicConversation`, `getPublicTrends`, `getPublicTrendFeed`; private app subscriptions: `listSubscriptions`, `subscribeToProfile`, `unsubscribeFromProfile`, `onSubscriptionChange`. Example after selecting a returned source post: `const { data: thread } = await r.protocols.getPublicConversation({ protocol: post.protocol, post: post.nativeId, readRef: post.readRef })`. Render `focus`, oldest-first `ancestors` and direct `replies`; `public_provider_window` never promises every reply, and `limited` marks known omissions. Requires both `users:read` and `posts:read` on the current human app key. Protocol discovery/settings: `const { data: adapters } = await r.protocols.list()` returns bounded `{ id, name, icon, color, maturity, capabilities, limitations }` metadata; `supported` means implemented, not configured, healthy, authorized, or enabled. Methods: `list`, `status`, `listCandidates`, `getCandidate`, `search`, `getSettings`, `refresh`, `updateSettings`.
|
|
709
715
|
- Native Nostr: `const { data: preview } = await r.protocols.prepareNativeNostr(identityId, { action: "publish", content: draft })`; review and sign the exact public event, then `submitNativeNostr(identityId, { intent: preview.intent, event, approve: true })`. Preparation/submission never auto-retry. After uncertainty, use `getNativeNostrPost(identityId, preview.postId ?? preview.eventId)` before a user-selected retry of the identical intent/event: deletion checks the original note's `postId`, publication checks `eventId`. Check `nativeNostrAccount()` enablement first; keys stay with the signer. For replies, after `nativeNostrAccount()` advertises `reply_text_note`, use `prepareNativeNostr(identityId, { action: "reply", content: draft, parentId: post.nativeId })`. Review returned parent/root/participants/relay and exact event, sign with the connected client signer, then `submitNativeNostr(identityId, { intent, event, approve: true })`. Requires users:read, posts:read and posts:write. Kind1 public notes only; reply enablement and shared UI/live proof remain pending. Do not inherit arbitrary notification tags or relay hints. Signing mutations never auto-retry; check `getNativeNostrPost(identityId, eventId)` after uncertainty before a user-selected identical-event retry.
|
|
710
716
|
- Hosted Bluesky account on the app's own PDS: `const { data } = await r.protocols.createHostedIdentity({ protocol: 'atproto', handle: 'alice', email })`. Only the label is sent; `data.handle` carries the domain and `data.password` is returned this once, never stored. Never auto-retried. Either the account is finished or it is removed again. 409 `handle_unavailable` = choose another label; 503 `handle_dns_unavailable`/`handle_unresolvable`/`handle_unverified` = operator setup, retry later unchanged.
|
|
711
|
-
- Anonymous discovery: `const visitor = new Recursiv({ anonymous: true, inheritEnvironmentApiKey: false }); const { data } = await visitor.protocols.getSharedTrends(projectId, { protocol: 'nostr', limit: 20, discovery: true })`. Use `sharedNetworks(projectId)` capabilities; open a returned topic with `getSharedTrendFeed(projectId, { protocol: 'bluesky', topic: trend.nativeId, cursor })`. Configured apps only; no account authority or writes. App moderation and per-IP/app rate limits apply. Categories and Jev layouts reuse cached judgments without initiating paid inference; handle partial categories and finite provider windows.
|
|
717
|
+
- Anonymous discovery: `const visitor = new Recursiv({ anonymous: true, inheritEnvironmentApiKey: false }); const { data } = await visitor.protocols.getSharedTrends(projectId, { protocol: 'nostr', limit: 20, discovery: true })`. Use `sharedNetworks(projectId)` capabilities; open a returned topic with `getSharedTrendFeed(projectId, { protocol: 'bluesky', topic: trend.nativeId, cursor })`. Configured apps only; no account authority or writes. App moderation and per-IP/app rate limits apply. Categories and Jev layouts reuse cached judgments without initiating paid inference; handle partial categories and finite provider windows. Category depth: `r.posts.list({ category: 'science', sort: 'hot', discovery: true })` pages the hot window by offset and then by keyset (`meta.next` is `{ offset }`, `{ cursor }` or `null`; pass a cursor back as `cursor`); when a stage ends, `meta.fallover` lists the next feeds in order (`category_older`, `category_network`, `category` with `via`, `for_you`, `all`). One call for a category's modules: `const { data } = await r.discovery.category({ category: 'science' })` returns `stories`, `network`, `clips`, `people`, `groups`, each `{ items, ms, reason? }`.
|
|
712
718
|
- Public trends: require `public_trends` from `publicNetworks()`, then `getPublicTrends({ protocol, limit?, cursor? })`; render `kind: topics` as source topics and `kind: posts` as source posts. With `trend_feed`, open only a returned current topic using `getPublicTrendFeed({ protocol, topic: trend.nativeId, limit?, cursor? })`. Show provider, observedAt and provider_ranked_window coverage; null rankingWindow is unknown, not a daily/global ranking. Preserve source order, canonical IDs and readRef, and opaque cursors. Topic catalogs do not paginate; Mastodon trends stop at 200 source positions. Both methods require users:read plus posts:read on the current human app key; Nostr trends are unsupported. Propagate errors; trend_unavailable means refresh the topic list. No ingestion or native writes.
|
|
713
719
|
- `listCandidates({ protocol?, limit?, cursor? })` and `getCandidate(id)` are read-only quarantine review, not published-post search. Both require `admin` scope, a project-bound key, a live admin role, and existing project-admin access. The key supplies app/network scope; neither method accepts another project or network. They never invoke refresh, change settings, or publish.
|
|
714
720
|
- Lists return `{ data: ProtocolQuarantinedCandidate[], meta: { limit, has_more, next_cursor } }`; use the opaque `next_cursor`, not offset pagination. The API defaults `limit` to 20 and permits at most 50. Details return `{ data: ProtocolQuarantinedCandidate }`. Every candidate has `state: 'quarantined'` and `content_format: 'plain'`; render its bounded `content` as text, respect `content_truncated`, and distinguish upstream `published_at` from local `observed_at` / `created_at`.
|
|
@@ -806,7 +812,7 @@ Pagination: pass `limit` and `offset`. Check `meta.has_more`.
|
|
|
806
812
|
| `RateLimitError` | 429 | Rate limit hit (`error.retryAfter` seconds) |
|
|
807
813
|
| `ConflictError` | 409 | Resource already exists |
|
|
808
814
|
|
|
809
|
-
Auto-retry on 429/5xx built in (exponential backoff, default `maxRetries: 2`).
|
|
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).
|
|
810
816
|
|
|
811
817
|
### Idempotency
|
|
812
818
|
|