@recursiv/sdk 0.7.54 → 0.7.57
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -19
- package/dist/client.d.ts +1 -0
- package/dist/client.js +10 -2
- package/dist/errors.d.ts +41 -1
- package/dist/errors.js +77 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.js +1 -1
- package/dist/realtime.d.ts +24 -1
- package/dist/realtime.js +29 -1
- package/dist/resources/auth.d.ts +46 -10
- package/dist/resources/auth.js +99 -38
- package/dist/resources/commands.d.ts +14 -1
- package/dist/resources/commands.js +7 -1
- package/dist/resources/e2ee.d.ts +221 -6
- package/dist/resources/e2ee.js +90 -0
- package/dist/resources/human-input.d.ts +4 -1
- package/dist/resources/human-input.js +7 -3
- package/dist/resources/moderation.d.ts +5 -1
- package/dist/resources/moderation.js +6 -2
- package/dist/resources/notifications.d.ts +19 -0
- package/dist/resources/protocols.d.ts +52 -5
- package/dist/resources/protocols.js +13 -4
- package/dist/resources/reports.d.ts +4 -0
- package/dist/resources/reports.js +5 -1
- package/dist/types.d.ts +6 -0
- package/llm.md +26 -6
- package/package.json +1 -1
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` | `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` |
|
|
@@ -84,7 +84,7 @@ for await (const chunk of r.agents.chatStream(agent.id, { message: 'Hello!' }))
|
|
|
84
84
|
### Identity & Auth
|
|
85
85
|
| Resource | Key methods |
|
|
86
86
|
|----------|------------|
|
|
87
|
-
| `r.auth` | `signUp`, `signIn`, `signInWithUsername`, `getSession`, `signOut`, `createApiKey`, `forgetPassword`, `resetPassword`; two-factor: `enableTwoFactor`, `verifyTwoFactor`, `disableTwoFactor`, `regenerateTwoFactorBackupCodes`, `requestTwoFactorEmailCode`, `completeTwoFactorSignIn`; OAuth consent: `exchangeSessionForCookie`, `getOAuthClientInfo` (session calls use explicit Bearer auth with a native/server Cookie fallback) |
|
|
87
|
+
| `r.auth` | `signUp`, `signIn`, `signInWithUsername`, `getSession`, `signOut`, `createApiKey`, `forgetPassword`, `resetPassword`; two-factor: `enableTwoFactor`, `verifyTwoFactor`, `disableTwoFactor`, `regenerateTwoFactorBackupCodes`, `requestTwoFactorEmailCode`, `completeTwoFactorSignIn`; OAuth consent: `exchangeSessionForCookie`, `getOAuthClientInfo`; cutover: `legacyHandoff` (a legacy Minds session for a session here; throws `LegacyHandoffError`) (session calls use explicit Bearer auth with a native/server Cookie fallback) |
|
|
88
88
|
| `@recursiv/sdk/next` | `createRecursivAuth` — cookie-session server auth for Next.js App Router (see below) |
|
|
89
89
|
| `r.users` | `me`, `get` |
|
|
90
90
|
| `r.organizations` | `create`, `members`, `addMember`, `invite` |
|
|
@@ -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 })`; 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
|
|
|
@@ -689,20 +689,29 @@ category, refreshed after 30 s, kept up to 300 s and warmed as the warmer reache
|
|
|
689
689
|
viewer's `people` and `groups`; a section that timed out, failed or is off is empty with a
|
|
690
690
|
`reason` for that request only (a failure is never cached, so asking again retries it), and one this
|
|
691
691
|
key may not read is empty with `refused` (`stories` and `network` need the member's own app
|
|
692
|
-
key with `users:read`; `people` and `groups` need `users:read`).
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
692
|
+
key with `users:read`; `people` and `groups` need `users:read`). A plain category page
|
|
693
|
+
(`posts.list({ category })` with no other filter) reads on past a slice the safety floors
|
|
694
|
+
thinned, up to six reads, until it holds `limit` posts or its candidates end: `meta.scanned`
|
|
695
|
+
is how many it examined (the next offset is `offset + scanned`), and the stage ends on the
|
|
696
|
+
page that reaches its end, with `next` and `fallover`, rather than after a trail of empty
|
|
697
|
+
pages. In a category, Discover's read of Bluesky (`getPublicTrends` with `category` and
|
|
698
|
+
`discovery: true`) is the network's posts judged into the category from the source index,
|
|
699
|
+
as a page of posts with an `idx:tf:` cursor (pass it back to `getPublicTrends`), not the
|
|
700
|
+
topic catalog, which ignores the category; with the index off the catalog is answered as
|
|
701
|
+
before. A Bluesky topic feed in a category whose topic is gone, or whose page holds nothing
|
|
702
|
+
in the category, is served the network's indexed posts in it (`sourceIndex.reason` of `gone`
|
|
703
|
+
or `empty`) with an `idx:tf:` cursor that continues in the index; the provider's deeper pages
|
|
704
|
+
of such a topic are not read. In a category, a network's feed ends when its source does (a null
|
|
705
|
+
cursor) or on the third page in a row that shows no posts: an `idx:` cursor carries that count,
|
|
706
|
+
so pass it back unchanged; a provider's own cursor continues until the provider ends it. While a
|
|
707
|
+
category is read from the index, the server reads one Bluesky catalog topic a minute into it in
|
|
708
|
+
the background. When the index cannot answer (busy, or its safety cache unreadable), the page
|
|
709
|
+
is what the provider answered: the empty page, or the gone topic's 404; only an `idx:tf:` page
|
|
710
|
+
answers 503 `unavailable`, and that ends the index continuation, not the network. The app
|
|
711
|
+
contract: a `trend_unavailable` 404, an empty topic page or a failed `idx:tf:` page never
|
|
712
|
+
disables a network for the session; move to the next topic, start the `idx:tf:` stream once per network and session
|
|
713
|
+
(every such topic answers the same index stream; after a failed page it may start again),
|
|
714
|
+
and pass cursors back unchanged. `DiscoveryFeedSession` does this for you.
|
|
706
715
|
|
|
707
716
|
`sdk.posts.categories()` answers the app's categories in the order this member
|
|
708
717
|
should see them. `meta.order` is `personal`, `popular` or `static`; `meta.reason`
|
package/dist/client.d.ts
CHANGED
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
|
}
|
|
@@ -182,6 +189,7 @@ export class HttpClient {
|
|
|
182
189
|
headers: {
|
|
183
190
|
...this.getAuthHeaders(),
|
|
184
191
|
'Content-Type': 'application/json',
|
|
192
|
+
...(options?.idempotencyKey ? { 'Idempotency-Key': options.idempotencyKey } : {}),
|
|
185
193
|
},
|
|
186
194
|
body: body ? JSON.stringify(body) : undefined,
|
|
187
195
|
};
|
package/dist/errors.d.ts
CHANGED
|
@@ -24,6 +24,19 @@ export declare class RecursivError extends Error {
|
|
|
24
24
|
export declare class AuthenticationError extends RecursivError {
|
|
25
25
|
constructor(body: ApiErrorBody['error']);
|
|
26
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* A failed legacy session handoff (`auth.legacyHandoff`). It keeps what a
|
|
29
|
+
* caller needs to tell a refusal from an outage: the HTTP status (0 when no
|
|
30
|
+
* response arrived), the server's code (`timeout` or `network_error` when no
|
|
31
|
+
* response arrived), and the seconds the server's Retry-After asked for.
|
|
32
|
+
* Only `legacy_token_used` and `legacy_token_invalid` are final; every other
|
|
33
|
+
* failure leaves the legacy session worth trying again.
|
|
34
|
+
*/
|
|
35
|
+
export declare class LegacyHandoffError extends RecursivError {
|
|
36
|
+
/** Seconds from the server's Retry-After header, when it sent one. */
|
|
37
|
+
readonly retryAfterSeconds: number | null;
|
|
38
|
+
constructor(status: number, body: ApiErrorBody['error']);
|
|
39
|
+
}
|
|
27
40
|
export declare class AuthorizationError extends RecursivError {
|
|
28
41
|
constructor(body: ApiErrorBody['error']);
|
|
29
42
|
}
|
|
@@ -37,9 +50,36 @@ export declare class InsufficientCreditsError extends RecursivError {
|
|
|
37
50
|
readonly upgradeUrl: string;
|
|
38
51
|
constructor(body: ApiErrorBody['error']);
|
|
39
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;
|
|
40
76
|
export declare class RateLimitError extends RecursivError {
|
|
41
77
|
readonly upgradeUrl: string;
|
|
42
|
-
|
|
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);
|
|
43
83
|
}
|
|
44
84
|
export declare class ConflictError extends RecursivError {
|
|
45
85
|
constructor(body: ApiErrorBody['error']);
|
package/dist/errors.js
CHANGED
|
@@ -66,6 +66,23 @@ export class AuthenticationError extends RecursivError {
|
|
|
66
66
|
this.name = 'AuthenticationError';
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* A failed legacy session handoff (`auth.legacyHandoff`). It keeps what a
|
|
71
|
+
* caller needs to tell a refusal from an outage: the HTTP status (0 when no
|
|
72
|
+
* response arrived), the server's code (`timeout` or `network_error` when no
|
|
73
|
+
* response arrived), and the seconds the server's Retry-After asked for.
|
|
74
|
+
* Only `legacy_token_used` and `legacy_token_invalid` are final; every other
|
|
75
|
+
* failure leaves the legacy session worth trying again.
|
|
76
|
+
*/
|
|
77
|
+
export class LegacyHandoffError extends RecursivError {
|
|
78
|
+
/** Seconds from the server's Retry-After header, when it sent one. */
|
|
79
|
+
retryAfterSeconds;
|
|
80
|
+
constructor(status, body) {
|
|
81
|
+
super(status, body);
|
|
82
|
+
this.name = 'LegacyHandoffError';
|
|
83
|
+
this.retryAfterSeconds = typeof body.retry_after === 'number' ? body.retry_after : null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
69
86
|
export class AuthorizationError extends RecursivError {
|
|
70
87
|
constructor(body) {
|
|
71
88
|
super(403, body);
|
|
@@ -93,11 +110,70 @@ export class InsufficientCreditsError extends RecursivError {
|
|
|
93
110
|
this.message += `\n Upgrade: ${this.upgradeUrl}`;
|
|
94
111
|
}
|
|
95
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
|
+
}
|
|
96
166
|
export class RateLimitError extends RecursivError {
|
|
97
167
|
upgradeUrl;
|
|
98
|
-
|
|
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 = {}) {
|
|
99
173
|
super(429, body);
|
|
100
174
|
this.name = 'RateLimitError';
|
|
175
|
+
this.scope = details.scope;
|
|
176
|
+
this.resetAt = details.resetAt;
|
|
101
177
|
this.upgradeUrl = body?.upgrade_url || body.details?.upgrade_url || 'https://recursiv.io/pricing';
|
|
102
178
|
// Append upgrade hint for free tier users
|
|
103
179
|
if (body.message?.includes('free tier') ||
|
package/dist/index.d.ts
CHANGED
|
@@ -147,14 +147,14 @@ export { HttpClient } from './client.js';
|
|
|
147
147
|
export { RealtimeClient } from './realtime.js';
|
|
148
148
|
export type { RealtimeConnectOptions } from './realtime.js';
|
|
149
149
|
export { AuthResource } from './resources/auth.js';
|
|
150
|
-
export type { AuthSession, CurrentSession, SignUpInput, SignInInput, UsernameSignInInput, AppContext, ResetPasswordInput, TwoFactorConfirmation, TwoFactorEnrollment, TwoFactorChallenge, CompleteTwoFactorSignInInput, OAuthClientInfo, SendOtpInput, VerifyOtpInput, CreateApiKeyInput, ApiKeyResult, } from './resources/auth.js';
|
|
150
|
+
export type { AuthSession, CurrentSession, SignUpInput, SignInInput, UsernameSignInInput, AppContext, ResetPasswordInput, TwoFactorConfirmation, TwoFactorEnrollment, TwoFactorChallenge, CompleteTwoFactorSignInInput, OAuthClientInfo, SendOtpInput, VerifyOtpInput, CreateApiKeyInput, ApiKeyResult, LegacyHandoffOptions, LegacyHandoffSession, } from './resources/auth.js';
|
|
151
151
|
export { PostsResource } from './resources/posts.js';
|
|
152
152
|
export type { CategoryOrder, CategoryOrderMeta, CategoryOrderReason } from './resources/posts.js';
|
|
153
153
|
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 } 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';
|
|
@@ -165,7 +165,7 @@ export { SandboxResource } from './resources/sandbox.js';
|
|
|
165
165
|
export { ProfilesResource } from './resources/profiles.js';
|
|
166
166
|
export { OrganizationsResource } from './resources/organizations.js';
|
|
167
167
|
export { NotificationsResource } from './resources/notifications.js';
|
|
168
|
-
export type { Notification, NotificationActor, NotificationCategory, NotificationPostPreview, NotificationScopeParams, NotificationUnreadCount, ListNotificationsParams, NotificationListResponse, } from './resources/notifications.js';
|
|
168
|
+
export type { Notification, NotificationActor, NotificationCategory, NotificationPostPreview, NotificationPostTag, NotificationScopeParams, NotificationUnreadCount, ListNotificationsParams, NotificationListResponse, } from './resources/notifications.js';
|
|
169
169
|
export { SettingsResource } from './resources/settings.js';
|
|
170
170
|
export { BillingResource } from './resources/billing.js';
|
|
171
171
|
export { FreeTierResource } from './resources/free-tier.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, 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, 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/auth.d.ts
CHANGED
|
@@ -1,4 +1,26 @@
|
|
|
1
1
|
import type { HttpClient } from '../client.js';
|
|
2
|
+
/** Options for `auth.legacyHandoff`. */
|
|
3
|
+
export interface LegacyHandoffOptions {
|
|
4
|
+
projectId?: string;
|
|
5
|
+
organizationId?: string;
|
|
6
|
+
/** Random, 22 to 128 URL-safe characters, kept with the legacy session and sent with every exchange of it. */
|
|
7
|
+
attemptId?: string;
|
|
8
|
+
/** The legacy OAuth refresh token, when the legacy session has one. */
|
|
9
|
+
refreshToken?: string | null;
|
|
10
|
+
/** Bound for this call, body included. Defaults to the client's timeout. */
|
|
11
|
+
timeoutMs?: number;
|
|
12
|
+
}
|
|
13
|
+
/** What `auth.legacyHandoff` answers. */
|
|
14
|
+
export interface LegacyHandoffSession extends AuthSession {
|
|
15
|
+
/**
|
|
16
|
+
* Present when an `attemptId` was sent. Give the API key made with this
|
|
17
|
+
* session this description (`createApiKey({ ..., description })`): the
|
|
18
|
+
* server counts only keys that carry it when it decides whether a replay of
|
|
19
|
+
* the attempt is still allowed, so the member signing in elsewhere never
|
|
20
|
+
* refuses this device, and the key revoking itself is never counted as use.
|
|
21
|
+
*/
|
|
22
|
+
apiKeyDescription?: string;
|
|
23
|
+
}
|
|
2
24
|
export interface AuthSession {
|
|
3
25
|
token: string;
|
|
4
26
|
user: {
|
|
@@ -356,18 +378,32 @@ export declare class AuthResource {
|
|
|
356
378
|
/**
|
|
357
379
|
* Exchange a legacy Minds session (the OAuth access token or minds_sess JWT
|
|
358
380
|
* the legacy mobile app stored) for a session on this stack. Only answers
|
|
359
|
-
* during the cutover window
|
|
381
|
+
* during the cutover window.
|
|
360
382
|
*
|
|
361
|
-
* A
|
|
362
|
-
*
|
|
363
|
-
*
|
|
383
|
+
* A failure throws `LegacyHandoffError` with the HTTP status (0 when no
|
|
384
|
+
* response arrived), the server's code and its Retry-After. Only
|
|
385
|
+
* `legacy_token_used` and `legacy_token_invalid` mean the legacy session can
|
|
386
|
+
* never be exchanged; forget it then. Anything else (`legacy_unavailable`,
|
|
387
|
+
* `legacy_handoff_in_progress`, a rate limit, a closed window, an account
|
|
388
|
+
* not moved yet, no response at all) leaves it worth trying again: keep it.
|
|
389
|
+
*
|
|
390
|
+
* `attemptId` is a random id (22 to 128 URL-safe characters) the caller
|
|
391
|
+
* keeps with the legacy session and sends with every exchange of it. The
|
|
392
|
+
* server binds its single-use claim to it, so a retry after a lost answer is
|
|
393
|
+
* answered again instead of refused. `refreshToken` is the legacy OAuth
|
|
394
|
+
* refresh token; the server redeems it only when the access token cannot
|
|
395
|
+
* prove the session by itself. `timeoutMs` bounds this call, body included
|
|
396
|
+
* (default: the client's timeout). With an `attemptId`, the answer carries
|
|
397
|
+
* `apiKeyDescription`: the description to give the API key made with it.
|
|
364
398
|
*/
|
|
365
|
-
legacyHandoff(legacyToken: string,
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
399
|
+
legacyHandoff(legacyToken: string, options?: LegacyHandoffOptions): Promise<LegacyHandoffSession>;
|
|
400
|
+
/**
|
|
401
|
+
* legacyHandoff, then the app API key, exactly like verifyOtpAndCreateKey.
|
|
402
|
+
* Pass the same `attemptId` (kept with the legacy session) on every call:
|
|
403
|
+
* a retry after the key step failed is then answered again instead of
|
|
404
|
+
* refused, and the key is made with the description the answer names.
|
|
405
|
+
*/
|
|
406
|
+
legacyHandoffAndCreateKey(legacyToken: string, keyInput: CreateApiKeyInput, options?: Pick<LegacyHandoffOptions, 'attemptId' | 'refreshToken' | 'timeoutMs'>): Promise<{
|
|
371
407
|
apiKey: string;
|
|
372
408
|
apiKeyId: string;
|
|
373
409
|
user: AuthSession['user'];
|