@recursiv/sdk 0.7.54 → 0.7.57

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -75,7 +75,7 @@ for await (const chunk of r.agents.chatStream(agent.id, { message: 'Hello!' }))
75
75
  | Resource | Key methods |
76
76
  |----------|------------|
77
77
  | `r.agents` | `create`, `update`, `chat`, `chatStream`, `retryChatStream`, `grantProjectAccess`, `conversations` |
78
- | `r.e2ee` | `registerDevice`, `devices`, `revokeDevice`, `publishKeyPackages`, `keyPackageStatus`, `claimKeyPackage`, `appendEvent`, `events`, `acknowledge` (flag-gated E2EE v2 transport) |
78
+ | `r.e2ee` | `status`, `registerDevice`, `devices`, `revokeDevice`, `publishKeyPackages`, `keyPackageStatus`, `claimKeyPackage`, `appendEvent`, `events`, `acknowledge`, `conversations`; device linking by scan: `createLink`, `link`, `scannedLink`, `approveLink`, `completeLink`, `linkProgress`, `cancelLink` (flag-gated E2EE v2 transport; its doorbell is `r.realtime.onE2eeEvent`, which carries a small message's stored ciphertext to the owner's own sockets; link and device changes arrive on `r.realtime.onE2eeLink` and `onE2eeDevices`) |
79
79
  | `r.brain` | `sendMessage` |
80
80
  | `r.commands` | `execute`, `gatePrompt` |
81
81
  | `r.projectBrain` | `tasks`, `decisions`, `milestones`, `completeTask`, `usage` |
@@ -84,7 +84,7 @@ for await (const chunk of r.agents.chatStream(agent.id, { message: 'Hello!' }))
84
84
  ### Identity & Auth
85
85
  | Resource | Key methods |
86
86
  |----------|------------|
87
- | `r.auth` | `signUp`, `signIn`, `signInWithUsername`, `getSession`, `signOut`, `createApiKey`, `forgetPassword`, `resetPassword`; two-factor: `enableTwoFactor`, `verifyTwoFactor`, `disableTwoFactor`, `regenerateTwoFactorBackupCodes`, `requestTwoFactorEmailCode`, `completeTwoFactorSignIn`; OAuth consent: `exchangeSessionForCookie`, `getOAuthClientInfo` (session calls use explicit Bearer auth with a native/server Cookie fallback) |
87
+ | `r.auth` | `signUp`, `signIn`, `signInWithUsername`, `getSession`, `signOut`, `createApiKey`, `forgetPassword`, `resetPassword`; two-factor: `enableTwoFactor`, `verifyTwoFactor`, `disableTwoFactor`, `regenerateTwoFactorBackupCodes`, `requestTwoFactorEmailCode`, `completeTwoFactorSignIn`; OAuth consent: `exchangeSessionForCookie`, `getOAuthClientInfo`; cutover: `legacyHandoff` (a legacy Minds session for a session here; throws `LegacyHandoffError`) (session calls use explicit Bearer auth with a native/server Cookie fallback) |
88
88
  | `@recursiv/sdk/next` | `createRecursivAuth` — cookie-session server auth for Next.js App Router (see below) |
89
89
  | `r.users` | `me`, `get` |
90
90
  | `r.organizations` | `create`, `members`, `addMember`, `invite` |
@@ -115,7 +115,7 @@ for await (const chunk of r.agents.chatStream(agent.id, { message: 'Hello!' }))
115
115
  | `r.admin` | User management, stats, content reports (`listReports`, `resolveReport`, `dismissReport`) |
116
116
  | `r.dispatcher` | Task queue, claims, signals, outcomes, webhooks |
117
117
  | `r.goals` | Org goals with task rollups (group dispatcher tasks) |
