@recursiv/sdk 0.7.53 → 0.7.55
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -3
- package/dist/client.d.ts +1 -0
- package/dist/client.js +1 -0
- package/dist/errors.d.ts +13 -0
- package/dist/errors.js +17 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.js +1 -1
- package/dist/resources/auth.d.ts +63 -13
- package/dist/resources/auth.js +106 -40
- package/dist/resources/commands.d.ts +14 -1
- package/dist/resources/commands.js +7 -1
- package/dist/resources/curator.d.ts +1 -1
- package/dist/resources/discovery.d.ts +78 -1
- package/dist/resources/discovery.js +55 -6
- package/dist/resources/e2ee.d.ts +52 -6
- package/dist/resources/e2ee.js +33 -0
- package/dist/resources/human-input.d.ts +4 -1
- package/dist/resources/human-input.js +7 -2
- package/dist/resources/moderation.d.ts +5 -1
- package/dist/resources/moderation.js +6 -2
- package/dist/resources/notifications.d.ts +19 -0
- package/dist/resources/profiles.d.ts +7 -3
- package/dist/resources/profiles.js +5 -1
- package/dist/resources/protocols.d.ts +34 -2
- package/dist/resources/protocols.js +8 -2
- package/dist/resources/reports.d.ts +4 -0
- package/dist/resources/reports.js +5 -1
- package/dist/types.d.ts +54 -0
- package/llm.md +13 -7
- package/package.json +1 -1
package/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
|
|
|
@@ -675,6 +675,44 @@ what the viewer follows, joined, blocked or muted, and says why a list is empty
|
|
|
675
675
|
showings without a tap rest a suggestion for fourteen days, and engaging clears it.
|
|
676
676
|
Keep one `session` token per app session so a rail does not reshuffle on every render.
|
|
677
677
|
|
|
678
|
+
A category feed never ends at an empty page. `sdk.posts.list({ category, sort: 'hot' })`
|
|
679
|
+
(or `score`, or the default chronological order) pages the window of the category's
|
|
680
|
+
newest judged posts, then continues by keyset through the whole indexed
|
|
681
|
+
history: every page's `meta.next` is `{ offset }`, `{ cursor }` or `null`, and when a
|
|
682
|
+
stage ends `meta.fallover` lists where to go, in order: `category_older` (the keyset
|
|
683
|
+
cursor `next` also carries), `category_network` (the same category on the open
|
|
684
|
+
networks), the member's next categories (`via` names the order they came from), `for_you`
|
|
685
|
+
and `all`. A `DiscoveryFeedSession` follows the cursor itself and reports the Minds
|
|
686
|
+
feed's `fallover` on its pages. `sdk.discovery.category({ category, session? })` answers a
|
|
687
|
+
category's surface in one call: `stories`, `network` and `clips` (each shared per app and
|
|
688
|
+
category, refreshed after 30 s, kept up to 300 s and warmed as the warmer reaches the category, then filtered for the viewer on every call: the cached safety verdicts of story members, network posts and clips again, the term list, the viewer's blocks and mutes over clips, and the member's own hidden sources over network posts and story members) and the
|
|
689
|
+
viewer's `people` and `groups`; a section that timed out, failed or is off is empty with a
|
|
690
|
+
`reason` for that request only (a failure is never cached, so asking again retries it), and one this
|
|
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`). 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.
|
|
715
|
+
|
|
678
716
|
`sdk.posts.categories()` answers the app's categories in the order this member
|
|
679
717
|
should see them. `meta.order` is `personal`, `popular` or `static`; `meta.reason`
|
|
680
718
|
says why an answer is not personal (`low_signal`, `deadline`, `requested`,
|
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, 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: {
|
|
@@ -8,6 +30,20 @@ export interface AuthSession {
|
|
|
8
30
|
image: string | null;
|
|
9
31
|
};
|
|
10
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* What `getSession` answers: the account, and which of its sessions the token
|
|
35
|
+
* is. `session.id` is the id `settings.listSessions()` lists that session
|
|
36
|
+
* under. A client that authenticates with an API key gets `is_current: false`
|
|
37
|
+
* on every listed row (a key belongs to no session), so it matches this id to
|
|
38
|
+
* mark its own device.
|
|
39
|
+
*/
|
|
40
|
+
export interface CurrentSession extends AuthSession {
|
|
41
|
+
/** Null only when the server's answer names no session row. */
|
|
42
|
+
session: {
|
|
43
|
+
id: string;
|
|
44
|
+
expiresAt: string | null;
|
|
45
|
+
} | null;
|
|
46
|
+
}
|
|
11
47
|
export interface SignUpInput {
|
|
12
48
|
email: string;
|
|
13
49
|
password: string;
|
|
@@ -275,10 +311,10 @@ export declare class AuthResource {
|
|
|
275
311
|
success: true;
|
|
276
312
|
}>;
|
|
277
313
|
/**
|
|
278
|
-
* Get current session info using a session token
|
|
279
|
-
* Returns null if the session is invalid or expired.
|
|
314
|
+
* Get current session info using a session token: the account, and the
|
|
315
|
+
* session's id and expiry. Returns null if the session is invalid or expired.
|
|
280
316
|
*/
|
|
281
|
-
getSession(sessionToken: string): Promise<
|
|
317
|
+
getSession(sessionToken: string): Promise<CurrentSession | null>;
|
|
282
318
|
/**
|
|
283
319
|
* Sign out (invalidate session).
|
|
284
320
|
*/
|
|
@@ -342,18 +378,32 @@ export declare class AuthResource {
|
|
|
342
378
|
/**
|
|
343
379
|
* Exchange a legacy Minds session (the OAuth access token or minds_sess JWT
|
|
344
380
|
* the legacy mobile app stored) for a session on this stack. Only answers
|
|
345
|
-
* during the cutover window
|
|
381
|
+
* during the cutover window.
|
|
346
382
|
*
|
|
347
|
-
* A
|
|
348
|
-
*
|
|
349
|
-
*
|
|
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.
|
|
350
398
|
*/
|
|
351
|
-
legacyHandoff(legacyToken: string,
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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<{
|
|
357
407
|
apiKey: string;
|
|
358
408
|
apiKeyId: string;
|
|
359
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 ?? {});
|
|
@@ -596,8 +604,8 @@ export class AuthResource {
|
|
|
596
604
|
return { success: true };
|
|
597
605
|
}
|
|
598
606
|
/**
|
|
599
|
-
* Get current session info using a session token
|
|
600
|
-
* Returns null if the session is invalid or expired.
|
|
607
|
+
* Get current session info using a session token: the account, and the
|
|
608
|
+
* session's id and expiry. Returns null if the session is invalid or expired.
|
|
601
609
|
*/
|
|
602
610
|
async getSession(sessionToken) {
|
|
603
611
|
const res = await this.fetchWithTimeout(`${this.baseOrigin}/api/auth/get-session`, {
|
|
@@ -612,6 +620,8 @@ export class AuthResource {
|
|
|
612
620
|
const data = await res.json();
|
|
613
621
|
if (!data?.user)
|
|
614
622
|
return null;
|
|
623
|
+
const sessionId = data.session?.id;
|
|
624
|
+
const expiresAt = data.session?.expiresAt;
|
|
615
625
|
return {
|
|
616
626
|
token: sessionToken,
|
|
617
627
|
user: {
|
|
@@ -620,6 +630,9 @@ export class AuthResource {
|
|
|
620
630
|
email: data.user.email,
|
|
621
631
|
image: data.user.image ?? null,
|
|
622
632
|
},
|
|
633
|
+
session: typeof sessionId === 'string' && sessionId
|
|
634
|
+
? { id: sessionId, expiresAt: typeof expiresAt === 'string' ? expiresAt : null }
|
|
635
|
+
: null,
|
|
623
636
|
};
|
|
624
637
|
}
|
|
625
638
|
/**
|
|
@@ -629,10 +642,16 @@ export class AuthResource {
|
|
|
629
642
|
const res = await this.fetchWithTimeout(`${this.baseOrigin}/api/auth/sign-out`, {
|
|
630
643
|
method: 'POST',
|
|
631
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',
|
|
632
650
|
...originHeader(this.baseOrigin),
|
|
633
651
|
Authorization: `Bearer ${sessionToken}`,
|
|
634
652
|
Cookie: `better-auth.session_token=${sessionToken}`,
|
|
635
653
|
},
|
|
654
|
+
body: '{}',
|
|
636
655
|
});
|
|
637
656
|
if (!res.ok) {
|
|
638
657
|
let body = {};
|
|
@@ -757,53 +776,96 @@ export class AuthResource {
|
|
|
757
776
|
/**
|
|
758
777
|
* Exchange a legacy Minds session (the OAuth access token or minds_sess JWT
|
|
759
778
|
* the legacy mobile app stored) for a session on this stack. Only answers
|
|
760
|
-
* during the cutover window
|
|
779
|
+
* during the cutover window.
|
|
761
780
|
*
|
|
762
|
-
* A
|
|
763
|
-
*
|
|
764
|
-
*
|
|
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.
|
|
765
796
|
*/
|
|
766
|
-
async legacyHandoff(legacyToken,
|
|
767
|
-
const
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
},
|
|
777
|
-
body: JSON.stringify({ legacy_token: legacyToken }),
|
|
778
|
-
});
|
|
779
|
-
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);
|
|
780
807
|
try {
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
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;
|
|
793
849
|
}
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
throw new AuthenticationError({ type: 'auth_error', message: 'Legacy handoff returned no session', code: 'legacy_handoff_failed' });
|
|
850
|
+
finally {
|
|
851
|
+
clearTimeout(timer);
|
|
797
852
|
}
|
|
798
|
-
return { token, user: data.user };
|
|
799
853
|
}
|
|
800
|
-
/**
|
|
801
|
-
|
|
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 = {}) {
|
|
802
861
|
const session = await this.legacyHandoff(legacyToken, {
|
|
803
862
|
projectId: keyInput.projectId,
|
|
804
863
|
organizationId: keyInput.organizationId,
|
|
864
|
+
attemptId: options.attemptId,
|
|
865
|
+
refreshToken: options.refreshToken,
|
|
866
|
+
timeoutMs: options.timeoutMs,
|
|
805
867
|
});
|
|
806
|
-
const key = await this.createApiKeyOrRetireSession(keyInput, session);
|
|
868
|
+
const key = await this.createApiKeyOrRetireSession(session.apiKeyDescription && !keyInput.description ? { ...keyInput, description: session.apiKeyDescription } : keyInput, session);
|
|
807
869
|
return { apiKey: key.key, apiKeyId: key.id, user: session.user, session };
|
|
808
870
|
}
|
|
809
871
|
/**
|
|
@@ -839,9 +901,13 @@ export class AuthResource {
|
|
|
839
901
|
method: 'POST',
|
|
840
902
|
credentials: 'include',
|
|
841
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',
|
|
842
907
|
...originHeader(this.baseOrigin),
|
|
843
908
|
Authorization: `Bearer ${sessionToken}`,
|
|
844
909
|
},
|
|
910
|
+
body: '{}',
|
|
845
911
|
});
|
|
846
912
|
let data = null;
|
|
847
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
|
}
|
|
@@ -7,7 +7,7 @@ export type ForYouParams = {
|
|
|
7
7
|
offset?: number;
|
|
8
8
|
/** The previous page's `meta.next_cursor`: pages a stored ranking, so posts read meanwhile never shift the next page. */
|
|
9
9
|
cursor?: string;
|
|
10
|
-
/** Adds `meta.discovery` (layouts)
|
|
10
|
+
/** Adds `meta.discovery` (layouts); limit <= 25. Every page carries the promotion safety floor, with or without it. */
|
|
11
11
|
discovery?: boolean;
|
|
12
12
|
/** `discover` ranks for Discover: fresher, beyond the member's follows, with their category order. */
|
|
13
13
|
surface?: ForYouSurface;
|