@recursiv/sdk 0.7.51 → 0.7.52

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
@@ -140,6 +140,7 @@ deployment, throw the normal SDK errors; do not present them as an empty queue.
140
140
  ```typescript
141
141
  const r = new Recursiv({
142
142
  apiKey: 'sk_live_...', // or set RECURSIV_API_KEY env var
143
+ inheritEnvironmentApiKey: true, // default; false guarantees keyless operation
143
144
  baseUrl: 'https://api.recursiv.io/api/v1', // default
144
145
  timeout: 30000, // request timeout in ms
145
146
  maxRetries: 2, // auto-retry on 429/5xx
@@ -148,9 +149,12 @@ const r = new Recursiv({
148
149
  // Zero-arg works when RECURSIV_API_KEY is set
149
150
  const r = new Recursiv()
150
151
 
151
- // Anonymous sandbox (no API key needed)
152
+ // Anonymous sandbox (no API key required; inherits RECURSIV_API_KEY when set)
152
153
  const r = new Recursiv({ anonymous: true })
153
154
 
155
+ // Guaranteed keyless public client, even inside a credentialed server process
156
+ const visitor = new Recursiv({ anonymous: true, inheritEnvironmentApiKey: false })
157
+
154
158
  // Self-hosted
155
159
  const r = new Recursiv({ baseUrl: 'https://my-instance.com/api/v1' })
156
160
  ```
@@ -613,7 +617,10 @@ state and obtain a fresh review before a new write.
613
617
  Visitors can read a public conversation in an explicitly configured app without signing in:
614
618
 
