@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 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
@@ -35,6 +35,7 @@ export declare class HttpClient {
35
35
  }): Promise<T>;
36
36
  post<T>(path: string, body?: unknown, options?: {
37
37
  retry?: boolean;
38
+ idempotencyKey?: string;
38
39
  }): Promise<T>;
39
40
  put<T>(path: string, body?: unknown, options?: {
40
41
  retry?: boolean;
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';
@@ -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<AuthSession | null>;
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; every failure means "show the OTP screen".
381
+ * during the cutover window.
346
382
  *
347
- * A legacy token is single-use: after one successful exchange the server
348
- * answers `legacy_token_used`, so erase the stored legacy token as soon as
349
- * this resolves and never retry it.
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, appContext?: {
352
- projectId?: string;
353
- organizationId?: string;
354
- }): Promise<AuthSession>;
355
- /** legacyHandoff, then the app API key, exactly like verifyOtpAndCreateKey. */
356
- legacyHandoffAndCreateKey(legacyToken: string, keyInput: CreateApiKeyInput): Promise<{
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'];
@@ -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; every failure means "show the OTP screen".
779
+ * during the cutover window.
761
780
  *
762
- * A legacy token is single-use: after one successful exchange the server
763
- * answers `legacy_token_used`, so erase the stored legacy token as soon as
764
- * this resolves and never retry it.
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, appContext) {
767
- const res = await this.fetchWithTimeout(`${this.baseOrigin}/api/auth/legacy-handoff`, {
768
- method: 'POST',
769
- headers: {
770
- 'Content-Type': 'application/json',
771
- ...originHeader(this.baseOrigin),
772
- ...(appContext?.projectId ? { 'x-recursiv-app-project': appContext.projectId } : {}),
773
- ...(appContext?.organizationId
774
- ? { 'x-recursiv-app-organization': appContext.organizationId }
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
- data = await res.json();
782
- }
783
- catch {
784
- data = null;
785
- }
786
- if (!res.ok) {
787
- const { code, message } = serverErrorInfo(data);
788
- throw new AuthenticationError({
789
- type: 'auth_error',
790
- message: message || `Legacy handoff failed: HTTP ${res.status}`,
791
- code: code || 'legacy_handoff_failed',
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
- const token = this.extractToken(res, data);
795
- if (!token) {
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
- /** legacyHandoff, then the app API key, exactly like verifyOtpAndCreateKey. */
801
- async legacyHandoffAndCreateKey(legacyToken, keyInput) {
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
- /** Run the gatekeeper prompt to evaluate an idea for access */
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
- /** Run the gatekeeper prompt to evaluate an idea for access */
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) and the public safety floor; limit <= 25. */
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;