@recursiv/sdk 0.7.54 → 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 +26 -17
- 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 +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 +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 -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 +18 -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 +6 -0
- package/llm.md +8 -4
- 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` (flag-gated E2EE v2 transport) |
|
|
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), 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
|
|
|
@@ -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
|
@@ -182,6 +182,7 @@ export class HttpClient {
|
|
|
182
182
|
headers: {
|
|
183
183
|
...this.getAuthHeaders(),
|
|
184
184
|
'Content-Type': 'application/json',
|
|
185
|
+
...(options?.idempotencyKey ? { 'Idempotency-Key': options.idempotencyKey } : {}),
|
|
185
186
|
},
|
|
186
187
|
body: body ? JSON.stringify(body) : undefined,
|
|
187
188
|
};
|
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
|
}
|
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);
|
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, E2eeEnvelope, E2eeEvent, E2eeKeyPackageStatus, E2eeObjectDownload, E2eeObjectUpload, 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,7 @@ 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, } from './errors.js';
|
|
244
244
|
export { parseSSE } from './lib/sse.js';
|
|
245
245
|
export * from './lib/webhooks.js';
|
|
246
246
|
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, } from './errors.js';
|
|
281
281
|
export { parseSSE } from './lib/sse.js';
|
|
282
282
|
export * from './lib/webhooks.js';
|
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'];
|
package/dist/resources/auth.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AuthenticationError, RateLimitError, RecursivError, ValidationError } from '../errors.js';
|
|
1
|
+
import { AuthenticationError, LegacyHandoffError, RateLimitError, RecursivError, ValidationError } from '../errors.js';
|
|
2
2
|
/**
|
|
3
3
|
* Better Auth's anti-CSRF middleware requires an Origin header when the
|
|
4
4
|
* request carries cookies or otherwise looks browser-initiated. We only
|
|
@@ -89,6 +89,14 @@ function serverErrorInfo(body) {
|
|
|
89
89
|
: undefined,
|
|
90
90
|
};
|
|
91
91
|
}
|
|
92
|
+
/** Seconds from a Retry-After header given in seconds; null when absent or not a number. */
|
|
93
|
+
function retryAfterHeaderSeconds(res) {
|
|
94
|
+
const raw = res.headers.get('retry-after');
|
|
95
|
+
if (!raw)
|
|
96
|
+
return null;
|
|
97
|
+
const seconds = Number(raw);
|
|
98
|
+
return Number.isFinite(seconds) && seconds >= 0 ? seconds : null;
|
|
99
|
+
}
|
|
92
100
|
/** Normalize the flat, nested-object, and legacy nested-string auth errors. */
|
|
93
101
|
function serverError(body, fallback) {
|
|
94
102
|
const record = (body ?? {});
|
|
@@ -634,10 +642,16 @@ export class AuthResource {
|
|
|
634
642
|
const res = await this.fetchWithTimeout(`${this.baseOrigin}/api/auth/sign-out`, {
|
|
635
643
|
method: 'POST',
|
|
636
644
|
headers: {
|
|
645
|
+
// Better Auth refuses a POST without a JSON Content-Type (415
|
|
646
|
+
// UNSUPPORTED_MEDIA_TYPE) before its handler runs, and on the Node
|
|
647
|
+
// server every POST carries a body stream, even an empty one. Without
|
|
648
|
+
// these two lines no sign-out ever reached the session.
|
|
649
|
+
'Content-Type': 'application/json',
|
|
637
650
|
...originHeader(this.baseOrigin),
|
|
638
651
|
Authorization: `Bearer ${sessionToken}`,
|
|
639
652
|
Cookie: `better-auth.session_token=${sessionToken}`,
|
|
640
653
|
},
|
|
654
|
+
body: '{}',
|
|
641
655
|
});
|
|
642
656
|
if (!res.ok) {
|
|
643
657
|
let body = {};
|
|
@@ -762,53 +776,96 @@ export class AuthResource {
|
|
|
762
776
|
/**
|
|
763
777
|
* Exchange a legacy Minds session (the OAuth access token or minds_sess JWT
|
|
764
778
|
* the legacy mobile app stored) for a session on this stack. Only answers
|
|
765
|
-
* during the cutover window
|
|
779
|
+
* during the cutover window.
|
|
766
780
|
*
|
|
767
|
-
* A
|
|
768
|
-
*
|
|
769
|
-
*
|
|
781
|
+
* A failure throws `LegacyHandoffError` with the HTTP status (0 when no
|
|
782
|
+
* response arrived), the server's code and its Retry-After. Only
|
|
783
|
+
* `legacy_token_used` and `legacy_token_invalid` mean the legacy session can
|
|
784
|
+
* never be exchanged; forget it then. Anything else (`legacy_unavailable`,
|
|
785
|
+
* `legacy_handoff_in_progress`, a rate limit, a closed window, an account
|
|
786
|
+
* not moved yet, no response at all) leaves it worth trying again: keep it.
|
|
787
|
+
*
|
|
788
|
+
* `attemptId` is a random id (22 to 128 URL-safe characters) the caller
|
|
789
|
+
* keeps with the legacy session and sends with every exchange of it. The
|
|
790
|
+
* server binds its single-use claim to it, so a retry after a lost answer is
|
|
791
|
+
* answered again instead of refused. `refreshToken` is the legacy OAuth
|
|
792
|
+
* refresh token; the server redeems it only when the access token cannot
|
|
793
|
+
* prove the session by itself. `timeoutMs` bounds this call, body included
|
|
794
|
+
* (default: the client's timeout). With an `attemptId`, the answer carries
|
|
795
|
+
* `apiKeyDescription`: the description to give the API key made with it.
|
|
770
796
|
*/
|
|
771
|
-
async legacyHandoff(legacyToken,
|
|
772
|
-
const
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
},
|
|
782
|
-
body: JSON.stringify({ legacy_token: legacyToken }),
|
|
783
|
-
});
|
|
784
|
-
let data = null;
|
|
797
|
+
async legacyHandoff(legacyToken, options = {}) {
|
|
798
|
+
const body = { legacy_token: legacyToken };
|
|
799
|
+
if (options.attemptId)
|
|
800
|
+
body.attempt_id = options.attemptId;
|
|
801
|
+
if (options.refreshToken)
|
|
802
|
+
body.legacy_refresh_token = options.refreshToken;
|
|
803
|
+
const timeoutMs = typeof options.timeoutMs === 'number' && options.timeoutMs > 0 ? options.timeoutMs : this.timeout;
|
|
804
|
+
const fail = (status, code, message, retryAfter = null) => new LegacyHandoffError(status, { type: 'auth_error', code, message, ...(retryAfter !== null ? { retry_after: retryAfter } : {}) });
|
|
805
|
+
const controller = new AbortController();
|
|
806
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
785
807
|
try {
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
808
|
+
let res;
|
|
809
|
+
try {
|
|
810
|
+
res = await fetch(`${this.baseOrigin}/api/auth/legacy-handoff`, {
|
|
811
|
+
method: 'POST',
|
|
812
|
+
headers: {
|
|
813
|
+
'Content-Type': 'application/json',
|
|
814
|
+
...originHeader(this.baseOrigin),
|
|
815
|
+
...(options.projectId ? { 'x-recursiv-app-project': options.projectId } : {}),
|
|
816
|
+
...(options.organizationId ? { 'x-recursiv-app-organization': options.organizationId } : {}),
|
|
817
|
+
},
|
|
818
|
+
body: JSON.stringify(body),
|
|
819
|
+
signal: controller.signal,
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
catch (err) {
|
|
823
|
+
if (controller.signal.aborted)
|
|
824
|
+
throw fail(0, 'timeout', `Legacy handoff timed out after ${timeoutMs}ms`);
|
|
825
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
826
|
+
throw fail(0, 'network_error', `Legacy handoff failed before a response arrived (${detail})`);
|
|
827
|
+
}
|
|
828
|
+
let data = null;
|
|
829
|
+
try {
|
|
830
|
+
data = await res.json();
|
|
831
|
+
}
|
|
832
|
+
catch {
|
|
833
|
+
if (controller.signal.aborted)
|
|
834
|
+
throw fail(0, 'timeout', `Legacy handoff timed out after ${timeoutMs}ms`);
|
|
835
|
+
data = null;
|
|
836
|
+
}
|
|
837
|
+
if (!res.ok) {
|
|
838
|
+
const { code, message } = serverErrorInfo(data);
|
|
839
|
+
throw fail(res.status, code || 'legacy_handoff_failed', message || `Legacy handoff failed: HTTP ${res.status}`, retryAfterHeaderSeconds(res));
|
|
840
|
+
}
|
|
841
|
+
const token = this.extractToken(res, data);
|
|
842
|
+
if (!token || typeof data?.user?.id !== 'string') {
|
|
843
|
+
throw fail(res.status, 'legacy_handoff_failed', 'Legacy handoff returned no session');
|
|
844
|
+
}
|
|
845
|
+
const answer = { token, user: data.user };
|
|
846
|
+
if (typeof data.api_key_description === 'string' && data.api_key_description)
|
|
847
|
+
answer.apiKeyDescription = data.api_key_description;
|
|
848
|
+
return answer;
|
|
798
849
|
}
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
throw new AuthenticationError({ type: 'auth_error', message: 'Legacy handoff returned no session', code: 'legacy_handoff_failed' });
|
|
850
|
+
finally {
|
|
851
|
+
clearTimeout(timer);
|
|
802
852
|
}
|
|
803
|
-
return { token, user: data.user };
|
|
804
853
|
}
|
|
805
|
-
/**
|
|
806
|
-
|
|
854
|
+
/**
|
|
855
|
+
* legacyHandoff, then the app API key, exactly like verifyOtpAndCreateKey.
|
|
856
|
+
* Pass the same `attemptId` (kept with the legacy session) on every call:
|
|
857
|
+
* a retry after the key step failed is then answered again instead of
|
|
858
|
+
* refused, and the key is made with the description the answer names.
|
|
859
|
+
*/
|
|
860
|
+
async legacyHandoffAndCreateKey(legacyToken, keyInput, options = {}) {
|
|
807
861
|
const session = await this.legacyHandoff(legacyToken, {
|
|
808
862
|
projectId: keyInput.projectId,
|
|
809
863
|
organizationId: keyInput.organizationId,
|
|
864
|
+
attemptId: options.attemptId,
|
|
865
|
+
refreshToken: options.refreshToken,
|
|
866
|
+
timeoutMs: options.timeoutMs,
|
|
810
867
|
});
|
|
811
|
-
const key = await this.createApiKeyOrRetireSession(keyInput, session);
|
|
868
|
+
const key = await this.createApiKeyOrRetireSession(session.apiKeyDescription && !keyInput.description ? { ...keyInput, description: session.apiKeyDescription } : keyInput, session);
|
|
812
869
|
return { apiKey: key.key, apiKeyId: key.id, user: session.user, session };
|
|
813
870
|
}
|
|
814
871
|
/**
|
|
@@ -844,9 +901,13 @@ export class AuthResource {
|
|
|
844
901
|
method: 'POST',
|
|
845
902
|
credentials: 'include',
|
|
846
903
|
headers: {
|
|
904
|
+
// JSON Content-Type and body, as in signOut: without them Better Auth
|
|
905
|
+
// answers 415 before the endpoint runs.
|
|
906
|
+
'Content-Type': 'application/json',
|
|
847
907
|
...originHeader(this.baseOrigin),
|
|
848
908
|
Authorization: `Bearer ${sessionToken}`,
|
|
849
909
|
},
|
|
910
|
+
body: '{}',
|
|
850
911
|
});
|
|
851
912
|
let data = null;
|
|
852
913
|
try {
|
|
@@ -6,6 +6,7 @@ interface GatePromptMessage {
|
|
|
6
6
|
}
|
|
7
7
|
interface GatePromptInput {
|
|
8
8
|
messages: GatePromptMessage[];
|
|
9
|
+
/** @deprecated Ignored: the server always runs its own gate model. Still accepted so older callers validate. */
|
|
9
10
|
model?: string;
|
|
10
11
|
}
|
|
11
12
|
interface GatePromptResult {
|
|
@@ -20,7 +21,13 @@ interface Attachment {
|
|
|
20
21
|
interface ExecutePromptInput {
|
|
21
22
|
prompt: string;
|
|
22
23
|
current_screen?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Organization to run under (its AI provider settings and billing). Must be one
|
|
26
|
+
* you are a member of on your network that the API key may reach, or the
|
|
27
|
+
* request fails with 403.
|
|
28
|
+
*/
|
|
23
29
|
org_slug?: string | null;
|
|
30
|
+
/** @deprecated Ignored: the organization's AI provider settings, or the platform default, pick the model. Kept so older callers validate. */
|
|
24
31
|
model?: string;
|
|
25
32
|
locale?: string;
|
|
26
33
|
attachments?: Attachment[];
|
|
@@ -32,7 +39,13 @@ interface ExecutePromptResult {
|
|
|
32
39
|
export declare class CommandsResource {
|
|
33
40
|
private client;
|
|
34
41
|
constructor(client: HttpClient);
|
|
35
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* Recursiv's own signup gate: evaluates a visitor's pitch for access to Recursiv.
|
|
44
|
+
* Not a moderation or guardrail API. It answers only for keys on the platform's
|
|
45
|
+
* networks (PLATFORM_SIGNUP_NETWORK_SLUGS) and returns 404 anywhere else, including
|
|
46
|
+
* an app's own network. Calls that reach the model share one global daily budget
|
|
47
|
+
* (GATE_PROMPT_DAILY_LLM_CALLS) with Recursiv's signup page.
|
|
48
|
+
*/
|
|
36
49
|
gatePrompt(input: GatePromptInput): Promise<SingleResponse<GatePromptResult>>;
|
|
37
50
|
/** Execute a command prompt with AI */
|
|
38
51
|
execute(input: ExecutePromptInput): Promise<SingleResponse<ExecutePromptResult>>;
|
|
@@ -3,7 +3,13 @@ export class CommandsResource {
|
|
|
3
3
|
constructor(client) {
|
|
4
4
|
this.client = client;
|
|
5
5
|
}
|
|
6
|
-
/**
|
|
6
|
+
/**
|
|
7
|
+
* Recursiv's own signup gate: evaluates a visitor's pitch for access to Recursiv.
|
|
8
|
+
* Not a moderation or guardrail API. It answers only for keys on the platform's
|
|
9
|
+
* networks (PLATFORM_SIGNUP_NETWORK_SLUGS) and returns 404 anywhere else, including
|
|
10
|
+
* an app's own network. Calls that reach the model share one global daily budget
|
|
11
|
+
* (GATE_PROMPT_DAILY_LLM_CALLS) with Recursiv's signup page.
|
|
12
|
+
*/
|
|
7
13
|
gatePrompt(input) {
|
|
8
14
|
return this.client.post('/commands/gate-prompt', input);
|
|
9
15
|
}
|
package/dist/resources/e2ee.d.ts
CHANGED
|
@@ -37,12 +37,6 @@ export interface E2eeKeyPackageStatus {
|
|
|
37
37
|
/** Of `available`, how many expire within 7 days. */
|
|
38
38
|
expiringSoon: number;
|
|
39
39
|
}
|
|
40
|
-
/**
|
|
41
|
-
* E2EE v2 ciphertext transport. The server relays envelopes and public
|
|
42
|
-
* credentials only; encryption happens in the shared core on each device.
|
|
43
|
-
* Requires the `e2eeV2` network feature flag and a key acting as the account
|
|
44
|
-
* owner (agent-scoped keys are refused). The realm is the network id.
|
|
45
|
-
*/
|
|
46
40
|
/** A reserved encrypted attachment object: PUT exactly `length` bytes of ciphertext to `uploadUrl`, then complete it. */
|
|
47
41
|
export interface E2eeObjectUpload {
|
|
48
42
|
reference: string;
|
|
@@ -56,9 +50,45 @@ export interface E2eeObjectDownload {
|
|
|
56
50
|
downloadUrl: string;
|
|
57
51
|
length: number;
|
|
58
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Why this account cannot use the E2EE relay on its network:
|
|
55
|
+
* - `network_off`: the network has not turned E2EE v2 on.
|
|
56
|
+
* - `not_allowlisted`: it is on, but only for pilot accounts, and this is not one.
|
|
57
|
+
*/
|
|
58
|
+
export type E2eeStatusReason = 'network_off' | 'not_allowlisted';
|
|
59
|
+
/** Whether the calling account is admitted to the E2EE relay on its network. */
|
|
60
|
+
export interface E2eeStatus {
|
|
61
|
+
/**
|
|
62
|
+
* True: this account may enroll devices, publish key packages, send, and
|
|
63
|
+
* start encryption in an eligible direct conversation. False: the relay is
|
|
64
|
+
* frozen for it. Conversations that already carry encryption stay readable
|
|
65
|
+
* (fetch, acknowledge, object downloads), its own devices can be listed and
|
|
66
|
+
* revoked, and nothing new can be sent or enrolled. It never falls back to
|
|
67
|
+
* plaintext.
|
|
68
|
+
*/
|
|
69
|
+
enabled: boolean;
|
|
70
|
+
/** Present only when `enabled` is false. Treat any unrecognised reason as off. */
|
|
71
|
+
reason?: E2eeStatusReason;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* E2EE v2 ciphertext transport. The server relays envelopes and public
|
|
75
|
+
* credentials only; encryption happens in the shared core on each device.
|
|
76
|
+
* Requires the `e2eeV2` network feature flag and a key acting as the account
|
|
77
|
+
* owner (agent-scoped keys are refused). The realm is the network id.
|
|
78
|
+
*/
|
|
59
79
|
export declare class E2eeResource {
|
|
60
80
|
private client;
|
|
61
81
|
constructor(client: HttpClient);
|
|
82
|
+
/**
|
|
83
|
+
* Whether this account is admitted to encrypted chat on its network, without
|
|
84
|
+
* attempting a relay call. `{ enabled: true }`, or `{ enabled: false, reason }`
|
|
85
|
+
* with `reason` `'network_off'` or `'not_allowlisted'`. Only the caller's own
|
|
86
|
+
* account is described; the network comes from the key or session. Enrolling
|
|
87
|
+
* a device still needs an accepted two-person DM (or an already-encrypted
|
|
88
|
+
* conversation) in this key's scope. Agent-scoped keys get
|
|
89
|
+
* `agent_key_not_allowed`.
|
|
90
|
+
*/
|
|
91
|
+
status(): Promise<SingleResponse<E2eeStatus>>;
|
|
62
92
|
/** Register this device's public identity and its signed approval chain. */
|
|
63
93
|
registerDevice(input: {
|
|
64
94
|
device_name: string;
|
|
@@ -96,12 +126,28 @@ export declare class E2eeResource {
|
|
|
96
126
|
bytes: string;
|
|
97
127
|
device: E2eeDevice;
|
|
98
128
|
}>>;
|
|
129
|
+
/**
|
|
130
|
+
* Append one MLS envelope to an encrypted conversation.
|
|
131
|
+
*
|
|
132
|
+
* `silent: true` marks a message that changes an earlier one (a reaction,
|
|
133
|
+
* an edit, a delete for everyone): the relay sends no push (so no unread
|
|
134
|
+
* badge either) and does not move the conversation up the chat list. The
|
|
135
|
+
* `e2ee:event` doorbell still rings exactly as for any message: on the
|
|
136
|
+
* sockets of every member, the sender's own account included, except
|
|
137
|
+
* across a block. So every listener hears a quiet event, not only an open
|
|
138
|
+
* thread: a chat list that pulls on the doorbell pulls it too, and should
|
|
139
|
+
* leave the row where it is once it has decrypted a reaction, an edit or a
|
|
140
|
+
* delete. A commit ignores the flag. The relay learns only that the event
|
|
141
|
+
* is quiet; the flag is not stored. A relay that predates it drops the
|
|
142
|
+
* key, so the event is simply not quiet there.
|
|
143
|
+
*/
|
|
99
144
|
appendEvent(conversationId: string, input: {
|
|
100
145
|
device_id: string;
|
|
101
146
|
envelope: E2eeEnvelope;
|
|
102
147
|
welcome?: string;
|
|
103
148
|
welcome_for_device_id?: string;
|
|
104
149
|
expected_commit_seq?: number;
|
|
150
|
+
silent?: boolean;
|
|
105
151
|
}): Promise<SingleResponse<{
|
|
106
152
|
seq: number;
|
|
107
153
|
duplicate: boolean;
|
package/dist/resources/e2ee.js
CHANGED
|
@@ -1,8 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E2EE v2 ciphertext transport. The server relays envelopes and public
|
|
3
|
+
* credentials only; encryption happens in the shared core on each device.
|
|
4
|
+
* Requires the `e2eeV2` network feature flag and a key acting as the account
|
|
5
|
+
* owner (agent-scoped keys are refused). The realm is the network id.
|
|
6
|
+
*/
|
|
1
7
|
export class E2eeResource {
|
|
2
8
|
client;
|
|
3
9
|
constructor(client) {
|
|
4
10
|
this.client = client;
|
|
5
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* Whether this account is admitted to encrypted chat on its network, without
|
|
14
|
+
* attempting a relay call. `{ enabled: true }`, or `{ enabled: false, reason }`
|
|
15
|
+
* with `reason` `'network_off'` or `'not_allowlisted'`. Only the caller's own
|
|
16
|
+
* account is described; the network comes from the key or session. Enrolling
|
|
17
|
+
* a device still needs an accepted two-person DM (or an already-encrypted
|
|
18
|
+
* conversation) in this key's scope. Agent-scoped keys get
|
|
19
|
+
* `agent_key_not_allowed`.
|
|
20
|
+
*/
|
|
21
|
+
status() {
|
|
22
|
+
return this.client.get('/e2ee/status');
|
|
23
|
+
}
|
|
6
24
|
/** Register this device's public identity and its signed approval chain. */
|
|
7
25
|
registerDevice(input) {
|
|
8
26
|
return this.client.post('/e2ee/devices', input);
|
|
@@ -28,6 +46,21 @@ export class E2eeResource {
|
|
|
28
46
|
claimKeyPackage(input) {
|
|
29
47
|
return this.client.post('/e2ee/key-packages/claim', input);
|
|
30
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Append one MLS envelope to an encrypted conversation.
|
|
51
|
+
*
|
|
52
|
+
* `silent: true` marks a message that changes an earlier one (a reaction,
|
|
53
|
+
* an edit, a delete for everyone): the relay sends no push (so no unread
|
|
54
|
+
* badge either) and does not move the conversation up the chat list. The
|
|
55
|
+
* `e2ee:event` doorbell still rings exactly as for any message: on the
|
|
56
|
+
* sockets of every member, the sender's own account included, except
|
|
57
|
+
* across a block. So every listener hears a quiet event, not only an open
|
|
58
|
+
* thread: a chat list that pulls on the doorbell pulls it too, and should
|
|
59
|
+
* leave the row where it is once it has decrypted a reaction, an edit or a
|
|
60
|
+
* delete. A commit ignores the flag. The relay learns only that the event
|
|
61
|
+
* is quiet; the flag is not stored. A relay that predates it drops the
|
|
62
|
+
* key, so the event is simply not quiet there.
|
|
63
|
+
*/
|
|
31
64
|
appendEvent(conversationId, input) {
|
|
32
65
|
// Retries are safe: the server is idempotent on (conversation, device, event id).
|
|
33
66
|
return this.client.post(`/e2ee/conversations/${encodeURIComponent(conversationId)}/events`, input, { retry: true });
|
|
@@ -15,6 +15,7 @@ export interface RequestHumanInputInput {
|
|
|
15
15
|
export interface HumanInputRequest {
|
|
16
16
|
conversation_id: string;
|
|
17
17
|
message_id: string;
|
|
18
|
+
human_ask_id: string | null;
|
|
18
19
|
/** Where to read the answer. MCP is stateless, so the agent polls. */
|
|
19
20
|
poll: string;
|
|
20
21
|
poll_after_seconds: number;
|
|
@@ -45,7 +46,9 @@ export declare class HumanInputResource {
|
|
|
45
46
|
private client;
|
|
46
47
|
constructor(client: HttpClient);
|
|
47
48
|
/** Raise a blocking question and notify the human wherever they are. */
|
|
48
|
-
request(input: RequestHumanInputInput
|
|
49
|
+
request(input: RequestHumanInputInput, options?: {
|
|
50
|
+
requestId?: string;
|
|
51
|
+
}): Promise<SingleResponse<HumanInputRequest>>;
|
|
49
52
|
/** Has the human answered yet? */
|
|
50
53
|
answers(conversationId: string): Promise<SingleResponse<HumanInputAnswers>>;
|
|
51
54
|
}
|
|
@@ -17,9 +17,13 @@ export class HumanInputResource {
|
|
|
17
17
|
this.client = client;
|
|
18
18
|
}
|
|
19
19
|
/** Raise a blocking question and notify the human wherever they are. */
|
|
20
|
-
request(input) {
|
|
21
|
-
//
|
|
22
|
-
|
|
20
|
+
request(input, options) {
|
|
21
|
+
// Never automatically replay: chat delivery may have committed before a
|
|
22
|
+
// response was lost, even when a keyed workspace ask was recorded once.
|
|
23
|
+
// Callers can reuse a requestId with the exact same input after an
|
|
24
|
+
// uncertain response. The server claims it against the durable
|
|
25
|
+
// organization-owned ask row; changed fields need a new requestId.
|
|
26
|
+
return this.client.post('/human-input', input, { retry: false, idempotencyKey: options?.requestId });
|
|
23
27
|
}
|
|
24
28
|
/** Has the human answered yet? */
|
|
25
29
|
answers(conversationId) {
|
|
@@ -81,7 +81,11 @@ export declare class ModerationResource {
|
|
|
81
81
|
* reachable through a valid key while the account is suspended or banned.
|
|
82
82
|
*/
|
|
83
83
|
mine(): Promise<SingleResponse<OwnModerationLog>>;
|
|
84
|
-
/**
|
|
84
|
+
/**
|
|
85
|
+
* Appeal a decision affecting the caller or content they own. Sent once,
|
|
86
|
+
* never retried automatically: a 429 past the member's appeal budget
|
|
87
|
+
* (`RateLimitError`, `retryAfter`) is for the member to see.
|
|
88
|
+
*/
|
|
85
89
|
appeal(actionId: string, statement: string): Promise<SingleResponse<{
|
|
86
90
|
success: true;
|
|
87
91
|
id: string;
|
|
@@ -22,9 +22,13 @@ export class ModerationResource {
|
|
|
22
22
|
mine() {
|
|
23
23
|
return this.client.get('/appeals/moderation-actions');
|
|
24
24
|
}
|
|
25
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* Appeal a decision affecting the caller or content they own. Sent once,
|
|
27
|
+
* never retried automatically: a 429 past the member's appeal budget
|
|
28
|
+
* (`RateLimitError`, `retryAfter`) is for the member to see.
|
|
29
|
+
*/
|
|
26
30
|
appeal(actionId, statement) {
|
|
27
|
-
return this.client.post(`/appeals/${encodeURIComponent(actionId)}`, { statement });
|
|
31
|
+
return this.client.post(`/appeals/${encodeURIComponent(actionId)}`, { statement }, { retry: false });
|
|
28
32
|
}
|
|
29
33
|
/** Admin review: uphold or overturn an appeal. */
|
|
30
34
|
resolveAppeal(appealId, input) {
|
|
@@ -19,6 +19,11 @@ export interface NotificationActor {
|
|
|
19
19
|
* state so inline Upvote and Repost can start in the right position. `null`
|
|
20
20
|
* when the post is gone or no longer visible to the viewer.
|
|
21
21
|
*/
|
|
22
|
+
/** A post's tag as a notification's post preview carries it. */
|
|
23
|
+
export interface NotificationPostTag {
|
|
24
|
+
name: string;
|
|
25
|
+
slug: string;
|
|
26
|
+
}
|
|
22
27
|
export interface NotificationPostPreview {
|
|
23
28
|
id: string;
|
|
24
29
|
/**
|
|
@@ -35,6 +40,20 @@ export interface NotificationPostPreview {
|
|
|
35
40
|
excerpt_truncated?: boolean;
|
|
36
41
|
media_url: string | null;
|
|
37
42
|
media_type: string | null;
|
|
43
|
+
/**
|
|
44
|
+
* The post's own NSFW flag. With `tags`, it says whether the post is
|
|
45
|
+
* sensitive: a client that masks a sensitive post's card masks its excerpt
|
|
46
|
+
* and picture here too, and holds the picture back while this is absent
|
|
47
|
+
* (servers older than this field).
|
|
48
|
+
*/
|
|
49
|
+
is_nsfw?: boolean;
|
|
50
|
+
/**
|
|
51
|
+
* The post's tags, each with its name as written and the slug the tag API
|
|
52
|
+
* derived from it ("nsfw", "nude", "porn" and the like mark it sensitive).
|
|
53
|
+
* Match both: a tag created as "NSFW!" is slugged "nsfw". Absent from
|
|
54
|
+
* servers older than this field.
|
|
55
|
+
*/
|
|
56
|
+
tags?: NotificationPostTag[];
|
|
38
57
|
reactions_count?: number;
|
|
39
58
|
score?: number;
|
|
40
59
|
upvote_count?: number;
|
|
@@ -1119,6 +1119,15 @@ export interface SocialRadarMember {
|
|
|
1119
1119
|
height?: number;
|
|
1120
1120
|
};
|
|
1121
1121
|
providerRank?: number;
|
|
1122
|
+
/**
|
|
1123
|
+
* Present on a post member whose image or video a signed-out visitor may see (an open-network post with a cached
|
|
1124
|
+
* safe verdict, or a Minds post that passes the visitor's floor); absent on every other post member, and on members
|
|
1125
|
+
* of a radar built before the server recorded it. An outlet's media needs no mark. The keyless routes
|
|
1126
|
+
* (`getSharedRadar`, `getSharedRadarPosts`, `getSharedRadarStory`) serve media only from marked members and outlets.
|
|
1127
|
+
* A signed-in read (`getPublicRadar` and friends) keeps every member's media, so use only an outlet's or a marked
|
|
1128
|
+
* member's media on a signed-out page or a share card.
|
|
1129
|
+
*/
|
|
1130
|
+
visitorMedia?: true;
|
|
1122
1131
|
}
|
|
1123
1132
|
/** One story on the radar. `breaking` is outlet corroboration inside a short window; `viral` is social velocity above its network's norm. */
|
|
1124
1133
|
export interface SocialRadarCluster {
|
|
@@ -1143,6 +1152,7 @@ export interface SocialRadarCluster {
|
|
|
1143
1152
|
normalized: number;
|
|
1144
1153
|
samples: number;
|
|
1145
1154
|
};
|
|
1155
|
+
/** On a signed-in read the lead may belong to a member without `visitorMedia`; find it by `memberKey` before a share card uses it. */
|
|
1146
1156
|
media: {
|
|
1147
1157
|
images: number;
|
|
1148
1158
|
videos: number;
|
|
@@ -1333,7 +1343,10 @@ export declare class ProtocolsResource {
|
|
|
1333
1343
|
category?: string;
|
|
1334
1344
|
discovery?: boolean;
|
|
1335
1345
|
}): Promise<SingleResponse<SocialTrendFeed>>;
|
|
1336
|
-
/**
|
|
1346
|
+
/**
|
|
1347
|
+
* Breaking and viral stories clustered across outlets, open networks and Minds, through the same public-only app boundary.
|
|
1348
|
+
* Images and videos come only from outlets and from members marked `visitorMedia`; every other member is text.
|
|
1349
|
+
*/
|
|
1337
1350
|
getSharedRadar(projectId: string, params?: SocialRadarParams): Promise<SingleResponse<SocialRadar>>;
|
|
1338
1351
|
/** One radar story's social member posts without a key, so a signed-out story card can show the posts it counts. */
|
|
1339
1352
|
getSharedRadarPosts(projectId: string, params: {
|
|
@@ -1395,7 +1408,10 @@ export declare class ProtocolsResource {
|
|
|
1395
1408
|
category?: string;
|
|
1396
1409
|
discovery?: boolean;
|
|
1397
1410
|
}, options?: SocialReadOptions): Promise<SingleResponse<SocialTrendFeed>>;
|
|
1398
|
-
/**
|
|
1411
|
+
/**
|
|
1412
|
+
* The app's topic radar under its own key: the same snapshot the keyless route serves, for a signed-in reader, so
|
|
1413
|
+
* every member keeps its media (the keyless route serves only outlets' and `visitorMedia` members'). Never triggers a build.
|
|
1414
|
+
*/
|
|
1399
1415
|
getPublicRadar(params?: SocialRadarParams, options?: SocialReadOptions): Promise<SingleResponse<SocialRadar>>;
|
|
1400
1416
|
/** One radar story's social member posts, so its card can show the posts it counts. Minds members are read with `posts.get`. */
|
|
1401
1417
|
getPublicRadarPosts(params: {
|
|
@@ -102,7 +102,10 @@ export class ProtocolsResource {
|
|
|
102
102
|
return this.client.get(`/protocols/public/${encodeURIComponent(projectId)}/trend-feed`, { protocol: params.protocol, topic: params.topic,
|
|
103
103
|
limit: params.limit, cursor: params.cursor, category: params.category, discovery: params.discovery });
|
|
104
104
|
}
|
|
105
|
-
/**
|
|
105
|
+
/**
|
|
106
|
+
* Breaking and viral stories clustered across outlets, open networks and Minds, through the same public-only app boundary.
|
|
107
|
+
* Images and videos come only from outlets and from members marked `visitorMedia`; every other member is text.
|
|
108
|
+
*/
|
|
106
109
|
getSharedRadar(projectId, params = {}) {
|
|
107
110
|
return this.client.get(`/protocols/public/${encodeURIComponent(projectId)}/radar`, radarQuery(params));
|
|
108
111
|
}
|
|
@@ -167,7 +170,10 @@ export class ProtocolsResource {
|
|
|
167
170
|
...(params.category !== undefined ? { category: params.category } : {}),
|
|
168
171
|
...(params.discovery !== undefined ? { discovery: params.discovery } : {}) }, ...readOptions(options));
|
|
169
172
|
}
|
|
170
|
-
/**
|
|
173
|
+
/**
|
|
174
|
+
* The app's topic radar under its own key: the same snapshot the keyless route serves, for a signed-in reader, so
|
|
175
|
+
* every member keeps its media (the keyless route serves only outlets' and `visitorMedia` members'). Never triggers a build.
|
|
176
|
+
*/
|
|
171
177
|
getPublicRadar(params = {}, options) {
|
|
172
178
|
return this.client.get('/protocols/social/radar', radarQuery(params), ...readOptions(options));
|
|
173
179
|
}
|
|
@@ -35,6 +35,10 @@ export declare class ReportsResource {
|
|
|
35
35
|
* Submit a content report. Targets a post, user, community, or message
|
|
36
36
|
* by ID, with a short reason and optional free-text details. Reporter
|
|
37
37
|
* is the authenticated user.
|
|
38
|
+
*
|
|
39
|
+
* Sent once, never retried automatically: past the member's report budget
|
|
40
|
+
* the server answers 429 (`RateLimitError`, `retryAfter` = seconds until
|
|
41
|
+
* the window opens), and that refusal is for the member to see.
|
|
38
42
|
*/
|
|
39
43
|
create(input: CreateReportInput): Promise<SingleResponse<{
|
|
40
44
|
success: true;
|
|
@@ -7,9 +7,13 @@ export class ReportsResource {
|
|
|
7
7
|
* Submit a content report. Targets a post, user, community, or message
|
|
8
8
|
* by ID, with a short reason and optional free-text details. Reporter
|
|
9
9
|
* is the authenticated user.
|
|
10
|
+
*
|
|
11
|
+
* Sent once, never retried automatically: past the member's report budget
|
|
12
|
+
* the server answers 429 (`RateLimitError`, `retryAfter` = seconds until
|
|
13
|
+
* the window opens), and that refusal is for the member to see.
|
|
10
14
|
*/
|
|
11
15
|
create(input) {
|
|
12
|
-
return this.client.post('/reports', input);
|
|
16
|
+
return this.client.post('/reports', input, { retry: false });
|
|
13
17
|
}
|
|
14
18
|
/** List recent reports in the caller's project/network (admin only). */
|
|
15
19
|
list() {
|
package/dist/types.d.ts
CHANGED
|
@@ -229,6 +229,12 @@ export interface RepostedPost {
|
|
|
229
229
|
/** The group the original is in (null when it is in none): it decides what the repost card offers. */
|
|
230
230
|
community?: PostCommunity | null;
|
|
231
231
|
organization_id: string | null;
|
|
232
|
+
/**
|
|
233
|
+
* The original's own NSFW flag. A repost or quote card shows the original's
|
|
234
|
+
* words and media: mask them when this is set or `tags` marks it sensitive.
|
|
235
|
+
* Absent from servers older than this field.
|
|
236
|
+
*/
|
|
237
|
+
is_nsfw?: boolean;
|
|
232
238
|
tags: Tag[];
|
|
233
239
|
reactions_count: number;
|
|
234
240
|
score: number;
|
package/llm.md
CHANGED
|
@@ -190,7 +190,7 @@ const { data } = await r.commands.execute({
|
|
|
190
190
|
// data: { text, actions }
|
|
191
191
|
```
|
|
192
192
|
|
|
193
|
-
Methods: `
|
|
193
|
+
Methods: `execute({ prompt, current_screen?, org_slug?, model?, locale?, attachments? })`, `gatePrompt({ messages })`. `gatePrompt` is Recursiv's own signup gate, not a moderation API: it answers only for keys on the platform's networks (404 anywhere else, including an app's own network), ignores `model`, and shares one daily LLM budget with Recursiv's signup page.
|
|
194
194
|
|
|
195
195
|
### r.projectBrain — Per-project knowledge base & task management
|
|
196
196
|
|
|
@@ -389,7 +389,7 @@ await r.auth.requestTwoFactorEmailCode(live, { projectId }) // app context brand
|
|
|
389
389
|
await r.auth.disableTwoFactor({ emailCode: '482913' }, live)
|
|
390
390
|
```
|
|
391
391
|
|
|
392
|
-
Two-factor methods: `enableTwoFactor`, `verifyTwoFactor`, `disableTwoFactor`, `regenerateTwoFactorBackupCodes`, `requestTwoFactorEmailCode`, `completeTwoFactorSignIn`, `completeTwoFactorSignInAndCreateKey`. OAuth consent screens: `exchangeSessionForCookie(token)` (browser, sets the API-origin cookie so `/api/auth/mcp/authorize` continues without a second sign-in), `getOAuthClientInfo({ clientId, redirectUri?, scope? })` (public client name, redirect hosts, `redirect_uri_registered`, granted scopes).
|
|
392
|
+
Two-factor methods: `enableTwoFactor`, `verifyTwoFactor`, `disableTwoFactor`, `regenerateTwoFactorBackupCodes`, `requestTwoFactorEmailCode`, `completeTwoFactorSignIn`, `completeTwoFactorSignInAndCreateKey`. Minds cutover: `legacyHandoff(legacyToken, { attemptId, refreshToken, projectId, timeoutMs })` exchanges the legacy app's session once; it throws `LegacyHandoffError` (`status` 0 when no response came, `code`, `retryAfterSeconds`), and only `legacy_token_used` / `legacy_token_invalid` mean forget the legacy session. With an `attemptId` a success carries `apiKeyDescription`; give the API key made with that session that `description`, so replays of the attempt count only that key. `legacyHandoffAndCreateKey(token, keyInput, { attemptId, refreshToken })` does both; pass the same `attemptId` on every retry. OAuth consent screens: `exchangeSessionForCookie(token)` (browser, sets the API-origin cookie so `/api/auth/mcp/authorize` continues without a second sign-in), `getOAuthClientInfo({ clientId, redirectUri?, scope? })` (public client name, redirect hosts, `redirect_uri_registered`, granted scopes).
|
|
393
393
|
|
|
394
394
|
**Mobile/React Native auth** (recommended — one call instead of three):
|
|
395
395
|
```typescript
|
|
@@ -502,14 +502,18 @@ Scroll back with `messages(id, { cursor: page.meta.next_cursor })`, not a growin
|
|
|
502
502
|
|
|
503
503
|
### r.e2ee — End-to-end encryption transport (flag-gated)
|
|
504
504
|
|
|
505
|
-
Relays opaque MLS envelopes, public device credentials and one-time key packages for the shared E2EE core; the server never sees keys or plaintext. Requires the `e2eeV2` network feature flag (otherwise `feature_disabled`) and a key acting as the account owner (agent-scoped keys get `agent_key_not_allowed`). The realm is the network id. Only a direct conversation between two people with an accepted request can start encryption (`conversation_not_eligible` otherwise); blocks apply (`blocked`); commits must send `expected_commit_seq`.
|
|
505
|
+
Relays opaque MLS envelopes, public device credentials and one-time key packages for the shared E2EE core; the server never sees keys or plaintext. Requires the `e2eeV2` network feature flag (otherwise `feature_disabled`) and a key acting as the account owner (agent-scoped keys get `agent_key_not_allowed`). The realm is the network id. Only a direct conversation between two people with an accepted request can start encryption (`conversation_not_eligible` otherwise), and every later append to it is checked the same way again (both people still accepted, still in the pilot, still only the two of them), so a client need not read the policy before each send; a resend of an event already stored still returns `duplicate: true`, even once a Welcome's target device is gone; blocks apply (`blocked`), to resends too; commits must send `expected_commit_seq`.
|
|
506
|
+
|
|
507
|
+
Ask first instead of trying a call: `r.e2ee.status()` returns `{ enabled: true }` or `{ enabled: false, reason: 'network_off' | 'not_allowlisted' }` for the calling account only. `enabled: false` means frozen: already-encrypted conversations stay readable, nothing new can be sent or enrolled.
|
|
506
508
|
|
|
507
509
|
```ts
|
|
510
|
+
const { data: access } = await r.e2ee.status(); // offer encryption only when access.enabled
|
|
508
511
|
const { data: device } = await r.e2ee.registerDevice({ device_name: 'phone-1', realm: networkId, signing_key, credential });
|
|
509
512
|
await r.e2ee.publishKeyPackages({ device_id: device.id, packages: [keyPackageBase64] }); // → { published, available }
|
|
510
513
|
const { data: stock } = await r.e2ee.keyPackageStatus({ device_id: device.id }); // { available, expiringSoon }; refill when low
|
|
511
514
|
const { data: claimed } = await r.e2ee.claimKeyPackage({ device_id: device.id, target_device_id: peerDeviceId });
|
|
512
515
|
await r.e2ee.appendEvent(conversationId, { device_id: device.id, envelope }); // idempotent per event_id
|
|
516
|
+
await r.e2ee.appendEvent(conversationId, { device_id: device.id, envelope: reaction, silent: true }); // no push, no move up the chat list; every member's doorbell still rings
|
|
513
517
|
const { data: page } = await r.e2ee.events(conversationId, { device_id: device.id, after_seq: 0 });
|
|
514
518
|
await r.e2ee.acknowledge(conversationId, { device_id: device.id, seq: page.headSeq });
|
|
515
519
|
```
|
|
@@ -808,7 +812,7 @@ Pagination: pass `limit` and `offset`. Check `meta.has_more`.
|
|
|
808
812
|
| `RateLimitError` | 429 | Rate limit hit (`error.retryAfter` seconds) |
|
|
809
813
|
| `ConflictError` | 409 | Resource already exists |
|
|
810
814
|
|
|
811
|
-
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).
|
|
812
816
|
|
|
813
817
|
### Idempotency
|
|
814
818
|
|