615
619
  ```ts
616
- const visitor = new Recursiv({ anonymous: true });
620
+ const visitor = new Recursiv({
621
+ anonymous: true,
622
+ inheritEnvironmentApiKey: false,
623
+ });
617
624
  const { data: networks } = await visitor.protocols.sharedNetworks(projectId);
618
625
  const { data: conversation } = await visitor.protocols.getSharedConversation(projectId, {
619
626
  protocol: 'nostr', post: publicEventId,
@@ -643,6 +650,20 @@ module specs and native or protocol post objects. Preserve their existing source
643
650
  authority and interactive controls. `retryFailed()` explicitly retries unavailable
644
651
  sources; create a new session to refresh exhausted source windows.
645
652
 
653
+ `sdk.discovery.createPersonalFeed({ networks, category?, learning?, recommend? })`
654
+ builds the same kind of session for a member's For you tab: its Minds slot reads
655
+ `sdk.curator.forYou({ surface: 'discover', discovery: true, cursor })`, which ranks
656
+ fresher and past the member's follows, pages a stored ranking by `meta.next_cursor`,
657
+ and says whether the member is personalized yet (`meta.personalized`). With a
658
+ `category` (the member's first), the session also reads open-network posts from it;
659
+ without, it is Minds only. It never carries the radar.
660
+
661
+ `sdk.posts.categories()` answers the app's categories in the order this member
662
+ should see them. `meta.order` is `personal`, `popular` or `static`; `meta.reason`
663
+ says why an answer is not personal (`low_signal`, `deadline`, `requested`,
664
+ `experiment_off`), and `meta.order_id` changes only when the order does. The order
665
+ is private to the member; show it as returned rather than sorting it again.
666
+
646
667
  Optional `DiscoveryLearning` consumes real visible impressions and explicit
647
668
  engagement/hide outcomes. Persist its count-only snapshot per viewer; never share
648
669
  a session or learning state across accounts. It starts conservatively and does
package/dist/client.js CHANGED
@@ -11,7 +11,9 @@ export class HttpClient {
11
11
  clientName;
12
12
  constructor(config) {
13
13
  const apiKey = config?.apiKey ||
14
- (typeof process !== 'undefined' ? process.env?.RECURSIV_API_KEY : undefined);
14
+ (config?.inheritEnvironmentApiKey !== false && typeof process !== 'undefined'
15
+ ? process.env?.RECURSIV_API_KEY
16
+ : undefined);
15
17
  this.anonymous = config?.anonymous ?? false;
16
18
  if (!apiKey && !this.anonymous && !config?.allowNoKey) {
17
19
  throw new RecursivError(0, {
package/dist/index.d.ts CHANGED
@@ -143,6 +143,7 @@ export type { RealtimeConnectOptions } from './realtime.js';
143
143
  export { AuthResource } from './resources/auth.js';
144
144
  export type { AuthSession, SignUpInput, SignInInput, UsernameSignInInput, AppContext, ResetPasswordInput, TwoFactorConfirmation, TwoFactorEnrollment, TwoFactorChallenge, CompleteTwoFactorSignInInput, OAuthClientInfo, SendOtpInput, VerifyOtpInput, CreateApiKeyInput, ApiKeyResult, } from './resources/auth.js';
145
145
  export { PostsResource } from './resources/posts.js';
146
+ export type { CategoryOrder, CategoryOrderMeta, CategoryOrderReason } from './resources/posts.js';
146
147
  export { UsersResource } from './resources/users.js';
147
148
  export { CommunitiesResource } from './resources/communities.js';
148
149
  export { ChatResource } from './resources/chat.js';
@@ -182,8 +183,8 @@ export { CuratorResource } from './resources/curator.js';
182
183
  export { VideoResource } from './resources/video.js';
183
184
  export type { VideoUploadCreds, CreateVideoUploadResult, VideoStatusInfo, VideoPlaybackToken, VideoEntitlement, } from './resources/video.js';
184
185
  export { AppSubscriptionsResource } from './resources/app-subscriptions.js';
185
- export type { AppSubscriptionCheckoutResult, AppSubscriptionPortalResult, AppSubscriptionStatusResult, AppSubscriptionTierChangePreview, AppSubscriptionTierChangeResult, } from './resources/app-subscriptions.js';
186
- export type { CuratorSource, CuratorPrompt, CuratorRunInput, CuratorRunResult, CuratorTodayEdition, CuratorTodayExcerpt, CuratorTodayLeadImage, CuratorTodayRadar, CuratorTodayReaction, CuratorTodayResponse, CuratorTodaySource, CuratorTodayStory, } from './resources/curator.js';
186
+ export type { AppSubscriptionBoostCredit, AppSubscriptionCheckoutResult, AppSubscriptionPortalResult, AppSubscriptionStatusResult, AppSubscriptionTierChangePreview, AppSubscriptionTierChangeResult, } from './resources/app-subscriptions.js';
187
+ export type { CuratorSource, CuratorPrompt, CuratorRunInput, CuratorRunResult, CuratorTodayEdition, CuratorTodayExcerpt, CuratorTodayLeadImage, CuratorTodayRadar, CuratorTodayReaction, CuratorTodayResponse, CuratorTodaySource, CuratorTodayStory, ForYouMeta, ForYouParams, ForYouSurface, } from './resources/curator.js';
187
188
  export { ReportsResource } from './resources/reports.js';
188
189
  export type { ReportTargetType, ReportStatus, CreateReportInput, ReportRecord, ResolveReportInput, } from './resources/reports.js';
189
190
  export { ModerationResource } from './resources/moderation.js';
@@ -201,7 +202,7 @@ export type { UploadMediaInput, UploadUrlInput, UploadUrlResult } from './resour
201
202
  export { WalletResource } from './resources/wallet.js';
202
203
  export type { LinkedTokenWallet, PayoutState, TokenBalance, TokenLedgerEntry } from './resources/wallet.js';
203
204
  export { BoostsResource } from './resources/boosts.js';
204
- export type { BoostBalance, BoostBalanceResponse, BoostCampaign, BoostCampaignResponse, BoostEventResult, BoostLedgerEntry, BoostLedgerKind, BoostRejectionReason, BoostReviewStatus, BoostStatus, BoostTopupCheckout, BoostTopupConfirmation, BoostTopupLimits, } from './resources/boosts.js';
205
+ export type { BoostBalance, BoostBalanceResponse, BoostCampaign, BoostCampaignResponse, BoostEventResult, BoostLedgerEntry, BoostLedgerKind, BoostPlanCredit, BoostRejectionReason, BoostReviewStatus, BoostStatus, BoostTopupCheckout, BoostTopupConfirmation, BoostTopupLimits, } from './resources/boosts.js';
205
206
  export { ProtocolsResource } from './resources/protocols.js';
206
207
  export { PROTOCOL_ADAPTER_MATURITIES, PROTOCOL_CAPABILITY_OPERATIONS, PROTOCOL_CAPABILITY_STATUSES, } from './resources/protocols.js';
207
208
  export type { SocialProfile, SocialSubscription, SocialSubscriptionChange, SocialProfilePage, SocialRadar, SocialRadarCluster, SocialRadarMember, SocialRadarParams, SocialRadarPosts, SocialRadarStory, SocialNetwork, SocialPost, SocialConversation, SocialTrend, SocialTrendProvenance, SocialTrends, SocialTrendSourceIndex, SocialTrendFeed, SocialPopularProfiles, SocialAuthorFeed, SocialFollowingFeed, SocialFollowingSource, SearchPublicProfilesParams, SearchPublicPostsParams, SocialPostSearch, HostedProtocolConnections, HostedProtocol, HostedProtocolIdentity, HostedNostrChallenge, HostedNostrIdentityProof, NativeNostrPrepareInput, NativeNostrPrepared, NativeNostrAccount, NativeNostrPostState, NativeNostrReceipt, NativeSocialProtocol, FederationIdentity, FederationIdentityView, NativeSocialAccount, NativeSocialCallbackInput, NativeReplyPrepareInput, NativePostPrepareInput, NativeMediaInput, NativeMediaItem, NativePostBatch, NativePostHistory, NativeDelegatedPostHistoryItem, NativeNostrPostHistoryItem, NativeFollowSourceInput, NativeFollowPrepareInput, NativeFollowTarget, NativeFollowReceipt, NativeFollowState, NativeFollowAccount, NativeFollowStates, NativeFollowReview, NativeLikeSourceInput, NativeRepostSourceInput, NativeLikePrepareInput, NativeRepostPrepareInput, NativeLikeTarget, NativeRepostTarget, NativeLikeReceipt, NativeRepostReceipt, NativeLikeState, NativeRepostState, NativeLikeReview, NativeRepostReview, NativeReplyReceipt, NativeReplyBatch, NativeReplyHistory, NativeDelegatedReplyHistoryItem, NativeNostrReplyResult, NativeNostrReplyHistoryItem, NativeReplyRemovalReceipt, NativeReplyRemovalPreview, SocialSourceRef, SocialSourceControl, SocialSourceControlDisplay, SocialSourceControlChange, HostedPostPreview, ConnectHostedIdentityInput, CreateHostedIdentityInput, CreatedHostedProtocolIdentity, PreviewHostedPostInput, PublishHostedPostInput, ListProtocolCandidatesParams, ProtocolAdapterMaturity, ProtocolAdapterMetadata, ProtocolCandidateListResponse, ProtocolCandidatePageMeta, ProtocolCapabilityOperation, ProtocolCapabilityStatus, ProtocolCollectionStatus, ProtocolIngestionReplayResult, ProtocolIngestionStatus, ProtocolQuarantinedCandidate, ProtocolRefreshResult, ProtocolSettings, ProtocolSettingsUpdateResult, ReplayProtocolIngestionJobInput, UpdateSettingsInput as UpdateProtocolSettingsInput, } from './resources/protocols.js';
package/dist/index.js CHANGED
@@ -181,7 +181,7 @@ export class Recursiv {
181
181
  this.webhooks = new WebhooksResource(client);
182
182
  this.jobs = new JobsResource(client);
183
183
  this.curator = new CuratorResource(client);
184
- this.discovery = new DiscoveryResource(this.posts, this.protocols);
184
+ this.discovery = new DiscoveryResource(this.posts, this.protocols, this.curator);
185
185
  this.video = new VideoResource(client);
186
186
  this.appSubscriptions = new AppSubscriptionsResource(client);
187
187
  this.boosts = new BoostsResource(client);
@@ -158,12 +158,25 @@ export interface InviteSystemStats {
158
158
  used: number;
159
159
  expired: number;
160
160
  revoked: number;
161
+ /** @deprecated Use `total`. */
162
+ total_codes: number;
163
+ /** @deprecated Use `active`. */
164
+ active_codes: number;
165
+ /** @deprecated Use `used`. */
166
+ used_codes: number;
167
+ /** @deprecated Use `expired`. */
168
+ expired_codes: number;
169
+ /** @deprecated Use `revoked`. */
170
+ revoked_codes: number;
161
171
  };
162
172
  waitlist: Record<string, number>;
163
173
  current_wave: {
164
174
  id: string;
165
175
  wave: number;
166
176
  name: string;
177
+ max_codes: number;
178
+ invites_per_user: number;
179
+ reward_tokens: number;
167
180
  is_active: boolean;
168
181
  } | null;
169
182
  }
@@ -65,7 +65,7 @@ export interface SalesLead {
65
65
  * Validation happens server-side too; this list lets apps validate or
66
66
  * surface choices client-side without round-tripping the API.
67
67
  */
68
- export declare const SUPPORTED_MODELS: readonly ["anthropic/claude-fable-5", "anthropic/claude-opus-4.6", "anthropic/claude-opus-4.8", "anthropic/claude-sonnet-4.6", "anthropic/claude-sonnet-5", "deepseek/deepseek-v3.2", "deepseek/deepseek-v4-pro", "google/gemini-3-flash-preview", "google/gemini-3.1-pro-preview", "google/gemini-3.5-flash", "minimax/minimax-m2.5", "minimax/minimax-m3", "moonshotai/kimi-k3", "openai/gpt-4o-mini", "openai/gpt-5.5", "openai/gpt-5.6-sol", "openai/gpt-5.6-terra", "x-ai/grok-4.3"];
68
+ export declare const SUPPORTED_MODELS: readonly ["anthropic/claude-fable-5", "anthropic/claude-fable-5.1", "anthropic/claude-opus-4.6", "anthropic/claude-opus-4.8", "anthropic/claude-opus-5", "anthropic/claude-opus-5.5", "anthropic/claude-sonnet-4.6", "anthropic/claude-sonnet-5", "deepseek/deepseek-v3.2", "deepseek/deepseek-v4-pro", "google/gemini-3-flash-preview", "google/gemini-3.1-pro-preview", "google/gemini-3.5-flash", "google/gemini-3.8-flash", "minimax/minimax-m2.5", "minimax/minimax-m3", "moonshotai/kimi-k3", "openai/gpt-4o-mini", "openai/gpt-5.5", "openai/gpt-5.6-sol", "openai/gpt-5.6-terra", "openai/gpt-6-astra", "openai/gpt-6-sol", "x-ai/grok-4.3"];
69
69
  export type SupportedModel = (typeof SUPPORTED_MODELS)[number];
70
70
  export interface EnsurePersonalAgentOverrides {
71
71
  /** Override the agent's display name. Defaults to 'Agent'. */
@@ -74,6 +74,17 @@ export interface EnsurePersonalAgentOverrides {
74
74
  model?: string;
75
75
  /** Override the system prompt. Apps like Minds can set their own voice. */
76
76
  system_prompt?: string;
77
+ /**
78
+ * Whether the agent may post publicly (reply to @mentions). The platform
79
+ * default for a new personal agent is 'chat_post'; pass 'chat_only' to keep
80
+ * it to private chat.
81
+ */
82
+ social_mode?: 'chat_only' | 'chat_post';
83
+ /**
84
+ * How the agent uses tools. The platform default for a new personal agent is
85
+ * 'autonomous'; 'permission' asks the owner before a tool acts.
86
+ */
87
+ tool_mode?: 'chat_only' | 'permission' | 'autonomous';
77
88
  }
78
89
  export interface EnsurePersonalAgentInput {
79
90
  /**
@@ -105,7 +116,7 @@ export declare class AgentsResource {
105
116
  }): Promise<PaginatedResponse<Agent>>;
106
117
  /** Get an agent by ID */
107
118
  get(id: string): Promise<SingleResponse<AgentDetail>>;
108
- /** Create a new AI agent */
119
+ /** Create a new AI agent. Omit username for a server-generated collision-resistant handle. */
109
120
  create(input: CreateAgentInput): Promise<SingleResponse<CreatedAgent>>;
110
121
  /** Update an agent's configuration */
111
122
  update(id: string, input: UpdateAgentInput): Promise<SingleResponse<UpdatedAgent>>;
@@ -12,8 +12,11 @@ import { parseSSE } from '../lib/sse.js';
12
12
  export const SUPPORTED_MODELS = [
13
13
  // <<< GENERATED: openrouter-sync — do not edit by hand (npm run models:sync)
14
14
  'anthropic/claude-fable-5',
15
+ 'anthropic/claude-fable-5.1',
15
16
  'anthropic/claude-opus-4.6',
16
17
  'anthropic/claude-opus-4.8',
18
+ 'anthropic/claude-opus-5',
19
+ 'anthropic/claude-opus-5.5',
17
20
  'anthropic/claude-sonnet-4.6',
18
21
  'anthropic/claude-sonnet-5',
19
22
  'deepseek/deepseek-v3.2',
@@ -21,6 +24,7 @@ export const SUPPORTED_MODELS = [
21
24
  'google/gemini-3-flash-preview',
22
25
  'google/gemini-3.1-pro-preview',
23
26
  'google/gemini-3.5-flash',
27
+ 'google/gemini-3.8-flash',
24
28
  'minimax/minimax-m2.5',
25
29
  'minimax/minimax-m3',
26
30
  'moonshotai/kimi-k3',
@@ -28,6 +32,8 @@ export const SUPPORTED_MODELS = [
28
32
  'openai/gpt-5.5',
29
33
  'openai/gpt-5.6-sol',
30
34
  'openai/gpt-5.6-terra',
35
+ 'openai/gpt-6-astra',
36
+ 'openai/gpt-6-sol',
31
37
  'x-ai/grok-4.3',
32
38
  // >>> GENERATED
33
39
  ];
@@ -51,7 +57,7 @@ export class AgentsResource {
51
57
  get(id) {
52
58
  return this.client.get(`/agents/${id}`);
53
59
  }
54
- /** Create a new AI agent */
60
+ /** Create a new AI agent. Omit username for a server-generated collision-resistant handle. */
55
61
  create(input) {
56
62
  return this.client.post('/agents', input);
57
63
  }
@@ -6,6 +6,30 @@ export interface AppSubscriptionCheckoutResult {
6
6
  export interface AppSubscriptionPortalResult {
7
7
  /** Stripe Billing Portal URL for the current app user. */
8
8
  url: string;
9
+ /**
10
+ * The page the portal opens on: `cancel` for the plan's cancel confirmation, null for the
11
+ * portal home. A plan already set to end opens the home, where the member can renew it.
12
+ * Absent from older servers.
13
+ */
14
+ flow?: 'cancel' | null;
15
+ }
16
+ /** The boost credit a paid plan on this app adds to the member's boost balance. */
17
+ export interface AppSubscriptionBoostCredit {
18
+ /** Micro-USD (1 USD = 1,000,000) each paid billing period of a plan adds, by tier key (e.g. `plus`, `pro`). */
19
+ perPeriodMicros: Record<string, number>;
20
+ /** What the member's current plan adds each paid period; 0 without a paid plan here. */
21
+ currentMicros: number;
22
+ /**
23
+ * When the next paid period starts and adds `currentMicros`. Null without a plan that adds
24
+ * credit, or while a payment is overdue. A plan set to end at the period's end still shows it.
25
+ */
26
+ nextGrantAt: string | null;
27
+ /**
28
+ * Plan credit the member owes: taken back after their plan dropped or ended inside a period, but
29
+ * already spent. The next plan credit repays it before adding anything. 0 when nothing is owed
30
+ * (absent from older servers).
31
+ */
32
+ owedMicros?: number;
9
33
  }
10
34
  export interface AppSubscriptionStatusResult {
11
35
  tier: string;
@@ -15,6 +39,11 @@ export interface AppSubscriptionStatusResult {
15
39
  currentPeriodEnd: string | null;
16
40
  /** Where the entitlement comes from: a Stripe subscription here, or the legacy import. */
17
41
  source?: 'stripe' | 'legacy';
42
+ /**
43
+ * The boost credit each paid period of a plan adds (once per period, from the payment that
44
+ * starts it). Null when this app's plans include none; absent from older servers.
45
+ */
46
+ boostCredit?: AppSubscriptionBoostCredit | null;
18
47
  }
19
48
  export interface AppSubscriptionTierChangePreview {
20
49
  /** The plan the preview prices. */
@@ -47,6 +76,13 @@ export declare class AppSubscriptionsResource {
47
76
  * Start a Stripe Checkout for the current user to subscribe to a plan tier ('plus' | 'pro').
48
77
  * Answers 409 `subscription_exists` when the member already has a subscription:
49
78
  * use `changeTier` for them instead, so they are never billed twice.
79
+ *
80
+ * Only one checkout can be paid at a time. Repeating the same request (same tier and
81
+ * `return_url`) returns the checkout already open; any other request expires the
82
+ * member's earlier open checkouts for this app. Answers 503 `checkout_busy` when
83
+ * Stripe is too slow to finish safely: retry. `return_url` keeps its own query and fragment;
84
+ * `sub=success&tier=<tier>` or `sub=cancelled` is added to its query (to the
85
+ * fragment's query for a hash route such as `#/billing`).
50
86
  */