118
- | `r.protocols` | Private Minds safety: `sourceControls` (current enablement and saved choices; filter with `control`, each item may carry a `display` label), `setSourceControl`, `onSourceControlChange`, `reportSource`. Delegated accounts: `nativeSocialAccounts`, `startNativeSocialConnection`, `finishNativeSocialConnection`, `disconnectNativeSocialAccount`. Native follows: `nativeFollowStates` (every connected account's state for one source, one read), `nativeFollowState`, `prepareNativeFollow`, `submitNativeFollow`, `nativeFollowStatus` (native following or pending approval, distinct from a Minds feed subscription; preserve the complete signed Nostr list). Native reposts and undo: `nativeRepostState`, `prepareNativeRepost`, `submitNativeRepost`, `nativeRepostStatus` (public posts, explicit approval, connected account, no uncertain retries). Native likes and unlikes: `nativeLikeState`, `prepareNativeLike`, `submitNativeLike`, `nativeLikeStatus` (connected authority, app enablement and explicit approval; resume pending actions instead of duplicating them). Reviewed replies and optional linked posts: `prepareNativeReply`, `submitNativeReply`, `nativeReplyStatus`, `nativeReplyHistory`, `prepareNativeReplyRemoval`, `removeNativeReply` (require deployed API support and app enablement). Keyless shared post links: `sharedNetworks(projectId)`, `getSharedConversation(projectId, { protocol, post, readRef? })`. Member public reading: `searchPublicPosts` (selected account required for Bluesky/ActivityPub; public Nostr search), `getConnectedSocialConversation` (preserve result readerAccountId/readRef), `publicNetworks`, `searchPublicProfiles`, `getPublicProfile`, `getPublicAuthorFeed`, `getFollowingFeed`, `getPublicConversation`, `getPublicTrends`, `getPublicTrendFeed`; radar stories: `getPublicRadarStory` (one story by id with its posts; one that has left the radar comes with its archived copy and the live story it continues as), keyless `getSharedRadarStory(projectId, { cluster })` and `getSharedRadarPosts(projectId, { cluster })`; private app subscriptions: `listSubscriptions`, `subscribeToProfile`, `unsubscribeFromProfile`, `onSubscriptionChange`. `list` returns bounded adapter `maturity`, `limitations`, and exhaustive `supported`/`planned`/`unsupported` implementation capabilities (not provider health or enablement); settings, candidate refresh/status, search, and read-only quarantined candidate review via `listCandidates` / `getCandidate` |
118
+ | `r.protocols` | Private Minds safety: `sourceControls` (current enablement and saved choices; filter with `control`, each item may carry a `display` label), `setSourceControl`, `onSourceControlChange`, `reportSource`. Delegated accounts: `nativeSocialAccounts`, `startNativeSocialConnection`, `finishNativeSocialConnection`, `disconnectNativeSocialAccount`. Native follows: `nativeFollowStates` (every connected account's state for one source, one read), `nativeFollowState`, `prepareNativeFollow`, `submitNativeFollow`, `nativeFollowStatus` (native following or pending approval, distinct from a Minds feed subscription; preserve the complete signed Nostr list). Native reposts and undo: `nativeRepostState`, `prepareNativeRepost`, `submitNativeRepost`, `nativeRepostStatus` (public posts, explicit approval, connected account, no uncertain retries). Native likes and unlikes: `nativeLikeState`, `prepareNativeLike`, `submitNativeLike`, `nativeLikeStatus` (connected authority, app enablement and explicit approval; resume pending actions instead of duplicating them). Reviewed replies and optional linked posts: `prepareNativeReply`, `submitNativeReply`, `nativeReplyStatus`, `nativeReplyHistory`, `prepareNativeReplyRemoval`, `removeNativeReply` (require deployed API support and app enablement). Keyless shared post links: `sharedNetworks(projectId)`, `getSharedConversation(projectId, { protocol, post, readRef? })`. Member public reading: `searchPublicPosts` (selected account required for Bluesky/ActivityPub; public Nostr search), `getConnectedSocialConversation` (preserve result readerAccountId/readRef), `publicNetworks`, `searchPublicProfiles`, `getPublicProfile`, `getPublicAuthorFeed`, `getFollowingFeed`, `getPublicConversation`, `getPublicTrends`, `getPublicTrendFeed`; radar stories: `getPublicRadarStory` (one story by id with its posts; one that has left the radar comes with its archived copy and the live story it continues as; the id may be a World Today edition story's, and the answer then names it in `editionStory` under the story's radar id), keyless `getSharedRadarStory(projectId, { cluster })` and `getSharedRadarPosts(projectId, { cluster })` (keyless reads carry images and videos only from outlets and members marked `visitorMedia`; signed-in reads keep all media, so a share card uses only those); private app subscriptions: `listSubscriptions`, `subscribeToProfile`, `unsubscribeFromProfile`, `onSubscriptionChange`. `list` returns bounded adapter `maturity`, `limitations`, and exhaustive `supported`/`planned`/`unsupported` implementation capabilities (not provider health or enablement); settings, candidate refresh/status, search, and read-only quarantined candidate review via `listCandidates` / `getCandidate` |
119
119
 
120
120
  Full reference: see [llm.md](./llm.md) (ships with this package) or the type definitions in `dist/`.
121
121
 
@@ -351,7 +351,7 @@ import {
351
351
  AuthorizationError, // 403 — API key lacks required scope
352
352
  NotFoundError, // 404
353
353
  ValidationError, // 400 — check error.details
354
- RateLimitError, // 429 — includes retryAfter, upgradeUrl
354
+ RateLimitError, // 429 — includes retryAfter, resetAt, scope, upgradeUrl
355
355
  ConflictError, // 409
356
356
  } from '@recursiv/sdk'
357
357
 
@@ -364,7 +364,7 @@ try {
364
364
  }
365
365
  ```
366
366
 
367
- Auto-retry is built in. 429 and 5xx responses retry with exponential backoff (configurable via `maxRetries`, default 2).
367
+ Auto-retry is built in. 429 and 5xx responses retry with exponential backoff (configurable via `maxRetries`, default 2). A 429 with `X-RateLimit-Scope: action` refuses one member action past its budget and is never retried: its `RateLimitError` has `scope: 'action'` and `resetAt` (a `Date`, when that action's budget reopens).
368
368
 
369
369
  ## Pagination
370
370
 
@@ -689,20 +689,29 @@ category, refreshed after 30 s, kept up to 300 s and warmed as the warmer reache
689
689
  viewer's `people` and `groups`; a section that timed out, failed or is off is empty with a
690
690
  `reason` for that request only (a failure is never cached, so asking again retries it), and one this
691
691
  key may not read is empty with `refused` (`stories` and `network` need the member's own app
692
- key with `users:read`; `people` and `groups` need `users:read`). In a category, a Bluesky
693
- topic the provider no longer lists, or whose feed holds nothing in the category, is served
694
- the network's indexed posts in it (`sourceIndex.reason` of `gone` or `empty`). While the
695
- provider has more of the topic the page's cursor is the provider's own, so the topic's
696
- deeper pages are still read; once it has none, or the topic is gone, the cursor is an
697
- `idx:tf:` one that continues in the index. When the index cannot answer (busy, or its safety
698
- cache unreadable), the page is what the provider answered: the empty page with its own
699
- cursor, or the gone topic's 404; only an `idx:tf:` page answers 503 `unavailable`, and that
700
- ends the index continuation, not the network. The app contract: a `trend_unavailable` 404,
701
- an empty topic page or a failed `idx:tf:` page never disables a network for the session;
702
- move to the next topic, keep every provider continuation, start the `idx:tf:` stream once
703
- per network and session (every such topic answers the same index stream; after a failed
704
- page it may start again), and pass cursors back unchanged. `DiscoveryFeedSession` does
705
- this for you.
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
@@ -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
@@ -1,4 +1,4 @@
1
- import { RecursivError, AuthenticationError, AuthorizationError, NotFoundError, ValidationError, InsufficientCreditsError, RateLimitError, ConflictError, errorBodyFromResponse, httpStatusMessage, } from './errors.js';
1
+ import { RecursivError, AuthenticationError, AuthorizationError, NotFoundError, ValidationError, InsufficientCreditsError, RateLimitError, ConflictError, errorBodyFromResponse, httpStatusMessage, isActionRateLimitScope, rateLimitDetailsFromResponse, } from './errors.js';
2
2
  const DEFAULT_BASE_URL = 'https://api.recursiv.io/api/v1';
3
3
  export class HttpClient {
4
4
  #apiKey;
@@ -59,6 +59,13 @@ export class HttpClient {
59
59
  if (response.status === 429 || response.status >= 500) {
60
60
  if (attempt === this.maxRetries)
61
61
  return response;
62
+ // X-RateLimit-Scope: action refuses this one action (a post, a follow,
63
+ // a report) past the member's budget. The API is not busy: the same
64
+ // request is refused again until the budget reopens, or, sent late,
65
+ // becomes a write the member saw refused. The caller gets the 429 now,
66
+ // with RateLimitError.scope and resetAt.
67
+ if (response.status === 429 && isActionRateLimitScope(response.headers))
68
+ return response;
62
69
  const retryAfter = response.headers.get('retry-after');
63
70
  const delay = retryAfter
64
71
  ? parseInt(retryAfter, 10) * 1000
@@ -163,7 +170,7 @@ export class HttpClient {
163
170
  case 409:
164
171
  throw new ConflictError(err);
165
172
  case 429:
166
- throw new RateLimitError(err);
173
+ throw new RateLimitError(err, rateLimitDetailsFromResponse(response.headers, err));
167
174
  default:
168
175
  throw new RecursivError(response.status, err);
169
176
  }
@@ -182,6 +189,7 @@ export class HttpClient {
182
189
  headers: {
183
190
  ...this.getAuthHeaders(),
184
191
  'Content-Type': 'application/json',
192
+ ...(options?.idempotencyKey ? { 'Idempotency-Key': options.idempotencyKey } : {}),
185
193
  },
186
194
  body: body ? JSON.stringify(body) : undefined,
187
195
  };
package/dist/errors.d.ts CHANGED
@@ -24,6 +24,19 @@ export declare class RecursivError extends Error {
24
24
  export declare class AuthenticationError extends RecursivError {
25
25
  constructor(body: ApiErrorBody['error']);
26
26
  }
27
+ /**
28
+ * A failed legacy session handoff (`auth.legacyHandoff`). It keeps what a
29
+ * caller needs to tell a refusal from an outage: the HTTP status (0 when no
30
+ * response arrived), the server's code (`timeout` or `network_error` when no
31
+ * response arrived), and the seconds the server's Retry-After asked for.
32
+ * Only `legacy_token_used` and `legacy_token_invalid` are final; every other
33
+ * failure leaves the legacy session worth trying again.
34
+ */
35
+ export declare class LegacyHandoffError extends RecursivError {
36
+ /** Seconds from the server's Retry-After header, when it sent one. */
37
+ readonly retryAfterSeconds: number | null;
38
+ constructor(status: number, body: ApiErrorBody['error']);
39
+ }
27
40
  export declare class AuthorizationError extends RecursivError {
28
41
  constructor(body: ApiErrorBody['error']);
29
42
  }
@@ -37,9 +50,36 @@ export declare class InsufficientCreditsError extends RecursivError {
37
50
  readonly upgradeUrl: string;
38
51
  constructor(body: ApiErrorBody['error']);
39
52
  }
53
+ /** What a 429 says beyond its body: which budget refused it, and when that budget reopens. */
54
+ export interface RateLimitDetails {
55
+ /**
56
+ * `'action'` when the server refused one member action past its budget
57
+ * (a post, a follow, a report: `X-RateLimit-Scope: action`). Only that
58
+ * action is refused, the rest of the API is not busy, and sending it again
59
+ * before `resetAt` is refused again. `undefined` for any other 429.
60
+ */
61
+ scope?: 'action';
62
+ /**
63
+ * When the refused budget reopens, for showing in the member's local time.
64
+ * From `X-RateLimit-Reset`, else `Retry-After`, else the body's
65
+ * `retry_after`; `undefined` when none of them says.
66
+ */
67
+ resetAt?: Date;
68
+ }
69
+ /** Whether a response's headers mark its 429 as refusing one action (`X-RateLimit-Scope: action`). */
70
+ export declare function isActionRateLimitScope(headers: Pick<Headers, 'get'> | null | undefined): boolean;
71
+ /**
72
+ * The scope and reset of a 429, read from its headers, and from the body's
73
+ * `retry_after` (seconds) when neither reset header was sent.
74
+ */
75
+ export declare function rateLimitDetailsFromResponse(headers: Pick<Headers, 'get'> | null | undefined, body?: Pick<ApiErrorBody['error'], 'retry_after'>, now?: number): RateLimitDetails;
40
76
  export declare class RateLimitError extends RecursivError {
41
77
  readonly upgradeUrl: string;
42
- constructor(body: ApiErrorBody['error']);
78
+ /** `'action'` when only this one action was refused (see RateLimitDetails.scope); otherwise undefined. */
79
+ readonly scope?: 'action';
80
+ /** When the refused budget reopens (see RateLimitDetails.resetAt); undefined when the server did not say. */
81
+ readonly resetAt?: Date;
82
+ constructor(body: ApiErrorBody['error'], details?: RateLimitDetails);
43
83
  }
44
84
  export declare class ConflictError extends RecursivError {
45
85
  constructor(body: ApiErrorBody['error']);
package/dist/errors.js CHANGED
@@ -66,6 +66,23 @@ export class AuthenticationError extends RecursivError {
66
66
  this.name = 'AuthenticationError';
67
67
  }
68
68
  }
69
+ /**
70
+ * A failed legacy session handoff (`auth.legacyHandoff`). It keeps what a
71
+ * caller needs to tell a refusal from an outage: the HTTP status (0 when no
72
+ * response arrived), the server's code (`timeout` or `network_error` when no
73
+ * response arrived), and the seconds the server's Retry-After asked for.
74
+ * Only `legacy_token_used` and `legacy_token_invalid` are final; every other
75
+ * failure leaves the legacy session worth trying again.
76
+ */
77
+ export class LegacyHandoffError extends RecursivError {
78
+ /** Seconds from the server's Retry-After header, when it sent one. */
79
+ retryAfterSeconds;
80
+ constructor(status, body) {
81
+ super(status, body);
82
+ this.name = 'LegacyHandoffError';
83
+ this.retryAfterSeconds = typeof body.retry_after === 'number' ? body.retry_after : null;
84
+ }
85
+ }
69
86
  export class AuthorizationError extends RecursivError {
70
87
  constructor(body) {
71
88
  super(403, body);
@@ -93,11 +110,70 @@ export class InsufficientCreditsError extends RecursivError {
93
110
  this.message += `\n Upgrade: ${this.upgradeUrl}`;
94
111
  }
95
112
  }
113
+ /** Whether a response's headers mark its 429 as refusing one action (`X-RateLimit-Scope: action`). */
114
+ export function isActionRateLimitScope(headers) {
115
+ return headers?.get?.('x-ratelimit-scope')?.trim().toLowerCase() === 'action';
116
+ }
117
+ // A bare number below this is seconds from now, not an epoch (1e9 s is 2001-09-09).
118
+ const EPOCH_SECONDS_FLOOR = 1e9;
119
+ // A bare number at or above this is epoch milliseconds (1e12 ms is 2001-09-09).
120
+ const EPOCH_MS_FLOOR = 1e12;
121
+ function dateOrUndefined(ms) {
122
+ return Number.isFinite(ms) ? new Date(ms) : undefined;
123
+ }
124
+ /** X-RateLimit-Reset: epoch seconds from Recursiv; epoch ms, seconds from now or an ISO/HTTP date elsewhere. */
125
+ function resetFromRateLimitReset(raw, now) {
126
+ const value = raw?.trim();
127
+ if (!value)
128
+ return undefined;
129
+ if (/^\d+(\.\d+)?$/.test(value)) {
130
+ const n = Number(value);
131
+ if (n >= EPOCH_MS_FLOOR)
132
+ return dateOrUndefined(n);
133
+ if (n >= EPOCH_SECONDS_FLOOR)
134
+ return dateOrUndefined(n * 1000);
135
+ return dateOrUndefined(now + n * 1000);
136
+ }
137
+ return dateOrUndefined(Date.parse(value));
138
+ }
139
+ /** Retry-After: seconds from now, or an HTTP date. */
140
+ function resetFromRetryAfter(raw, now) {
141
+ const value = raw?.trim();
142
+ if (!value)
143
+ return undefined;
144
+ if (/^\d+$/.test(value))
145
+ return dateOrUndefined(now + Number(value) * 1000);
146
+ return dateOrUndefined(Date.parse(value));
147
+ }
148
+ /**
149
+ * The scope and reset of a 429, read from its headers, and from the body's
150
+ * `retry_after` (seconds) when neither reset header was sent.
151
+ */
152
+ export function rateLimitDetailsFromResponse(headers, body, now = Date.now()) {
153
+ const retryAfterSeconds = body?.retry_after;
154
+ const resetAt = resetFromRateLimitReset(headers?.get?.('x-ratelimit-reset'), now)
155
+ ?? resetFromRetryAfter(headers?.get?.('retry-after'), now)
156
+ ?? (typeof retryAfterSeconds === 'number' && retryAfterSeconds >= 0
157
+ ? dateOrUndefined(now + retryAfterSeconds * 1000)
158
+ : undefined);
159
+ const details = {};
160
+ if (isActionRateLimitScope(headers))
161
+ details.scope = 'action';
162
+ if (resetAt)
163
+ details.resetAt = resetAt;
164
+ return details;
165
+ }
96
166
  export class RateLimitError extends RecursivError {
97
167
  upgradeUrl;
98
- constructor(body) {
168
+ /** `'action'` when only this one action was refused (see RateLimitDetails.scope); otherwise undefined. */
169
+ scope;
170
+ /** When the refused budget reopens (see RateLimitDetails.resetAt); undefined when the server did not say. */
171
+ resetAt;
172
+ constructor(body, details = {}) {
99
173
  super(429, body);
100
174
  this.name = 'RateLimitError';
175
+ this.scope = details.scope;
176
+ this.resetAt = details.resetAt;
101
177
  this.upgradeUrl = body?.upgrade_url || body.details?.upgrade_url || 'https://recursiv.io/pricing';
102
178
  // Append upgrade hint for free tier users
103
179
  if (body.message?.includes('free tier') ||
package/dist/index.d.ts CHANGED
@@ -147,14 +147,14 @@ export { HttpClient } from './client.js';
147
147
  export { RealtimeClient } from './realtime.js';
148
148
  export type { RealtimeConnectOptions } from './realtime.js';
149
149
  export { AuthResource } from './resources/auth.js';
150
- export type { AuthSession, CurrentSession, SignUpInput, SignInInput, UsernameSignInInput, AppContext, ResetPasswordInput, TwoFactorConfirmation, TwoFactorEnrollment, TwoFactorChallenge, CompleteTwoFactorSignInInput, OAuthClientInfo, SendOtpInput, VerifyOtpInput, CreateApiKeyInput, ApiKeyResult, } from './resources/auth.js';
150
+ export type { AuthSession, CurrentSession, SignUpInput, SignInInput, UsernameSignInInput, AppContext, ResetPasswordInput, TwoFactorConfirmation, TwoFactorEnrollment, TwoFactorChallenge, CompleteTwoFactorSignInInput, OAuthClientInfo, SendOtpInput, VerifyOtpInput, CreateApiKeyInput, ApiKeyResult, LegacyHandoffOptions, LegacyHandoffSession, } from './resources/auth.js';
151
151
  export { PostsResource } from './resources/posts.js';
152
152
  export type { CategoryOrder, CategoryOrderMeta, CategoryOrderReason } from './resources/posts.js';
153
153
  export { UsersResource } from './resources/users.js';
154
154
  export { CommunitiesResource } from './resources/communities.js';
155
155
  export { ChatResource } from './resources/chat.js';
156
156
  export { E2eeResource } from './resources/e2ee.js';
157
- export type { E2eeDevice, E2eeEnvelope, E2eeEvent, E2eeKeyPackageStatus, E2eeObjectDownload, E2eeObjectUpload } from './resources/e2ee.js';
157
+ export type { E2eeDevice, E2eeDevicesRealtimeEvent, E2eeEnvelope, E2eeEvent, E2eeKeyPackageStatus, E2eeLink, E2eeLinkCancelledBy, E2eeLinkProgress, E2eeLinkRealtimeEvent, E2eeLinkState, E2eeObjectDownload, E2eeObjectUpload, E2eeRealtimeEvent, E2eeStatus, E2eeStatusReason, } from './resources/e2ee.js';
158
158
  export { ProjectsResource } from './resources/projects.js';
159
159
  export { AgentsResource, SUPPORTED_MODELS } from './resources/agents.js';
160
160
  export { EvidenceResource } from './resources/evidence.js';
@@ -165,7 +165,7 @@ export { SandboxResource } from './resources/sandbox.js';
165
165
  export { ProfilesResource } from './resources/profiles.js';
166
166
  export { OrganizationsResource } from './resources/organizations.js';
167
167
  export { NotificationsResource } from './resources/notifications.js';
168
- export type { Notification, NotificationActor, NotificationCategory, NotificationPostPreview, NotificationScopeParams, NotificationUnreadCount, ListNotificationsParams, NotificationListResponse, } from './resources/notifications.js';
168
+ export type { Notification, NotificationActor, NotificationCategory, NotificationPostPreview, NotificationPostTag, NotificationScopeParams, NotificationUnreadCount, ListNotificationsParams, NotificationListResponse, } from './resources/notifications.js';
169
169
  export { SettingsResource } from './resources/settings.js';
170
170
  export { BillingResource } from './resources/billing.js';
171
171
  export { FreeTierResource } from './resources/free-tier.js';
@@ -240,7 +240,8 @@ export { TemplatesResource } from './resources/templates.js';
240
240
  export type { Template, CreateTemplateInput, UpdateTemplateInput, ListTemplatesParams } from './resources/templates.js';
241
241
  export { MediaResource } from './resources/media.js';
242
242
  export type { GenerateImageInput, GeneratedImage, ImageProvider } from './resources/media.js';
243
- export { RecursivError, AuthenticationError, AuthorizationError, NotFoundError, ValidationError, InsufficientCreditsError, RateLimitError, ConflictError, } from './errors.js';
243
+ export { RecursivError, AuthenticationError, AuthorizationError, LegacyHandoffError, NotFoundError, ValidationError, InsufficientCreditsError, RateLimitError, ConflictError, isActionRateLimitScope, } from './errors.js';
244
+ export type { RateLimitDetails } from './errors.js';
244
245
  export { parseSSE } from './lib/sse.js';
245
246
  export * from './lib/webhooks.js';
246
247
  export type * from './types.js';
package/dist/index.js CHANGED
@@ -277,6 +277,6 @@ export { HumanInputResource } from './resources/human-input.js';
277
277
  export { SwarmsResource } from './resources/swarms.js';
278
278
  export { TemplatesResource } from './resources/templates.js';
279
279
  export { MediaResource } from './resources/media.js';
280
- export { RecursivError, AuthenticationError, AuthorizationError, NotFoundError, ValidationError, InsufficientCreditsError, RateLimitError, ConflictError, } from './errors.js';
280
+ export { RecursivError, AuthenticationError, AuthorizationError, LegacyHandoffError, NotFoundError, ValidationError, InsufficientCreditsError, RateLimitError, ConflictError, isActionRateLimitScope, } from './errors.js';
281
281
  export { parseSSE } from './lib/sse.js';
282
282
  export * from './lib/webhooks.js';
@@ -1,5 +1,6 @@
1
1
  import type { Socket } from 'socket.io-client';
2
2
  import { HttpClient } from './client.js';
3
+ import type { E2eeDevicesRealtimeEvent, E2eeLinkRealtimeEvent, E2eeRealtimeEvent } from './resources/e2ee.js';
3
4
  export interface ChatMessageEvent {
4
5
  id: string;
5
6
  conversationId: string;
@@ -145,7 +146,8 @@ export declare class RealtimeClient {
145
146
  * replacement. Returns an unsubscribe function.
146
147
  *
147
148
  * Server events include `notification`, `feed_update`,
148
- * `conversation_created`, `chat_message`, `chat_typing` and `agent_thinking`.
149
+ * `conversation_created`, `chat_message`, `chat_typing`, `agent_thinking`,
150
+ * `e2ee:event`, `e2ee:link` and `e2ee:devices`.
149
151
  */
150
152
  on<T = unknown>(event: string, listener: (payload: T) => void): () => void;
151
153
  /** Join a conversation room now, and again after every reconnect, until you leave it. */
@@ -157,4 +159,25 @@ export declare class RealtimeClient {
157
159
  onMessageDelete(callback: (msg: ChatMessageDeleteEvent) => void): (() => void) | undefined;
158
160
  onTyping(callback: (event: TypingEvent) => void): (() => void) | undefined;
159
161
  onAgentThinking(callback: (event: AgentThinkingEvent) => void): (() => void) | undefined;
162
+ /**
163
+ * The E2EE doorbell after each append to an encrypted conversation this
164
+ * account belongs to: `{ conversationId, seq }`, plus the stored `event`
165
+ * when this socket may carry it (see E2eeRealtimeEvent). Pass `event` to the
166
+ * engine's `applyPushed`; without one, sync the conversation.
167
+ */
168
+ onE2eeEvent(callback: (event: E2eeRealtimeEvent) => void): () => void;
169
+ /**
170
+ * A device link of this account changed: `{ linkId, state }`, state
171
+ * `scanned`, `approved`, `completed`, `progress` (at most once a second) or
172
+ * `cancelled`. Only on sockets opened by a sign-in with the link's exact
173
+ * binding; there is never an event when a link opens. Read the link with
174
+ * `r.e2ee.link(linkId)`; poll as well, since an event can be missed.
175
+ */
176
+ onE2eeLink(callback: (event: E2eeLinkRealtimeEvent) => void): () => void;
177
+ /**
178
+ * One of this account's devices changed in a way the others act on:
179
+ * `{ deviceId, reason: 'packages' }` when it can now be claimed, or
180
+ * `{ deviceId, reason: 'revoked' }`.
181
+ */
182
+ onE2eeDevices(callback: (event: E2eeDevicesRealtimeEvent) => void): () => void;
160
183
  }
package/dist/realtime.js CHANGED
@@ -368,7 +368,8 @@ export class RealtimeClient {
368
368
  * replacement. Returns an unsubscribe function.
369
369
  *
370
370
  * Server events include `notification`, `feed_update`,
371
- * `conversation_created`, `chat_message`, `chat_typing` and `agent_thinking`.
371
+ * `conversation_created`, `chat_message`, `chat_typing`, `agent_thinking`,
372
+ * `e2ee:event`, `e2ee:link` and `e2ee:devices`.
372
373
  */
373
374
  on(event, listener) {
374
375
  // Every call is its own subscription, even for a function that is already
@@ -425,4 +426,31 @@ export class RealtimeClient {
425
426
  onAgentThinking(callback) {
426
427
  return this.on('agent_thinking', callback);
427
428
  }
429
+ /**
430
+ * The E2EE doorbell after each append to an encrypted conversation this
431
+ * account belongs to: `{ conversationId, seq }`, plus the stored `event`
432
+ * when this socket may carry it (see E2eeRealtimeEvent). Pass `event` to the
433
+ * engine's `applyPushed`; without one, sync the conversation.
434
+ */
435
+ onE2eeEvent(callback) {
436
+ return this.on('e2ee:event', callback);
437
+ }
438
+ /**
439
+ * A device link of this account changed: `{ linkId, state }`, state
440
+ * `scanned`, `approved`, `completed`, `progress` (at most once a second) or
441
+ * `cancelled`. Only on sockets opened by a sign-in with the link's exact
442
+ * binding; there is never an event when a link opens. Read the link with
443
+ * `r.e2ee.link(linkId)`; poll as well, since an event can be missed.
444
+ */
445
+ onE2eeLink(callback) {
446
+ return this.on('e2ee:link', callback);
447
+ }
448
+ /**
449
+ * One of this account's devices changed in a way the others act on:
450
+ * `{ deviceId, reason: 'packages' }` when it can now be claimed, or
451
+ * `{ deviceId, reason: 'revoked' }`.
452
+ */
453
+ onE2eeDevices(callback) {
454
+ return this.on('e2ee:devices', callback);
455
+ }
428
456
  }
@@ -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; every failure means "show the OTP screen".
381
+ * during the cutover window.
360
382
  *
361
- * A legacy token is single-use: after one successful exchange the server
362
- * answers `legacy_token_used`, so erase the stored legacy token as soon as
363
- * 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.
364
398
  */
365
- legacyHandoff(legacyToken: string, appContext?: {
366
- projectId?: string;
367
- organizationId?: string;
368
- }): Promise<AuthSession>;
369
- /** legacyHandoff, then the app API key, exactly like verifyOtpAndCreateKey. */
370
- 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<{
371
407
  apiKey: string;
372
408
  apiKeyId: string;
373
409
  user: AuthSession['user'];