51
87
  checkout(input: {
52
88
  tier: string;
@@ -79,13 +115,23 @@ export declare class AppSubscriptionsResource {
79
115
  }): Promise<{
80
116
  data: AppSubscriptionTierChangeResult;
81
117
  }>;
82
- /** Open the current app user's Stripe Billing Portal for this project. */
118
+ /**
119
+ * Open the current app user's Stripe Billing Portal for this project.
120
+ *
121
+ * `flow: 'cancel'` opens it straight on the cancel confirmation for the member's plan, and
122
+ * returns to `return_url` once they confirm. Answers 409 `no_subscription` when there is no
123
+ * plan to cancel, and 409 `legacy_membership` for a carried-over member with nothing to manage.
124
+ */
83
125
  createPortalSession(input: {
84
126
  return_url: string;
127
+ flow?: 'cancel';
85
128
  }): Promise<{
86
129
  data: AppSubscriptionPortalResult;
87
130
  }>;
88
- /** Read the current user's app-subscription status for this project-scoped key. */
131
+ /**
132
+ * Read the current user's app-subscription status for this project-scoped key, with the
133
+ * boost credit the app's plans include (`boostCredit`).
134
+ */
89
135
  status(): Promise<{
90
136
  data: AppSubscriptionStatusResult;
91
137
  }>;
@@ -11,6 +11,13 @@ export class AppSubscriptionsResource {
11
11
  * Start a Stripe Checkout for the current user to subscribe to a plan tier ('plus' | 'pro').
12
12
  * Answers 409 `subscription_exists` when the member already has a subscription:
13
13
  * use `changeTier` for them instead, so they are never billed twice.
14
+ *
15
+ * Only one checkout can be paid at a time. Repeating the same request (same tier and
16
+ * `return_url`) returns the checkout already open; any other request expires the
17
+ * member's earlier open checkouts for this app. Answers 503 `checkout_busy` when
18
+ * Stripe is too slow to finish safely: retry. `return_url` keeps its own query and fragment;
19
+ * `sub=success&tier=<tier>` or `sub=cancelled` is added to its query (to the
20
+ * fragment's query for a hash route such as `#/billing`).
14
21
  */
15
22
  checkout(input) {
16
23
  return this.client.post('/app-subscriptions/checkout', input);
@@ -35,11 +42,20 @@ export class AppSubscriptionsResource {
35
42
  changeTier(input) {
36
43
  return this.client.post('/app-subscriptions/change-tier', input, { retry: false });
37
44
  }
38
- /** Open the current app user's Stripe Billing Portal for this project. */
45
+ /**
46
+ * Open the current app user's Stripe Billing Portal for this project.
47
+ *
48
+ * `flow: 'cancel'` opens it straight on the cancel confirmation for the member's plan, and
49
+ * returns to `return_url` once they confirm. Answers 409 `no_subscription` when there is no
50
+ * plan to cancel, and 409 `legacy_membership` for a carried-over member with nothing to manage.
51
+ */
39
52
  createPortalSession(input) {
40
53
  return this.client.post('/app-subscriptions/portal', input);
41
54
  }
42
- /** Read the current user's app-subscription status for this project-scoped key. */
55
+ /**
56
+ * Read the current user's app-subscription status for this project-scoped key, with the
57
+ * boost credit the app's plans include (`boostCredit`).
58
+ */
43
59
  status() {
44
60
  return this.client.get('/app-subscriptions/status');
45
61
  }
@@ -44,10 +44,27 @@ export interface BoostBalance {
44
44
  /** The 50 most recent balance movements, newest first. */
45
45
  ledger: BoostLedgerEntry[];
46
46
  }
47
+ /** The boost credit the key's app includes with its paid plans. */
48
+ export interface BoostPlanCredit {
49
+ /** Micro-USD each paid billing period of a plan adds, by tier key (e.g. `plus`, `pro`). */
50
+ per_period_micros: Record<string, number>;
51
+ /** What your current plan on this app adds each paid period; 0 without one. */
52
+ current_micros: number;
53
+ /** When your next paid period starts and adds `current_micros`; null when none is due. */
54
+ next_grant_at: string | null;
55
+ /**
56
+ * Plan credit you owe: taken back after your plan dropped or ended inside a period, but already
57
+ * spent. Your next plan credit repays it before adding anything. 0 when nothing is owed
58
+ * (absent from older servers).
59
+ */
60
+ owed_micros?: number;
61
+ }
47
62
  export interface BoostBalanceResponse extends SingleResponse<BoostBalance> {
63
+ /** `plan_credit` is null when the app's plans include no boost credit (absent from older servers). */
48
64
  meta: {
49
65
  min_cpm_micros: number;
50
66
  topup?: BoostTopupLimits;
67
+ plan_credit?: BoostPlanCredit | null;
51
68
  };
52
69
  }
53
70
  export interface BoostCampaignResponse extends SingleResponse<BoostCampaign> {
@@ -98,7 +115,10 @@ export declare class BoostsResource {
98
115
  limit?: number;
99
116
  offset?: number;
100
117
  }): Promise<PaginatedResponse<BoostCampaign>>;
101
- /** Your balance, recent ledger, the CPM floor, and the top-up limits. */
118
+ /**
119
+ * Your balance, recent ledger, the CPM floor, the top-up limits, and the boost credit your
120
+ * plan on this app adds each paid period (`meta.plan_credit`, a `grant` ledger row per period).
121
+ */
102
122
  balance(): Promise<BoostBalanceResponse>;
103
123
  /** Pause one of your active campaigns. */
104
124
  pause(campaignId: string): Promise<BoostCampaignResponse>;
@@ -112,6 +132,8 @@ export declare class BoostsResource {
112
132
  * Buy boost credit: opens a one-time Stripe Checkout for `amount_cents` (USD) on the app's
113
133
  * own Stripe account. The balance is credited once the payment succeeds. Limits are in
114
134
  * `balance().meta.topup`. Requires a project-scoped key with `billing:write`.
135
+ * `return_url` keeps its own query and fragment; `topup=success&session_id=<id>` or
136
+ * `topup=cancelled` is added to its query (to the fragment's query for a hash route).
115
137
  */
116
138
  topup(input: {
117
139
  amount_cents: number;
@@ -20,7 +20,10 @@ export class BoostsResource {
20
20
  mine(params) {
21
21
  return this.client.get('/boosts/mine', params);
22
22
  }
23
- /** Your balance, recent ledger, the CPM floor, and the top-up limits. */
23
+ /**
24
+ * Your balance, recent ledger, the CPM floor, the top-up limits, and the boost credit your
25
+ * plan on this app adds each paid period (`meta.plan_credit`, a `grant` ledger row per period).
26
+ */
24
27
  balance() {
25
28
  return this.client.get('/boosts/balance');
26
29
  }
@@ -44,6 +47,8 @@ export class BoostsResource {
44
47
  * Buy boost credit: opens a one-time Stripe Checkout for `amount_cents` (USD) on the app's
45
48
  * own Stripe account. The balance is credited once the payment succeeds. Limits are in
46
49
  * `balance().meta.topup`. Requires a project-scoped key with `billing:write`.
50
+ * `return_url` keeps its own query and fragment; `topup=success&session_id=<id>` or
51
+ * `topup=cancelled` is added to its query (to the fragment's query for a hash route).
47
52
  */
48
53
  topup(input) {
49
54
  return this.client.post('/boosts/topup', input, { retry: false });
@@ -25,7 +25,7 @@ export declare class CommunitiesResource {
25
25
  }): Promise<PaginatedResponse<RelatedCommunity>>;
26
26
  /** List members of a community. `q` searches by name or username. */
27
27
  members(id: string, params?: CommunityPeopleParams): Promise<PaginatedResponse<CommunityMember>>;
28
- /** Create a new community */
28
+ /** Create a new community. Omit slug for a server-generated collision-resistant slug. */
29
29
  create(input: CreateCommunityInput): Promise<SingleResponse<CreatedCommunity>>;
30
30
  /** Join a community */
31
31
  join(id: string): Promise<SingleResponse<JoinResult>>;
@@ -33,7 +33,7 @@ export class CommunitiesResource {
33
33
  members(id, params) {
34
34
  return this.client.get(`/communities/${id}/members`, params);
35
35
  }
36
- /** Create a new community */
36
+ /** Create a new community. Omit slug for a server-generated collision-resistant slug. */
37
37
  create(input) {
38
38
  return this.client.post('/communities', input);
39
39
  }
@@ -1,5 +1,23 @@
1
1
  import type { HttpClient } from '../client.js';
2
- import type { PaginatedResponse, Post, SingleResponse } from '../types.js';
2
+ import type { DiscoveryLayout, PaginatedResponse, PaginationMeta, Post, SingleResponse } from '../types.js';
3
+ /** Which surface is asking: Home's For You (default) or Discover's For you. */
4
+ export type ForYouSurface = 'home' | 'discover';
5
+ export type ForYouParams = {
6
+ limit?: number;
7
+ offset?: number;
8
+ /** The previous page's `meta.next_cursor`: pages a stored ranking, so posts read meanwhile never shift the next page. */
9
+ cursor?: string;
10
+ /** Adds `meta.discovery` (layouts) and the public safety floor; limit <= 25. */
11
+ discovery?: boolean;
12
+ /** `discover` ranks for Discover: fresher, beyond the member's follows, with their category order. */
13
+ surface?: ForYouSurface;
14
+ };
15
+ export interface ForYouMeta extends PaginationMeta {
16
+ discovery?: DiscoveryLayout;
17
+ next_cursor?: string;
18
+ /** Whether the ranker can personalize for this member yet (3 follows or 3 upvoted authors). */
19
+ personalized?: boolean;
20
+ }
3
21
  /** RSS / Atom source. Default — `type` may be omitted for back-compat. */
4
22
  export interface CuratorRssSource {
5
23
  type?: 'rss';
@@ -250,10 +268,10 @@ export declare class CuratorResource {
250
268
  * existing posts for the signed-in user (engagement + recency + social +
251
269
  * affinity, no model calls). Returns posts in the same shape as
252
270
  * `sdk.posts.list()`, so any feed UI renders it unchanged. Requires a
253
- * project-scoped key (the feed is per-app).
271
+ * project-scoped key (the feed is per-app). Servers that predate `cursor`,
272
+ * `discovery` and `surface` ignore them.
254
273
  */
255
- forYou(params?: {
256
- limit?: number;
257
- offset?: number;
258
- }): Promise<PaginatedResponse<Post>>;
274
+ forYou(params?: ForYouParams): Promise<PaginatedResponse<Post> & {
275
+ meta: ForYouMeta;
276
+ }>;
259
277
  }
@@ -47,7 +47,8 @@ export class CuratorResource {
47
47
  * existing posts for the signed-in user (engagement + recency + social +
48
48
  * affinity, no model calls). Returns posts in the same shape as
49
49
  * `sdk.posts.list()`, so any feed UI renders it unchanged. Requires a
50
- * project-scoped key (the feed is per-app).
50
+ * project-scoped key (the feed is per-app). Servers that predate `cursor`,
51
+ * `discovery` and `surface` ignore them.
51
52
  */
52
53
  forYou(params = {}) {
53
54
  return this.client.get('/curator/foryou', params);
@@ -1,4 +1,5 @@
1
1
  import type { DiscoveryLayout, Post } from '../types.js';
2
+ import type { CuratorResource } from './curator.js';
2
3
  import type { DiscoveryLearning } from './discovery-learning.js';
3
4
  import type { PostsResource } from './posts.js';
4
5
  import type { ProtocolsResource, SocialNetwork, SocialPost, SocialRadar, SocialRadarPosts } from './protocols.js';
@@ -201,7 +202,20 @@ export declare class DiscoveryFeedSession {
201
202
  export declare class DiscoveryResource {
202
203
  private posts;
203
204
  private protocols;
204
- constructor(posts: PostsResource, protocols: ProtocolsResource);
205
+ private curator?;
206
+ constructor(posts: PostsResource, protocols: ProtocolsResource, curator?: Pick<CuratorResource, "forYou"> | undefined);
207
+ /**
208
+ * A For you Discover feed: the member's For You ranking (asked for Discover's surface, with layouts and a
209
+ * snapshot cursor) in a session's Minds slot. With a category, the session also reads open-network posts
210
+ * from that category (the member's top one); without, it is Minds only. Never the radar. The page-0
211
+ * arrangement and when to use which are the app's choice.
212
+ */
213
+ createPersonalFeed(options: {
214
+ networks: SocialNetwork[];
215
+ learning?: DiscoveryLearning;
216
+ recommend?: DiscoveryRecommendationSources;
217
+ category?: string;
218
+ }): DiscoveryFeedSession;
205
219
  createFeed(options: {
206
220
  networks: SocialNetwork[];
207
221
  category?: string;
@@ -160,6 +160,9 @@ export class DiscoveryFeedSession {
160
160
  return this.pending;
161
161
  }
162
162
  async read(stream) {
163
+ // `discovery` asks only for the layout judgment (and, for Minds posts, the public-only floor). The
164
+ // server's safety floor does not depend on it, so a reader that drops it to skip layout work (a clip
165
+ // pool) is screened exactly like this session.
163
166
  const params = { limit: 25, ...(this.category ? { category: this.category } : {}), discovery: true };
164
167
  if (stream.protocol === 'minds') {
165
168
  // Discover leads with what is moving now: the REST hot order (engagement
@@ -462,9 +465,34 @@ export class DiscoveryFeedSession {
462
465
  export class DiscoveryResource {
463
466
  posts;
464
467
  protocols;
465
- constructor(posts, protocols) {
468
+ curator;
469
+ constructor(posts, protocols, curator) {
466
470
  this.posts = posts;
467
471
  this.protocols = protocols;
472
+ this.curator = curator;
473
+ }
474
+ /**
475
+ * A For you Discover feed: the member's For You ranking (asked for Discover's surface, with layouts and a
476
+ * snapshot cursor) in a session's Minds slot. With a category, the session also reads open-network posts
477
+ * from that category (the member's top one); without, it is Minds only. Never the radar. The page-0
478
+ * arrangement and when to use which are the app's choice.
479
+ */
480
+ createPersonalFeed(options) {
481
+ const curator = this.curator;
482
+ if (!curator)
483
+ throw new Error('createPersonalFeed needs the curator resource');
484
+ const list = {
485
+ // The session's `sort` and `category` are Discover's hot-order knobs; the ranker ignores them.
486
+ list: async (params) => {
487
+ const response = await curator.forYou({ limit: params?.limit, offset: params?.offset, cursor: params?.cursor, discovery: true, surface: 'discover' });
488
+ // A server that predates the cursor says has_more: false on a partial ranked page although its
489
+ // continuation serves the next offset. A cursor-aware server always says `personalized`, and its
490
+ // has_more is the end of the feed.
491
+ const legacy = typeof response.meta.personalized !== 'boolean';
492
+ return { ...response, meta: { ...response.meta, has_more: response.meta.has_more || (legacy && response.data.length > 0) } };
493
+ },
494
+ };
495
+ return new DiscoveryFeedSession(list, this.protocols, options.category ? options.networks : [], options.category, options.category ? 'all' : 'minds', options.learning, options.recommend);
468
496
  }
469
497
  createFeed(options) {
470
498
  // The radar rides along by default; an older server answers 404 and the session simply shows no story module.
@@ -18,12 +18,25 @@ export interface InviteCodeStats {
18
18
  used: number;
19
19
  expired: number;
20
20
  revoked: number;
21
+ /** @deprecated Use `total`. */
22
+ total_codes: number;
23
+ /** @deprecated Use `active`. */
24
+ active_codes: number;
25
+ /** @deprecated Use `used`. */
26
+ used_codes: number;
27
+ /** @deprecated Use `expired`. */
28
+ expired_codes: number;
29
+ /** @deprecated Use `revoked`. */
30
+ revoked_codes: number;
21
31
  };
22
32
  waitlist: Record<string, number>;
23
33
  current_wave: {
24
34
  id: string;
25
35
  wave: number;
26
36
  name: string;
37
+ max_codes: number;
38
+ invites_per_user: number;
39
+ reward_tokens: number;
27
40
  is_active: boolean;
28
41
  } | null;
29
42
  }
@@ -90,10 +90,20 @@ export interface BulkGenerateResult {
90
90
  }
91
91
  export interface InviteStats {
92
92
  codes: {
93
+ total: number;
94
+ active: number;
95
+ used: number;
96
+ expired: number;
97
+ revoked: number;
98
+ /** @deprecated Use `total`. Retained for legacy admin clients. */
93
99
  total_codes: number;
100
+ /** @deprecated Use `active`. Retained for legacy admin clients. */
94
101
  active_codes: number;
102
+ /** @deprecated Use `used`. Retained for legacy admin clients. */
95
103
  used_codes: number;
104
+ /** @deprecated Use `expired`. Retained for legacy admin clients. */
96
105
  expired_codes: number;
106
+ /** @deprecated Use `revoked`. Retained for legacy admin clients. */
97
107
  revoked_codes: number;
98
108
  };
99
109
  waitlist: Record<string, number>;
@@ -43,26 +43,26 @@ export class InviteCodesResource {
43
43
  // ── Admin Methods ──
44
44
  /** Admin: Initialize wave configs for the network */
45
45
  initializeWaves() {
46
- return this.client.post('/invite-codes/admin/initialize-waves');
46
+ return this.client.post('/invite-codes-admin/waves/initialize');
47
47
  }
48
48
  /** Admin: Get all wave configs */
49
49
  getWaves() {
50
- return this.client.get('/invite-codes/admin/waves');
50
+ return this.client.get('/invite-codes-admin/waves');
51
51
  }
52
52
  /** Admin: Activate a specific wave */
53
53
  activateWave(wave) {
54
- return this.client.post('/invite-codes/admin/activate-wave', { wave });
54
+ return this.client.post('/invite-codes-admin/waves/activate', { wave });
55
55
  }
56
56
  /** Admin: Bulk generate invite codes */
57
57
  bulkGenerate(input) {
58
- return this.client.post('/invite-codes/admin/bulk-generate', input);
58
+ return this.client.post('/invite-codes-admin/bulk-generate', input);
59
59
  }
60
60
  /** Admin: Get invite system stats */
61
61
  getStats() {
62
- return this.client.get('/invite-codes/admin/stats');
62
+ return this.client.get('/invite-codes-admin/stats');
63
63
  }
64
64
  /** Admin: Get waitlist entries */
65
65
  getWaitlist(params) {
66
- return this.client.get('/invite-codes/admin/waitlist', params);
66
+ return this.client.get('/invite-codes-admin/waitlist', params);
67
67
  }
68
68
  }
@@ -1,15 +1,36 @@
1
1
  import type { HttpClient } from '../client.js';
2
2
  import type { PaginatedResponse, SingleResponse, DeleteResponse, Post, PostDetail, CreatePostInput, CreatedPost, UpdatePostInput, ListPostsParams, ListPostRepliesParams, PostRepliesPage, SearchPostsParams, ReactionType } from '../types.js';
3
+ /** How GET /posts/categories ordered its answer: the member's own order, the app's popularity order, or the fixed order. */
4
+ export type CategoryOrder = 'personal' | 'popular' | 'static';
5
+ /** Why an answer is not `personal`. */
6
+ export type CategoryOrderReason = 'low_signal' | 'deadline' | 'requested' | 'experiment_off';
7
+ export interface CategoryOrderMeta {
8
+ order: CategoryOrder;
9
+ /** 16 hex; equal ids mean the same order. */
10
+ order_id: string;
11
+ as_of: string;
12
+ reason?: CategoryOrderReason;
13
+ /** Whether For You can personalize for this member yet; absent when the member's order was not read. */
14
+ personalized?: boolean;
15
+ }
3
16
  export declare class PostsResource {
4
17
  private client;
5
18
  constructor(client: HttpClient);
6
19
  /** List posts with optional filters */
7
20
  list(params?: ListPostsParams): Promise<PaginatedResponse<Post>>;
8
- /** Broad categories currently enabled for public posts in this app. */
9
- categories(): Promise<SingleResponse<Array<{
21
+ /**
22
+ * Broad categories currently enabled for public posts in this app, in the order this member should see
23
+ * them: their own order once the app serves personal orders, otherwise the app's popularity order. The
24
+ * order is private to the member. `order` asks for a specific one (default `personal`).
25
+ */
26
+ categories(params?: {
27
+ order?: CategoryOrder;
28
+ }): Promise<SingleResponse<Array<{
10
29
  id: string;
11
30
  name: string;
12
- }>>>;
31
+ }>> & {
32
+ meta?: CategoryOrderMeta;
33
+ }>;
13
34
  /** List posts the authenticated caller has upvoted. */
14
35
  liked(params?: Pick<ListPostsParams, 'limit' | 'offset' | 'cursor'>): Promise<PaginatedResponse<Post>>;
15
36
  /** Posts similar to this one (embedding neighbours), in the app's scope. */
@@ -7,9 +7,14 @@ export class PostsResource {
7
7
  list(params) {
8
8
  return this.client.get('/posts', params);
9
9
  }
10
- /** Broad categories currently enabled for public posts in this app. */
11
- categories() {
12
- return this.client.get('/posts/categories');
10
+ /**
11
+ * Broad categories currently enabled for public posts in this app, in the order this member should see
12
+ * them: their own order once the app serves personal orders, otherwise the app's popularity order. The
13
+ * order is private to the member. `order` asks for a specific one (default `personal`).
14
+ */
15
+ categories(params) {
16
+ // No params, no query: the same request older SDKs make.
17
+ return params ? this.client.get('/posts/categories', params) : this.client.get('/posts/categories');
13
18
  }
14
19
  /** List posts the authenticated caller has upvoted. */
15
20
  liked(params) {
@@ -1255,7 +1255,10 @@ export declare class ProtocolsResource {
1255
1255
  limit?: number;
1256
1256
  cursor?: string;
1257
1257
  }): Promise<SingleResponse<SocialAuthorFeed>>;
1258
- /** Public, app-moderated discovery without account authority. Enrichment uses cached judgments only. */
1258
+ /**
1259
+ * Public, app-moderated discovery without account authority. Enrichment uses cached judgments only.
1260
+ * Every page, previews included, gets the safety floor; `discovery` only asks for a layout.
1261
+ */
1259
1262
  getSharedTrends(projectId: string, params: {
1260
1263
  protocol: string;
1261
1264
  limit?: number;
@@ -1264,7 +1267,7 @@ export declare class ProtocolsResource {
1264
1267
  category?: string;
1265
1268
  discovery?: boolean;
1266
1269
  }): Promise<SingleResponse<SocialTrends>>;
1267
- /** Open a provider-listed topic through the same public-only app boundary. */
1270
+ /** Open a provider-listed topic through the same public-only app boundary, with the same safety floor. */
1268
1271
  getSharedTrendFeed(projectId: string, params: {
1269
1272
  protocol: string;
1270
1273
  topic: string;
@@ -1314,7 +1317,10 @@ export declare class ProtocolsResource {
1314
1317
  post: string;
1315
1318
  readRef?: string;
1316
1319
  }): Promise<SingleResponse<SocialConversation>>;
1317
- /** Current provider-ranked topics or posts, with explicit source and coverage. Does not ingest content. */
1320
+ /**
1321
+ * Current provider-ranked topics or posts, with explicit source and coverage. Does not ingest content.
1322
+ * Every page, previews included, gets the safety floor; `discovery` only asks for a layout.
1323
+ */
1318
1324
  getPublicTrends(params: {
1319
1325
  protocol: string;
1320
1326
  limit?: number;
@@ -1323,7 +1329,7 @@ export declare class ProtocolsResource {
1323
1329
  category?: string;
1324
1330
  discovery?: boolean;
1325
1331
  }, options?: SocialReadOptions): Promise<SingleResponse<SocialTrends>>;
1326
- /** Open a topic currently listed by the provider. Use its nativeId as topic and preserve the opaque cursor. */
1332
+ /** Open a topic currently listed by the provider. Use its nativeId as topic and preserve the opaque cursor. Screened like `getPublicTrends`. */
1327
1333
  getPublicTrendFeed(params: {
1328
1334
  protocol: string;
1329
1335
  topic: string;
@@ -89,12 +89,15 @@ export class ProtocolsResource {
89
89
  return this.client.get(`/protocols/public/${encodeURIComponent(projectId)}/author-feed`, { protocol: params.protocol, actor: params.actor,
90
90
  limit: params.limit, cursor: params.cursor });
91
91
  }
92
- /** Public, app-moderated discovery without account authority. Enrichment uses cached judgments only. */
92
+ /**
93
+ * Public, app-moderated discovery without account authority. Enrichment uses cached judgments only.
94
+ * Every page, previews included, gets the safety floor; `discovery` only asks for a layout.
95
+ */
93
96
  getSharedTrends(projectId, params) {
94
97
  return this.client.get(`/protocols/public/${encodeURIComponent(projectId)}/trends`, { protocol: params.protocol, limit: params.limit, cursor: params.cursor,
95
98
  includePreviews: params.includePreviews, category: params.category, discovery: params.discovery });
96
99
  }
97
- /** Open a provider-listed topic through the same public-only app boundary. */
100
+ /** Open a provider-listed topic through the same public-only app boundary, with the same safety floor. */
98
101
  getSharedTrendFeed(projectId, params) {
99
102
  return this.client.get(`/protocols/public/${encodeURIComponent(projectId)}/trend-feed`, { protocol: params.protocol, topic: params.topic,
100
103
  limit: params.limit, cursor: params.cursor, category: params.category, discovery: params.discovery });
@@ -148,14 +151,17 @@ export class ProtocolsResource {
148
151
  getPublicConversation(params) {
149
152
  return this.client.get('/protocols/social/conversation', { protocol: params.protocol, post: params.post, readRef: params.readRef });
150
153
  }
151
- /** Current provider-ranked topics or posts, with explicit source and coverage. Does not ingest content. */
154
+ /**
155
+ * Current provider-ranked topics or posts, with explicit source and coverage. Does not ingest content.
156
+ * Every page, previews included, gets the safety floor; `discovery` only asks for a layout.
157
+ */
152
158
  getPublicTrends(params, options) {
153
159
  return this.client.get('/protocols/social/trends', { protocol: params.protocol, limit: params.limit, cursor: params.cursor,
154
160
  ...(params.includePreviews !== undefined ? { includePreviews: params.includePreviews } : {}),
155
161
  ...(params.category !== undefined ? { category: params.category } : {}),
156
162
  ...(params.discovery !== undefined ? { discovery: params.discovery } : {}) }, ...readOptions(options));
157
163
  }
158
- /** Open a topic currently listed by the provider. Use its nativeId as topic and preserve the opaque cursor. */
164
+ /** Open a topic currently listed by the provider. Use its nativeId as topic and preserve the opaque cursor. Screened like `getPublicTrends`. */
159
165
  getPublicTrendFeed(params, options) {
160
166
  return this.client.get('/protocols/social/trend-feed', { protocol: params.protocol, topic: params.topic, limit: params.limit, cursor: params.cursor,
161
167
  ...(params.category !== undefined ? { category: params.category } : {}),
package/dist/types.d.ts CHANGED
@@ -226,7 +226,11 @@ export interface UpdatePostInput {
226
226
  }
227
227
  export type ReactionType = 'like' | 'heart' | 'fire' | 'laugh' | 'sad' | 'angry' | 'upvote' | 'downvote';
228
228
  export interface ListPostsParams extends PaginationParams {
229
- /** Public-only feed with a bounded Jev presentation hint. Limit <=25. */
229
+ /**
230
+ * Public-only feed with a bounded Jev presentation hint. Limit <=25.
231
+ * The safety floor does not depend on it: a category page and every app-wide
232
+ * `sort: 'hot'` or `'score'` read are screened with or without a layout.
233
+ */
230
234
  discovery?: boolean;
231
235
  /** Public-only broad category. Requires an enabled app experiment; limit <=25. */
232
236
  category?: string;
@@ -366,7 +370,8 @@ export interface CommunityBannedMember {
366
370
  }
367
371
  export interface CreateCommunityInput {
368
372
  name: string;
369
- slug: string;
373
+ /** Optional vanity slug. Omit it for a server-generated collision-resistant default. */
374
+ slug?: string;
370
375
  description?: string;
371
376
  privacy?: 'public' | 'private' | 'hidden';
372
377
  }
@@ -719,7 +724,8 @@ export interface AgentDetail extends Agent {
719
724
  }
720
725
  export interface CreateAgentInput {
721
726
  name: string;
722
- username: string;
727
+ /** Optional public handle. Omit it for a server-generated collision-resistant default. */
728
+ username?: string;
723
729
  bio?: string;
724
730
  model?: string;
725
731
  system_prompt?: string;
@@ -1082,6 +1088,13 @@ export interface LoginHistoryEntry {
1082
1088
  }
1083
1089
  export interface RecursivConfig {
1084
1090
  apiKey?: string;
1091
+ /**
1092
+ * Whether to fall back to `RECURSIV_API_KEY` when `apiKey` is omitted.
1093
+ * Defaults to true for backwards compatibility and branded pre-auth flows.
1094
+ * Set to false for public clients that must remain keyless even in a
1095
+ * credentialed server process.
1096
+ */
1097
+ inheritEnvironmentApiKey?: boolean;
1085
1098
  baseUrl?: string;
1086
1099
  timeout?: number;
1087
1100
  maxRetries?: number;
package/llm.md CHANGED
@@ -131,7 +131,7 @@ const { data } = await r.sandbox.execute({ code: '1 + 1', language: 'typescript'
131
131
  ```typescript
132
132
  const { data: agent } = await r.agents.create({
133
133
  name: 'Home Assistant',
134
- username: 'home_bot_' + Date.now(),
134
+ // username is optional; the server generates a collision-resistant handle
135
135
  model: 'anthropic/claude-sonnet-4.6', // see SUPPORTED_MODELS
136
136
  system_prompt: 'You help homeowners maintain their property.',
137
137
  project_id: projectId, // app-customer flow; works with per-user keys. Use organization_id only for team-owned agents.
@@ -647,7 +647,7 @@ Methods: `list`, `get`, `create`, `delete`
647
647
 
648
648
  - `r.settings` — User preferences, sessions, password changes, account deletion. `revokeOtherSessions({ keep_session_token? })` signs out every other device and keeps this key (and the proven session); `revokeAllSessions()` also revokes this key. Accounts with no password confirm `requestEmailChange({ new_email, email_code })` with a code from `requestConfirmationCode('email_change')`. With a project-bound key, `getPreferences()` includes app-scoped `content`; persist changes with `updateContent({ show_nsfw?, autoplay_video?, ai_enabled?, default_feed? })`.
649
649
  - `r.billing` — Usage tracking, checkout sessions, portal sessions, metered billing
650
- - `r.appSubscriptions` — Project-scoped consumer plans: `checkout` (409 `subscription_exists` when the member already pays), `status`, `createPortalSession`, `previewTierChange({ tier })`, `changeTier({ tier, proration_date? })` (in-place, prorated, never a second subscription)
650
+ - `r.appSubscriptions` — Project-scoped consumer plans: `checkout` (409 `subscription_exists` when the member already pays), `status`, `createPortalSession({ return_url, flow?: 'cancel' })` (`flow: 'cancel'` opens Stripe's cancel confirmation; 409 `no_subscription` when there is no plan to cancel), `previewTierChange({ tier })`, `changeTier({ tier, proration_date? })` (in-place, prorated, never a second subscription)
651
651
  - `r.boosts` — Promoted posts (micro-USD): `create`, `mine`, `balance`, `pause`, `resume`, `cancel`, `recordEvent`; buy credit with `topup({ amount_cents, return_url })` then `confirmTopup(sessionId)` (credits once); admin `listPendingReview`, `review`, `grant`
652
652
  - `r.notifications` — The notification stream: `list({ category: 'social' })` (rows carry `actor`, `reaction_type` and a `post_preview` with the viewer's `user_reaction`/`reposted_by_viewer_id` and counts), `unreadCount({ category })` for badges (`const { data } = await r.notifications.unreadCount({ category: 'social' })`), `markAsRead(id)`, `archive(id)` to dismiss, `markAllAsRead(category)` or `markAllAsRead({ category, organization_id })`, plus push token `registerToken`/`unregisterToken`. A push's `data.notificationId` is the row to mark read on tap. DMs are not in the stream.
653
653
  - `r.uploads` — Legacy media upload URLs (prefer `r.storage` for new projects)
@@ -676,10 +676,10 @@ await r.appSubscriptions.changeTier({ tier: 'pro', proration_date: quote.prorati
676
676
  - Native follows: `nativeFollowState({ accountId, protocol, nativeId })`, `prepareNativeFollow({ clientMutationId, accountId, protocol, nativeId, action: "follow" | "unfollow" })`, `submitNativeFollow(id, { approve: true, event? })`, `nativeFollowStatus(id)`. Show the acting identity and native following/requested/unfollowed state separately from the Minds feed subscription. Nostr must sign the complete review event without dropping other contacts; stale lists require a fresh review. Keep mutation IDs and reconcile ambiguous outcomes without replay. Requires current follow scopes and API `NATIVE_SOCIAL_FOLLOW_PROJECTS` plus source controls.
677
677
  - Native reposts: `nativeRepostState({ accountId, protocol, nativeId })`, `prepareNativeRepost({ clientMutationId, accountId, protocol, nativeId, action: "repost" | "unrepost" })`, `submitNativeRepost(id, { approve: true, event? })`, `nativeRepostStatus(id)`. Requires `NATIVE_SOCIAL_REPOST_PROJECTS`, account capability and explicit approval; Nostr signs the exact review (kind 6 or own kind 5). Unknown attempts only reconcile. Bluesky connections need the new repost scope; ActivityPub uses write:statuses. Native likes: `nativeLikeState({ accountId, protocol, nativeId })`, `prepareNativeLike({ clientMutationId, accountId, protocol, nativeId, action: "like" | "unlike" })`, `submitNativeLike(id, { approve: true, event? })`, and `nativeLikeStatus(id)`. Display the selected account, source and action before approval. Only Nostr supplies `event`: sign the exact public `signing.event` in the review; never transmit a private key. Preserve mutation IDs on ambiguous responses and refresh unknown results instead of resending. Read capabilities before offering actions; old reply-only connections need reconnecting for likes. Nostr results describe the connected relay, not every public relay. The app-specific `NATIVE_SOCIAL_LIKE_PROJECTS` switch and source controls must be enabled on the API.
678
678
  - Delegated social accounts: `r.protocols.nativeSocialAccounts()`, `startNativeSocialConnection({ protocol, identifier })`, `finishNativeSocialConnection({ protocol, state, code?, iss?, error? })`, `disconnectNativeSocialAccount(id)`. Example: `const { data } = await r.protocols.startNativeSocialConnection({ protocol: "bluesky", identifier: "alice.bsky.social" })`; open `data.authorizationUrl`, then complete with the same signed-in app user. Never log callback codes, send passwords, or auto-retry OAuth callbacks. Shared replies (off until schema/API deployment and live verification): `prepareNativeReply({ clientMutationId, text, parent: { protocol, nativeId, readRef? }, accountIds, media?, linkedPostAccountIds?, recoverFrom?: { batchId, deliveryIds } })`, `submitNativeReply(id, { approve: true, deliveryIds })`, `nativeReplyStatus(id)`, `nativeReplyHistory({ limit?, cursor? })`, `prepareNativeReplyRemoval(id, { deliveryIds })`, `removeNativeReply(id, { approve: true, removals })`. Review account, destination, `kind`, source, exact `wireText` and every reviewed `media` item with its alt text; connection is not publication approval. `media` takes the same `NativeMediaInput[]` as `prepareNativePost` and needs the account's `publish_media` capability. `linkedPostAccountIds` explicitly selects different-network standalone posts with a source link, never matching threaded replies or a same-network denial fallback. Preserve mutation IDs, show per-destination results and reconcile uncertain attempts without reposting. To review selected unsent or definitively rejected deliveries again, use recoverFrom with a new mutation ID, the same source/accounts and freshly reviewed text; preserve that ID on uncertain preparation. Only saved recoverable destinations qualify, current gates still apply, and replacedBy disables old sends atomically. Never restart unknown, accepted or published deliveries. The new preview still needs explicit approval. Saved delegated drafts/results: `const { data: history } = await r.protocols.nativeReplyHistory({ limit: 20 })`; use its opaque cursor for the next page and `nativeReplyStatus(item.id)` for details. This is a private read, not a write or provider refresh, and includes Nostr results marked type=nostr. Use nativeNostrReplyResult(item.id) to check that recorded event on its relay without signing or resending. Discard page state on owner/app changes. See README for examples and the callback, revocation and removal contracts.
679
- - `r.protocols` — Private app safety: `sourceControls` returns current `enabled` plus paged saved choices; `setSourceControl`, `onSourceControlChange` and `reportSource` share app scope. Example: `const choices = new Map(); const stop = r.protocols.onSourceControlChange(change => choices.set(JSON.stringify([change.source, change.control]), change.enabled));` then `await r.protocols.setSourceControl({ source: { protocol: "bluesky", kind: "actor", nativeId: "did:plc:aaaaaaaaaaaaaaaaaaaaaaaa" }, control: "mute", enabled: true, scope: "minds" });` and call `stop(); choices.clear()` on account change/unmount. Notifications follow an exact server confirmation, never an uncertain response. These preferences and reports apply in Minds, not on the remote network. Keyless post links: `const visitor = new Recursiv({ anonymous: true }); const { data: thread } = await visitor.protocols.getSharedConversation(projectId, { protocol: 'nostr', post: publicEventId })`; `sharedNetworks(projectId)` lists public conversation capabilities. Only configured apps; app moderation and rate limits apply, no member state or write authority. Member public reading: `publicNetworks`, `searchPublicProfiles`, `getPublicProfile`, `getPublicAuthorFeed`, `getFollowingFeed`, `getPublicConversation`, `getPublicTrends`, `getPublicTrendFeed`; private app subscriptions: `listSubscriptions`, `subscribeToProfile`, `unsubscribeFromProfile`, `onSubscriptionChange`. Example after selecting a returned source post: `const { data: thread } = await r.protocols.getPublicConversation({ protocol: post.protocol, post: post.nativeId, readRef: post.readRef })`. Render `focus`, oldest-first `ancestors` and direct `replies`; `public_provider_window` never promises every reply, and `limited` marks known omissions. Requires both `users:read` and `posts:read` on the current human app key. Protocol discovery/settings: `const { data: adapters } = await r.protocols.list()` returns bounded `{ id, name, icon, color, maturity, capabilities, limitations }` metadata; `supported` means implemented, not configured, healthy, authorized, or enabled. Methods: `list`, `status`, `listCandidates`, `getCandidate`, `search`, `getSettings`, `refresh`, `updateSettings`.
679
+ - `r.protocols` — Private app safety: `sourceControls` returns current `enabled` plus paged saved choices; `setSourceControl`, `onSourceControlChange` and `reportSource` share app scope. Example: `const choices = new Map(); const stop = r.protocols.onSourceControlChange(change => choices.set(JSON.stringify([change.source, change.control]), change.enabled));` then `await r.protocols.setSourceControl({ source: { protocol: "bluesky", kind: "actor", nativeId: "did:plc:aaaaaaaaaaaaaaaaaaaaaaaa" }, control: "mute", enabled: true, scope: "minds" });` and call `stop(); choices.clear()` on account change/unmount. Notifications follow an exact server confirmation, never an uncertain response. These preferences and reports apply in Minds, not on the remote network. Keyless post links: `const visitor = new Recursiv({ anonymous: true, inheritEnvironmentApiKey: false }); const { data: thread } = await visitor.protocols.getSharedConversation(projectId, { protocol: 'nostr', post: publicEventId })`; `sharedNetworks(projectId)` lists public conversation capabilities. Only configured apps; app moderation and rate limits apply, no member state or write authority. Member public reading: `publicNetworks`, `searchPublicProfiles`, `getPublicProfile`, `getPublicAuthorFeed`, `getFollowingFeed`, `getPublicConversation`, `getPublicTrends`, `getPublicTrendFeed`; private app subscriptions: `listSubscriptions`, `subscribeToProfile`, `unsubscribeFromProfile`, `onSubscriptionChange`. Example after selecting a returned source post: `const { data: thread } = await r.protocols.getPublicConversation({ protocol: post.protocol, post: post.nativeId, readRef: post.readRef })`. Render `focus`, oldest-first `ancestors` and direct `replies`; `public_provider_window` never promises every reply, and `limited` marks known omissions. Requires both `users:read` and `posts:read` on the current human app key. Protocol discovery/settings: `const { data: adapters } = await r.protocols.list()` returns bounded `{ id, name, icon, color, maturity, capabilities, limitations }` metadata; `supported` means implemented, not configured, healthy, authorized, or enabled. Methods: `list`, `status`, `listCandidates`, `getCandidate`, `search`, `getSettings`, `refresh`, `updateSettings`.
680
680
  - Native Nostr: `const { data: preview } = await r.protocols.prepareNativeNostr(identityId, { action: "publish", content: draft })`; review and sign the exact public event, then `submitNativeNostr(identityId, { intent: preview.intent, event, approve: true })`. Preparation/submission never auto-retry. After uncertainty, use `getNativeNostrPost(identityId, preview.postId ?? preview.eventId)` before a user-selected retry of the identical intent/event: deletion checks the original note's `postId`, publication checks `eventId`. Check `nativeNostrAccount()` enablement first; keys stay with the signer. For replies, after `nativeNostrAccount()` advertises `reply_text_note`, use `prepareNativeNostr(identityId, { action: "reply", content: draft, parentId: post.nativeId })`. Review returned parent/root/participants/relay and exact event, sign with the connected client signer, then `submitNativeNostr(identityId, { intent, event, approve: true })`. Requires users:read, posts:read and posts:write. Kind1 public notes only; reply enablement and shared UI/live proof remain pending. Do not inherit arbitrary notification tags or relay hints. Signing mutations never auto-retry; check `getNativeNostrPost(identityId, eventId)` after uncertainty before a user-selected identical-event retry.
681
681
  - Hosted Bluesky account on the app's own PDS: `const { data } = await r.protocols.createHostedIdentity({ protocol: 'atproto', handle: 'alice', email })`. Only the label is sent; `data.handle` carries the domain and `data.password` is returned this once, never stored. Never auto-retried. Either the account is finished or it is removed again. 409 `handle_unavailable` = choose another label; 503 `handle_dns_unavailable`/`handle_unresolvable`/`handle_unverified` = operator setup, retry later unchanged.
682
- - Anonymous discovery: `const visitor = new Recursiv({ anonymous: true }); const { data } = await visitor.protocols.getSharedTrends(projectId, { protocol: 'nostr', limit: 20, discovery: true })`. Use `sharedNetworks(projectId)` capabilities; open a returned topic with `getSharedTrendFeed(projectId, { protocol: 'bluesky', topic: trend.nativeId, cursor })`. Configured apps only; no account authority or writes. App moderation and per-IP/app rate limits apply. Categories and Jev layouts reuse cached judgments without initiating paid inference; handle partial categories and finite provider windows.
682
+ - Anonymous discovery: `const visitor = new Recursiv({ anonymous: true, inheritEnvironmentApiKey: false }); const { data } = await visitor.protocols.getSharedTrends(projectId, { protocol: 'nostr', limit: 20, discovery: true })`. Use `sharedNetworks(projectId)` capabilities; open a returned topic with `getSharedTrendFeed(projectId, { protocol: 'bluesky', topic: trend.nativeId, cursor })`. Configured apps only; no account authority or writes. App moderation and per-IP/app rate limits apply. Categories and Jev layouts reuse cached judgments without initiating paid inference; handle partial categories and finite provider windows.
683
683
  - Public trends: require `public_trends` from `publicNetworks()`, then `getPublicTrends({ protocol, limit?, cursor? })`; render `kind: topics` as source topics and `kind: posts` as source posts. With `trend_feed`, open only a returned current topic using `getPublicTrendFeed({ protocol, topic: trend.nativeId, limit?, cursor? })`. Show provider, observedAt and provider_ranked_window coverage; null rankingWindow is unknown, not a daily/global ranking. Preserve source order, canonical IDs and readRef, and opaque cursors. Topic catalogs do not paginate; Mastodon trends stop at 200 source positions. Both methods require users:read plus posts:read on the current human app key; Nostr trends are unsupported. Propagate errors; trend_unavailable means refresh the topic list. No ingestion or native writes.
684
684
  - `listCandidates({ protocol?, limit?, cursor? })` and `getCandidate(id)` are read-only quarantine review, not published-post search. Both require `admin` scope, a project-bound key, a live admin role, and existing project-admin access. The key supplies app/network scope; neither method accepts another project or network. They never invoke refresh, change settings, or publish.
685
685
  - Lists return `{ data: ProtocolQuarantinedCandidate[], meta: { limit, has_more, next_cursor } }`; use the opaque `next_cursor`, not offset pagination. The API defaults `limit` to 20 and permits at most 50. Details return `{ data: ProtocolQuarantinedCandidate }`. Every candidate has `state: 'quarantined'` and `content_format: 'plain'`; render its bounded `content` as text, respect `content_truncated`, and distinguish upstream `published_at` from local `observed_at` / `created_at`.
@@ -790,6 +790,7 @@ The REST API (`/api/v1/*`) supports the `Idempotency-Key` header on POST/PUT/PAT
790
790
  ```typescript
791
791
  const r = new Recursiv({
792
792
  apiKey: 'sk_live_...', // or set RECURSIV_API_KEY env var
793
+ inheritEnvironmentApiKey: true, // default; false guarantees keyless operation
793
794
  baseUrl: 'https://api.recursiv.io/api/v1', // default
794
795
  timeout: 30000, // request timeout in ms
795
796
  maxRetries: 2, // auto-retry on 429/5xx
@@ -798,9 +799,12 @@ const r = new Recursiv({
798
799
  // Zero-arg works when RECURSIV_API_KEY is set
799
800
  const r = new Recursiv()
800
801
 
801
- // Anonymous sandbox (no API key needed)
802
+ // Anonymous sandbox (no API key required; inherits RECURSIV_API_KEY when set)
802
803
  const r = new Recursiv({ anonymous: true })
803
804
 
805
+ // Guaranteed keyless public client, even inside a credentialed server process
806
+ const visitor = new Recursiv({ anonymous: true, inheritEnvironmentApiKey: false })
807
+
804
808
  // Self-hosted
805
809
  const r = new Recursiv({ baseUrl: 'https://my-instance.com/api/v1' })
806
810
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@recursiv/sdk",
3
- "version": "0.7.51",
3
+ "version": "0.7.52",
4
4
  "description": "TypeScript SDK for the Recursiv API",
5
5
  "license": "FSL-1.1-ALv2",
6
6
  "type": "module",
@@ -79,6 +79,6 @@
79
79
  "build": "tsc",
80
80
  "typecheck": "tsc --noEmit",
81
81
  "test": "vitest run",
82
- "test:unit": "vitest run"
82
+ "test:unit": "vitest run && tsc && node ../../scripts/security/verify-sdk-anonymous-package.mjs"
83
83
  }
84
84
  }