@recursiv/sdk 0.7.26 → 0.7.28

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
@@ -110,7 +110,7 @@ for await (const chunk of r.agents.chatStream(agent.id, { message: 'Hello!' }))
110
110
  | `r.admin` | User management, stats, content reports (`listReports`, `resolveReport`, `dismissReport`) |
111
111
  | `r.dispatcher` | Task queue, claims, signals, outcomes, webhooks |
112
112
  | `r.goals` | Org goals with task rollups (group dispatcher tasks) |
113
- | `r.protocols` | Public reading: `publicNetworks`, `searchPublicProfiles`, `getPublicProfile`, `getPublicAuthorFeed`, `getFollowingFeed`, `getPublicConversation`, `getPublicTrends`, `getPublicTrendFeed`; 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` |
113
+ | `r.protocols` | Private Minds safety: `sourceControls` (current enablement and saved choices), `setSourceControl`, `onSourceControlChange`, `reportSource`. Delegated accounts: `nativeSocialAccounts`, `startNativeSocialConnection`, `finishNativeSocialConnection`, `disconnectNativeSocialAccount`. Reviewed replies and optional linked posts: `prepareNativeReply`, `submitNativeReply`, `nativeReplyStatus`, `nativeReplyHistory`, `prepareNativeReplyRemoval`, `removeNativeReply` (require deployed API support and app enablement). Public reading: `publicNetworks`, `searchPublicProfiles`, `getPublicProfile`, `getPublicAuthorFeed`, `getFollowingFeed`, `getPublicConversation`, `getPublicTrends`, `getPublicTrendFeed`; 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` |
114
114
 
115
115
  Full reference: see [llm.md](./llm.md) (ships with this package) or the type definitions in `dist/`.
116
116
 
@@ -459,3 +459,114 @@ configured. After an uncertain submission, check
459
459
  `getNativeNostrPost(identityId, preview.postId ?? preview.eventId)`: deletion uses
460
460
  the original note's `postId`, while publication uses the new `eventId`. Any
461
461
  user-selected retry must keep the same intent and signed event.
462
+
463
+ When the deployed API advertises `reply_text_note`, the same method can prepare
464
+ `{ action: 'reply', content: draftText, parentId: selectedPost.nativeId }`. Review
465
+ the returned parent, root, participants, relay and exact event before asking the
466
+ signer to sign. The API constructs verified NIP-10 references; callers cannot
467
+ inject tags, participant lists, relay URLs or signing keys. Reply prepare/submit
468
+ also require `posts:read`. This capability remains off until the shared reply UI
469
+ and live delivery are verified. Only public kind-1 notes are supported; additional
470
+ participant selection is unfinished. `textTruncated` labels an abbreviated source
471
+ preview. Preparation and submission never automatically retry; check the event ID
472
+ after an uncertain result before choosing a retry of the identical signed event.
473
+
474
+ ### Delegated Bluesky and ActivityPub accounts
475
+
476
+ These methods require the same signed-in human app key throughout. The API must
477
+ enable the app and deploy the authorization schema before use.
478
+
479
+ ```ts
480
+ const { data: connection } = await r.protocols.startNativeSocialConnection({
481
+ protocol: 'bluesky', identifier: 'alice.bsky.social',
482
+ })
483
+ // Navigate to connection.authorizationUrl. On the configured app callback:
484
+ const { data: account } = await r.protocols.finishNativeSocialConnection({
485
+ protocol: 'bluesky', state, code, iss,
486
+ })
487
+ const { data: accounts } = await r.protocols.nativeSocialAccounts()
488
+ await r.protocols.disconnectNativeSocialAccount(account.id)
489
+ ```
490
+
491
+ ActivityPub uses an operator-approved HTTPS server origin as `identifier` and
492
+ requires Mastodon-compatible OAuth metadata with S256 PKCE. The server configures
493
+ the exact callback path `/social/connect/callback/:protocol`; the app must remove
494
+ its query from browser history immediately and never log codes. No password or
495
+ native token is sent through these SDK methods. Start and callback are single-use
496
+ requests and are never automatically retried. If a callback result is uncertain,
497
+ refresh the account list before starting again.
498
+
499
+ Connection does not approve publication. The account's `capabilities` lists only
500
+ implemented actions, currently empty for these adapters. Local disconnect takes
501
+ effect even when `providerRevocation` is `unconfirmed`; the user can also revoke
502
+ Minds from their network's authorized-app settings.
503
+
504
+ ### One reviewed reply across selected accounts
505
+
506
+ The shared reply API is off by default. Deploy its required schema/API and finish
507
+ live authorization and delivery verification before enabling it. This example
508
+ prepares a preview; it does not publish anything:
509
+
510
+ ```ts
511
+ const { data: preview } = await r.protocols.prepareNativeReply({
512
+ clientMutationId: crypto.randomUUID(), text: draftText,
513
+ parent: { protocol: 'bluesky', nativeId: selectedPost.nativeId },
514
+ accountIds: [blueskyAccount.id, activityPubAccount.id],
515
+ linkedPostAccountIds: [activityPubAccount.id],
516
+ })
517
+ // Review each delivery's account, protocol, kind, parent and exact wireText.
518
+ // After the user approves selected delivery IDs:
519
+ await r.protocols.submitNativeReply(preview.id, { approve: true, deliveryIds: approvedDeliveryIds })
520
+ ```
521
+
522
+ Use `linkedPostAccountIds` only after the user selects a standalone post with the
523
+ source link on that different network. It is not a matching threaded reply, and
524
+ a denied same-network reply never falls back automatically. Verified cross-network
525
+ thread mappings are not implemented here. Nostr uses the connected client signer
526
+ described above. Preserve the mutation
527
+ ID while recovering an uncertain preparation; read `nativeReplyStatus(preview.id)` to
528
+ show each destination's result. Repeated submit reconciles an attempted delivery
529
+ without publishing it again. Removal uses a fresh `prepareNativeReplyRemoval`
530
+ preview and explicit `removeNativeReply` approval for the selected own posts.
531
+
532
+ For a selected unsent or definitively rejected destination, the same prepare
533
+ method accepts `recoverFrom: { batchId: preview.id, deliveryIds: selectedIds }`
534
+ with a **new** mutation ID, the same source/accounts and freshly reviewed text.
535
+ Preserve that new ID if preparation is uncertain. The server creates the fresh
536
+ review and disables only those old destinations atomically; `replacedBy` points
537
+ to the new batch. `recoverable` describes saved eligibility, while current account,
538
+ source and safety checks still apply. Sending, unknown, accepted or published
539
+ destinations cannot be resent this way. The fresh preview still needs explicit
540
+ approval; completed destinations retain their outcomes.
541
+
542
+ `nativeReplyHistory` pages through one private history containing Bluesky/ActivityPub
543
+ drafts and delivery results, plus Nostr replies and linked posts once submitted. It also includes unsent or expired preparations. The
544
+ summary contains a bounded preview of the user's own text and source references,
545
+ not old remote parent text or approval material. Opening a result with
546
+ `nativeReplyStatus(id)` does not send anything. Reading remains available when
547
+ new replies are disabled; it does not refresh credentials or contact a network.
548
+ Nostr items have `type: "nostr"`. `nativeNostrReplyResult(id)` checks the saved event
549
+ on its recorded relay without signing or resending. It requires the same connected
550
+ identity and preserves historical publication/deletion evidence separately from
551
+ `lastReadBack`. A relay absence is not proof of global deletion. Signatures and
552
+ intents are never retained in history.
553
+
554
+ ```ts
555
+ const { data: history } = await r.protocols.nativeReplyHistory({ limit: 20 })
556
+ // Read a saved result without submitting it.
557
+ const firstResult = history.items[0]
558
+ if (firstResult) {
559
+ if ('type' in firstResult && firstResult.type === 'nostr') {
560
+ await r.protocols.nativeNostrReplyResult(firstResult.id)
561
+ } else await r.protocols.nativeReplyStatus(firstResult.id)
562
+ }
563
+ if (history.cursor) {
564
+ const { data: next } = await r.protocols.nativeReplyHistory({ limit: 20, cursor: history.cursor })
565
+ console.log(next.items.length)
566
+ }
567
+ ```
568
+
569
+ Keep the cursor opaque and discard it when the app or signed-in user changes.
570
+ History order is fixed by creation time, so later delivery updates do not move
571
+ older entries across page boundaries. Reopen each result for its current saved
572
+ state and obtain a fresh review before a new write.
package/dist/index.d.ts CHANGED
@@ -160,7 +160,7 @@ export { EnvVarsResource } from './resources/env-vars.js';
160
160
  export type { QueryDatabaseInput, QueryResult } from './resources/databases.js';
161
161
  export { GitHubResource } from './resources/github.js';
162
162
  export { DeploymentsResource } from './resources/deployments.js';
163
- export type { PrepareProtocolServersInput, PreparedProtocolServers, ProtocolServerPreparation } from './resources/deployments.js';
163
+ export type { PrepareProtocolServersInput, PreparedProtocolServers, ProtocolServerPreparation, ProtocolServerUpgradePreview, ProtocolServerUpgradeInput, ProtocolServerUpgradeStatus } from './resources/deployments.js';
164
164
  export { IntegrationsResource } from './resources/integrations.js';
165
165
  export type { ExecuteToolInput, ExecuteToolResult } from './resources/integrations.js';
166
166
  export { ProjectBrainResource } from './resources/project-brain.js';
@@ -171,7 +171,7 @@ export { VideoResource } from './resources/video.js';
171
171
  export type { VideoUploadCreds, CreateVideoUploadResult, VideoStatusInfo, VideoPlaybackToken, } from './resources/video.js';
172
172
  export { AppSubscriptionsResource } from './resources/app-subscriptions.js';
173
173
  export type { AppSubscriptionCheckoutResult, AppSubscriptionPortalResult, AppSubscriptionStatusResult, } from './resources/app-subscriptions.js';
174
- export type { CuratorSource, CuratorPrompt, CuratorRunInput, CuratorRunResult, } from './resources/curator.js';
174
+ export type { CuratorSource, CuratorPrompt, CuratorRunInput, CuratorRunResult, CuratorTodayEdition, CuratorTodayResponse, CuratorTodaySource, CuratorTodayStory, } from './resources/curator.js';
175
175
  export { ReportsResource } from './resources/reports.js';
176
176
  export type { ReportTargetType, ReportStatus, CreateReportInput, ReportRecord, ResolveReportInput, } from './resources/reports.js';
177
177
  export { ModerationResource } from './resources/moderation.js';
@@ -189,7 +189,7 @@ export type { UploadMediaInput, UploadUrlInput, UploadUrlResult } from './resour
189
189
  export { WalletResource } from './resources/wallet.js';
190
190
  export { ProtocolsResource } from './resources/protocols.js';
191
191
  export { PROTOCOL_ADAPTER_MATURITIES, PROTOCOL_CAPABILITY_OPERATIONS, PROTOCOL_CAPABILITY_STATUSES, } from './resources/protocols.js';
192
- export type { SocialProfile, SocialSubscription, SocialSubscriptionChange, SocialProfilePage, SocialNetwork, SocialPost, SocialConversation, SocialTrend, SocialTrendProvenance, SocialTrends, SocialTrendFeed, SocialAuthorFeed, SocialFollowingFeed, SocialFollowingSource, SearchPublicProfilesParams, HostedProtocolConnections, HostedProtocol, HostedProtocolIdentity, HostedNostrChallenge, HostedNostrIdentityProof, NativeNostrPrepareInput, NativeNostrPrepared, NativeNostrAccount, NativeNostrPostState, NativeNostrReceipt, HostedPostPreview, ConnectHostedIdentityInput, PreviewHostedPostInput, PublishHostedPostInput, ListProtocolCandidatesParams, ProtocolAdapterMaturity, ProtocolAdapterMetadata, ProtocolCandidateListResponse, ProtocolCandidatePageMeta, ProtocolCapabilityOperation, ProtocolCapabilityStatus, ProtocolCollectionStatus, ProtocolQuarantinedCandidate, ProtocolRefreshResult, ProtocolSettings, ProtocolSettingsUpdateResult, UpdateSettingsInput as UpdateProtocolSettingsInput, } from './resources/protocols.js';
192
+ export type { SocialProfile, SocialSubscription, SocialSubscriptionChange, SocialProfilePage, SocialNetwork, SocialPost, SocialConversation, SocialTrend, SocialTrendProvenance, SocialTrends, SocialTrendFeed, SocialAuthorFeed, SocialFollowingFeed, SocialFollowingSource, SearchPublicProfilesParams, HostedProtocolConnections, HostedProtocol, HostedProtocolIdentity, HostedNostrChallenge, HostedNostrIdentityProof, NativeNostrPrepareInput, NativeNostrPrepared, NativeNostrAccount, NativeNostrPostState, NativeNostrReceipt, NativeSocialProtocol, FederationIdentity, FederationIdentityView, NativeSocialAccount, NativeSocialCallbackInput, NativeReplyPrepareInput, NativeReplyReceipt, NativeReplyBatch, NativeReplyHistory, NativeDelegatedReplyHistoryItem, NativeNostrReplyResult, NativeNostrReplyHistoryItem, NativeReplyRemovalReceipt, NativeReplyRemovalPreview, SocialSourceRef, SocialSourceControl, SocialSourceControlChange, HostedPostPreview, ConnectHostedIdentityInput, PreviewHostedPostInput, PublishHostedPostInput, ListProtocolCandidatesParams, ProtocolAdapterMaturity, ProtocolAdapterMetadata, ProtocolCandidateListResponse, ProtocolCandidatePageMeta, ProtocolCapabilityOperation, ProtocolCapabilityStatus, ProtocolCollectionStatus, ProtocolQuarantinedCandidate, ProtocolRefreshResult, ProtocolSettings, ProtocolSettingsUpdateResult, UpdateSettingsInput as UpdateProtocolSettingsInput, } from './resources/protocols.js';
193
193
  export { InboxResource } from './resources/inbox.js';
194
194
  export { NetworkResource } from './resources/network.js';
195
195
  export { SimulatorResource } from './resources/simulator.js';
@@ -6,11 +6,15 @@ export interface SiblingAccount {
6
6
  username: string;
7
7
  name: string;
8
8
  image: string | null;
9
- created_at: string;
9
+ created_at: string | null;
10
+ /** Followers on this account. The main-account rule reads this field. */
11
+ follower_count: number;
10
12
  /** The stored primary email is a migration placeholder, not a real inbox. */
11
13
  is_placeholder_email: boolean;
12
14
  /** Server heuristic used to collapse test/demo identities in account pickers. */
13
15
  looks_like_test: boolean;
16
+ /** The account OTP sign-in lands on. Exactly one row carries it. */
17
+ is_primary: boolean;
14
18
  is_current: boolean;
15
19
  }
16
20
  export interface SiblingAccountsResponse {
@@ -19,6 +23,8 @@ export interface SiblingAccountsResponse {
19
23
  /** The verified real address shared by the returned accounts. */
20
24
  email: string;
21
25
  count: number;
26
+ /** Id of the main account, or null when the caller has no usable address. */
27
+ primary_id?: string | null;
22
28
  };
23
29
  }
24
30
  export interface SwitchAccountResult {
@@ -36,7 +42,7 @@ export interface SwitchAccountResult {
36
42
  export declare class AccountsResource {
37
43
  private client;
38
44
  constructor(client: HttpClient);
39
- /** List every account controlled by the caller's verified real email. */
45
+ /** List every account controlled by the caller's verified real email, main account first. */
40
46
  listSiblings(): Promise<SiblingAccountsResponse>;
41
47
  /** Mint a replacement key for a sibling account in the caller's project. */
42
48
  switchAccount(targetUserId: string): Promise<SingleResponse<SwitchAccountResult>>;
@@ -11,7 +11,7 @@ export class AccountsResource {
11
11
  constructor(client) {
12
12
  this.client = client;
13
13
  }
14
- /** List every account controlled by the caller's verified real email. */
14
+ /** List every account controlled by the caller's verified real email, main account first. */
15
15
  listSiblings() {
16
16
  return this.client.get('/accounts/siblings');
17
17
  }
@@ -105,9 +105,77 @@ export interface CuratorSeedDeckResponse {
105
105
  data: CuratorSeedDeckItem[];
106
106
  meta: CuratorSeedDeckMeta;
107
107
  }
108
+ /** Which half of the press an outlet belongs to. */
109
+ export type CuratorOutletLane = 'mainstream' | 'independent';
110
+ /** One outlet item behind a world story. Headline and link only, never an article body. */
111
+ export interface CuratorTodaySource {
112
+ outlet: string;
113
+ /** The outlet's lane. Absent on stories written before lanes existed. */
114
+ lane?: CuratorOutletLane;
115
+ title: string;
116
+ url: string;
117
+ published_at: string | null;
118
+ }
119
+ /** Distinct outlets per lane behind a world story. */
120
+ export interface CuratorTodayLaneMix {
121
+ mainstream: number;
122
+ independent: number;
123
+ }
124
+ export interface CuratorTodayStory {
125
+ id: string;
126
+ /** 1 = lead. World stories hold 1..N, Minds stories follow. */
127
+ rank: number;
128
+ /**
129
+ * 'world': the open web set it, ranked by how many outlets moved on it in
130
+ * the last 24 h with an independent outlet weighted above a mainstream one.
131
+ * 'minds': a group of real Minds posts.
132
+ */
133
+ kind: 'world' | 'minds';
134
+ kicker: string;
135
+ headline: string;
136
+ dek: string | null;
137
+ topic: string | null;
138
+ /** Outlet links for a world story; null for a Minds story. */
139
+ sources: CuratorTodaySource[] | null;
140
+ /** Distinct outlets behind a world story; null for a Minds story. */
141
+ outlet_count: number | null;
142
+ /** Distinct outlets per lane behind a world story; null for a Minds story. */
143
+ lane_mix: CuratorTodayLaneMix | null;
144
+ /**
145
+ * Which lanes carry a world story: 'mainstream', 'independent' (nobody in
146
+ * the mainstream lane ran it) or 'both'. Null for a Minds story.
147
+ */
148
+ lane: CuratorOutletLane | 'both' | null;
149
+ /** The discussion-room post when one exists. */
150
+ discussion_post_id: string | null;
151
+ /** Replies on the discussion post; 0 when there is none. */
152
+ reply_count: number;
153
+ /** Minds posts in the same shape as `sdk.posts.list()`. May be empty for a world story. */
154
+ posts: Post[];
155
+ }
156
+ export interface CuratorTodayEdition {
157
+ id: string;
158
+ edition_date: string;
159
+ status: 'draft' | 'published';
160
+ generated_at: string;
161
+ stories: CuratorTodayStory[];
162
+ }
163
+ export interface CuratorTodayResponse {
164
+ /** null when the network has no published edition yet. */
165
+ data: CuratorTodayEdition | null;
166
+ meta: {
167
+ generating: boolean;
168
+ };
169
+ }
108
170
  export declare class CuratorResource {
109
171
  private client;
110
172
  constructor(client: HttpClient);
173
+ /**
174
+ * Today: the daily front page for the caller's network. World stories
175
+ * (set by the open web, ranked by outlet count) come first; Minds
176
+ * conversation stories follow. Requires `posts:read`.
177
+ */
178
+ today(): Promise<CuratorTodayResponse>;
111
179
  /**
112
180
  * Fetch the supplied sources in parallel, annotate each top item with
113
181
  * the supplied prompt template, and insert audience-scoped posts into
@@ -3,6 +3,14 @@ export class CuratorResource {
3
3
  constructor(client) {
4
4
  this.client = client;
5
5
  }
6
+ /**
7
+ * Today: the daily front page for the caller's network. World stories
8
+ * (set by the open web, ranked by outlet count) come first; Minds
9
+ * conversation stories follow. Requires `posts:read`.
10
+ */
11
+ today() {
12
+ return this.client.get('/curator/today');
13
+ }
6
14
  /**
7
15
  * Fetch the supplied sources in parallel, annotate each top item with
8
16
  * the supplied prompt template, and insert audience-scoped posts into
@@ -62,6 +62,12 @@ export interface PreparedProtocolServers {
62
62
  runtime_status: 'unverified';
63
63
  }
64
64
  export interface ProtocolServerPreparation {
65
+ /** Non-secret locator for recovering inspection after an operator/client restart. */
66
+ upgrade?: {
67
+ revision: string;
68
+ image: string;
69
+ phase: ProtocolServerUpgradeStatus['phase'];
70
+ };
65
71
  /** Provider process observation only; protocol interoperability remains unverified. */
66
72
  provider_status: 'running' | 'stopped' | 'transitioning' | 'unhealthy' | 'unknown';
67
73
  id: string;
@@ -87,6 +93,32 @@ export interface HostedServiceScope {
87
93
  export interface HostedServiceOperation extends HostedServiceScope {
88
94
  request_hash: string;
89
95
  }
96
+ export interface ProtocolServerUpgradePreview {
97
+ id: string;
98
+ provider_service_uuid: string;
99
+ revision: string;
100
+ from_image: string;
101
+ to_image: string;
102
+ restart_services: ['pds', 'activitypub', 'nostr'];
103
+ provider_status: 'running' | 'stopped';
104
+ operation: 'preview';
105
+ runtime_status: 'unverified';
106
+ storage_identity_verification_required: true;
107
+ }
108
+ export interface ProtocolServerUpgradeInput extends HostedServiceOperation {
109
+ image: string;
110
+ revision: string;
111
+ }
112
+ export interface ProtocolServerUpgradeStatus {
113
+ revision: string;
114
+ phase: 'applying' | 'configuration_saved' | 'restarting' | 'restart_requested';
115
+ runtime_status: 'unverified';
116
+ automatic_retry_allowed: false;
117
+ configuration_matches?: boolean;
118
+ provider_status?: ProtocolServerPreparation['provider_status'];
119
+ restart_services?: ['pds', 'activitypub', 'nostr'];
120
+ identity_verification_required?: true;
121
+ }
90
122
  export interface HostedServiceReservation {
91
123
  id: string;
92
124
  provider_service_uuid: string | null;
@@ -115,6 +147,16 @@ export declare class DeploymentsResource {
115
147
  project_id: string;
116
148
  network_id: string;
117
149
  }): Promise<SingleResponse<ProtocolServerPreparation>>;
150
+ /** Read-only staging image proposal; does not update or restart servers.
151
+ * Requires an unbound owner key and the server-configured approved image digest. */
152
+ previewProtocolServerUpgrade(input: HostedServiceOperation & {
153
+ image: string;
154
+ }): Promise<SingleResponse<ProtocolServerUpgradePreview>>;
155
+ /** Apply a reviewed staging upgrade once. Never automatically retry a lost response.
156
+ * Acceptance does not establish live identity, storage contents or interoperability. */
157
+ upgradeProtocolServers(input: ProtocolServerUpgradeInput): Promise<SingleResponse<ProtocolServerUpgradeStatus>>;
158
+ /** Read the saved upgrade and provider configuration; never replays a write. */
159
+ getProtocolServerUpgrade(input: ProtocolServerUpgradeInput): Promise<SingleResponse<ProtocolServerUpgradeStatus>>;
118
160
  /** Queue a single activation after verifying the saved provider configuration and secrets.
119
161
  * A 202 response is not evidence of live runtime health. */
120
162
  startProtocolServers(input: {
@@ -26,6 +26,20 @@ export class DeploymentsResource {
26
26
  getProtocolServerPreparation(scope) {
27
27
  return this.client.get('/deployments/protocol-servers', { project_id: scope.project_id, network_id: scope.network_id });
28
28
  }
29
+ /** Read-only staging image proposal; does not update or restart servers.
30
+ * Requires an unbound owner key and the server-configured approved image digest. */
31
+ previewProtocolServerUpgrade(input) {
32
+ return this.client.post('/deployments/protocol-servers/upgrade-preview', input);
33
+ }
34
+ /** Apply a reviewed staging upgrade once. Never automatically retry a lost response.
35
+ * Acceptance does not establish live identity, storage contents or interoperability. */
36
+ upgradeProtocolServers(input) {
37
+ return this.client.post('/deployments/protocol-servers/upgrade', input, { retry: false });
38
+ }
39
+ /** Read the saved upgrade and provider configuration; never replays a write. */
40
+ getProtocolServerUpgrade(input) {
41
+ return this.client.post('/deployments/protocol-servers/upgrade-status', input);
42
+ }
29
43
  /** Queue a single activation after verifying the saved provider configuration and secrets.
30
44
  * A 202 response is not evidence of live runtime health. */
31
45
  startProtocolServers(input) {
@@ -1,6 +1,6 @@
1
1
  import type { HttpClient } from '../client.js';
2
2
  import type { SingleResponse } from '../types.js';
3
- export type ModerationTargetType = 'post' | 'user' | 'message' | 'community' | 'agent';
3
+ export type ModerationTargetType = 'post' | 'user' | 'message' | 'community' | 'agent' | 'source_actor' | 'source_post';
4
4
  export type ModerationActionType = 'down_rank' | 'label' | 'remove' | 'suspend' | 'ban';
5
5
  export type ModerationAppealStatus = 'none' | 'pending' | 'upheld' | 'overturned';
6
6
  export interface ModerationPrinciple {
@@ -53,7 +53,8 @@ export interface OwnModerationLog {
53
53
  actions: OwnModerationAction[];
54
54
  }
55
55
  export interface ApplyModerationActionInput {
56
- target_type: ModerationTargetType;
56
+ /** Source actions are resolved through the scoped report queue. */
57
+ target_type: Exclude<ModerationTargetType, 'source_actor' | 'source_post'>;
57
58
  target_id: string;
58
59
  action: ModerationActionType;
59
60
  label?: string;
@@ -82,7 +82,12 @@ export interface ProtocolQuarantinedCandidate {
82
82
  provider: string;
83
83
  /** Safe IDs are byte-exact; null means unsafe or malformed provenance was withheld. */
84
84
  external_id: string | null;
85
- state: 'quarantined';
85
+ state: ProtocolCandidateState;
86
+ /** Increments whenever the payload digest changes; decisions and verdicts are keyed by it. */
87
+ revision: number;
88
+ revision_digest: string | null;
89
+ /** Latest screening verdict on the current revision, or null when unscreened. */
90
+ latest_verdict: ProtocolCandidateVerdictSummary | null;
86
91
  title: string | null;
87
92
  content: string;
88
93
  content_format: 'plain';
@@ -99,9 +104,37 @@ export interface ProtocolQuarantinedCandidate {
99
104
  observed_at: string;
100
105
  created_at: string;
101
106
  }
107
+ export type ProtocolCandidateState = 'quarantined' | 'approved' | 'rejected' | 'escalated' | 'tombstoned';
108
+ export type ProtocolScreeningVerdict = 'pass' | 'hold' | 'fail';
109
+ export type ProtocolCandidateDecision = 'approved' | 'rejected' | 'escalated';
110
+ export interface ProtocolCandidateVerdictSummary {
111
+ verdict: ProtocolScreeningVerdict;
112
+ line: string;
113
+ category: string | null;
114
+ high_risk: boolean;
115
+ reason: string | null;
116
+ created_at: string;
117
+ }
118
+ export interface ProtocolCandidateVerdict extends ProtocolCandidateVerdictSummary {
119
+ id: string;
120
+ revision: number;
121
+ model: string | null;
122
+ confidence: number | null;
123
+ }
124
+ export interface DecideProtocolCandidateInput {
125
+ decision: ProtocolCandidateDecision;
126
+ /** Required for escalation; recorded on every decision. */
127
+ reason?: string;
128
+ }
129
+ export interface ProtocolCandidateDecisionResult {
130
+ candidate: ProtocolQuarantinedCandidate;
131
+ decision_id: string;
132
+ }
102
133
  /** App/network scope comes only from the authenticated project-bound key. */
103
134
  export interface ListProtocolCandidatesParams {
104
135
  protocol?: string;
136
+ /** Candidate state to list; the API defaults to quarantined. */
137
+ state?: ProtocolCandidateState;
105
138
  /** Page size, default 20 and maximum 50; enforced by the API. */
106
139
  limit?: number;
107
140
  /** Opaque cursor from this endpoint's previous page; never an offset. */
@@ -162,10 +195,199 @@ export interface HostedNostrIdentityProof {
162
195
  export type NativeNostrPrepareInput = {
163
196
  action: 'publish';
164
197
  content: string;
198
+ } | {
199
+ action: 'reply';
200
+ content: string;
201
+ parentId: string;
202
+ } | {
203
+ action: 'linked_post';
204
+ content: string;
205
+ source: {
206
+ protocol: NativeSocialProtocol;
207
+ nativeId: string;
208
+ readRef?: string;
209
+ };
165
210
  } | {
166
211
  action: 'delete';
167
212
  postId: string;
168
213
  };
214
+ export type NativeSocialProtocol = 'bluesky' | 'activitypub';
215
+ export interface NativeSocialAccount {
216
+ id: string;
217
+ protocol: NativeSocialProtocol;
218
+ nativeId: string;
219
+ serverUrl: string;
220
+ handle: string;
221
+ connectedAt: string;
222
+ capabilities: string[];
223
+ }
224
+ /** A Minds-hosted ActivityPub identity for this app user. Keys never leave the platform. */
225
+ export interface FederationIdentity {
226
+ identifier: string;
227
+ actorUri: string;
228
+ handle: string;
229
+ enabled: boolean;
230
+ origin: string;
231
+ }
232
+ export interface FederationIdentityView {
233
+ enabled: boolean;
234
+ origin: string | null;
235
+ identity: FederationIdentity | null;
236
+ }
237
+ export interface SocialSourceRef {
238
+ protocol: 'bluesky' | 'activitypub' | 'nostr';
239
+ kind: 'actor' | 'post';
240
+ nativeId: string;
241
+ }
242
+ export interface SocialSourceControl {
243
+ id: string;
244
+ source: SocialSourceRef;
245
+ control: 'block' | 'mute' | 'hide';
246
+ scope: 'minds';
247
+ createdAt: string;
248
+ }
249
+ export type SocialSourceControlChange = Omit<SocialSourceControl, 'id' | 'createdAt'> & {
250
+ enabled: boolean;
251
+ };
252
+ export interface NativeReplyPrepareInput {
253
+ clientMutationId: string;
254
+ text: string;
255
+ parent: {
256
+ protocol: SocialSourceRef['protocol'];
257
+ nativeId: string;
258
+ readRef?: string;
259
+ };
260
+ accountIds: string[];
261
+ /** Selected different-network accounts to review as standalone posts with a source link. */
262
+ linkedPostAccountIds?: string[];
263
+ /** Fresh review of selected unsent or definitively rejected deliveries. The original selected reviews are atomically fenced; preserve this mutation ID on retry. */
264
+ recoverFrom?: {
265
+ batchId: string;
266
+ deliveryIds: string[];
267
+ };
268
+ }
269
+ export interface NativeReplyReceipt {
270
+ state: 'published' | 'accepted' | 'unknown' | 'rejected';
271
+ nativeId: string | null;
272
+ sourceUrl: string | null;
273
+ providerId: string | null;
274
+ acknowledged: boolean;
275
+ readBack: 'matched' | 'absent' | 'unavailable';
276
+ }
277
+ export interface NativeReplyRemovalReceipt {
278
+ /** Absence is observed on the owning server; this is not global erasure. */
279
+ state: 'absent' | 'accepted' | 'unknown' | 'rejected';
280
+ acknowledged: boolean;
281
+ readBack: 'absent' | 'present' | 'unavailable';
282
+ }
283
+ export interface NativeReplyRemovalPreview {
284
+ id: string;
285
+ previews: Array<{
286
+ deliveryId: string;
287
+ removalId: string;
288
+ expiresAt: string;
289
+ protocol: NativeSocialProtocol;
290
+ nativeId: string;
291
+ providerId: string;
292
+ sourceUrl: string;
293
+ revision: string | null;
294
+ text: string | null;
295
+ additionalContent: boolean;
296
+ account: {
297
+ nativeId: string;
298
+ handle: string;
299
+ serverUrl: string;
300
+ } | null;
301
+ }>;
302
+ errors: Record<string, {
303
+ code: string;
304
+ message: string;
305
+ }>;
306
+ }
307
+ export interface NativeReplyBatch {
308
+ id: string;
309
+ clientMutationId: string;
310
+ expiresAt: string;
311
+ deliveries: Array<{
312
+ id: string;
313
+ grantId: string;
314
+ protocol: NativeSocialProtocol;
315
+ kind: 'reply' | 'linked_post';
316
+ state: 'prepared' | 'sending' | NativeReplyReceipt['state'];
317
+ attemptedAt: string | null;
318
+ /** Saved attempt eligibility only; source, account authority and app/provider gates are checked again during preparation. */
319
+ recoverable?: boolean;
320
+ /** Newer batch that replaced this destination's approval. This old destination cannot publish. */
321
+ replacedBy?: string;
322
+ account: {
323
+ nativeId: string;
324
+ handle: string;
325
+ serverUrl: string;
326
+ } | null;
327
+ parent: {
328
+ protocol: SocialSourceRef['protocol'];
329
+ nativeId: string;
330
+ sourceUrl: string;
331
+ authorId: string;
332
+ authorHandle: string;
333
+ text: string;
334
+ textTruncated?: boolean;
335
+ };
336
+ text: string;
337
+ wireText: string;
338
+ receipt: NativeReplyReceipt | null;
339
+ removal: {
340
+ id: string;
341
+ expiresAt: string;
342
+ attemptedAt: string | null;
343
+ receipt: NativeReplyRemovalReceipt | null;
344
+ } | null;
345
+ }>;
346
+ }
347
+ export interface NativeSocialCallbackInput {
348
+ protocol: NativeSocialProtocol;
349
+ state: string;
350
+ code?: string;
351
+ iss?: string;
352
+ error?: string;
353
+ }
354
+ export interface NativeDelegatedReplyHistoryItem {
355
+ id: string;
356
+ createdAt: string;
357
+ text: string;
358
+ textTruncated: boolean;
359
+ source: Pick<NativeReplyBatch['deliveries'][number]['parent'], 'protocol' | 'nativeId' | 'sourceUrl'>;
360
+ deliveries: Array<Pick<NativeReplyBatch['deliveries'][number], 'id' | 'protocol' | 'kind' | 'state' | 'attemptedAt' | 'account' | 'recoverable' | 'replacedBy'> & {
361
+ removalReceipt: NativeReplyRemovalReceipt | null;
362
+ }>;
363
+ }
364
+ export interface NativeNostrReplyResult {
365
+ id: string;
366
+ createdAt: string;
367
+ text: string;
368
+ source: {
369
+ protocol: SocialSourceRef['protocol'];
370
+ nativeId: string;
371
+ sourceUrl: string;
372
+ };
373
+ identityId: string;
374
+ pubkey: string;
375
+ relay: string;
376
+ eventId: string;
377
+ action: 'reply' | 'linked_post';
378
+ receipt: NativeNostrReceipt;
379
+ checkedAt: string | null;
380
+ lastReadBack: NativeNostrReceipt['readBack'];
381
+ }
382
+ export interface NativeNostrReplyHistoryItem extends NativeNostrReplyResult {
383
+ type: 'nostr';
384
+ textTruncated: boolean;
385
+ }
386
+ export interface NativeReplyHistory {
387
+ /** One private history, including client-signed Nostr replies once submitted. */
388
+ items: Array<NativeDelegatedReplyHistoryItem | NativeNostrReplyHistoryItem>;
389
+ cursor: string | null;
390
+ }
169
391
  export interface NativeNostrPrepared {
170
392
  intent: string;
171
393
  event: {
@@ -175,17 +397,42 @@ export interface NativeNostrPrepared {
175
397
  tags: string[][];
176
398
  };
177
399
  eventId: string;
178
- action: 'publish' | 'delete';
400
+ action: 'publish' | 'reply' | 'linked_post' | 'delete';
179
401
  identityId: string;
180
402
  pubkey: string;
181
403
  relay: string;
182
404
  expiresAt: string;
183
405
  postId?: string;
406
+ parent?: {
407
+ protocol: 'nostr';
408
+ nativeId: string;
409
+ authorId: string;
410
+ authorHandle: string;
411
+ sourceUrl: string;
412
+ text: string;
413
+ textTruncated: boolean;
414
+ root: {
415
+ nativeId: string;
416
+ authorId: string;
417
+ sourceUrl: string;
418
+ };
419
+ participants: string[];
420
+ tags: string[][];
421
+ };
422
+ source?: {
423
+ protocol: NativeSocialProtocol;
424
+ nativeId: string;
425
+ authorId: string;
426
+ authorHandle: string;
427
+ sourceUrl: string;
428
+ text: string;
429
+ textTruncated?: boolean;
430
+ };
184
431
  }
185
432
  export interface NativeNostrAccount {
186
433
  enabled: boolean;
187
434
  identity: HostedProtocolIdentity | null;
188
- capabilities: Array<'publish_original_text' | 'request_own_post_deletion'>;
435
+ capabilities: Array<'publish_original_text' | 'request_own_post_deletion' | 'reply_text_note' | 'publish_linked_text'>;
189
436
  signing?: 'client';
190
437
  }
191
438
  export interface NativeNostrPostState {
@@ -202,7 +449,7 @@ export interface NativeNostrReceipt {
202
449
  eventId: string;
203
450
  postId: string;
204
451
  relay: string;
205
- action: 'publish' | 'delete';
452
+ action: 'publish' | 'reply' | 'linked_post' | 'delete';
206
453
  state: 'published' | 'deletion_requested' | 'unknown' | 'rejected';
207
454
  acknowledged: boolean;
208
455
  readBack: 'found' | 'absent' | 'unavailable';
@@ -380,10 +627,13 @@ export interface SocialFollowingFeed {
380
627
  export declare class ProtocolsResource {
381
628
  private client;
382
629
  private subscriptionListeners;
630
+ private sourceControlListeners;
383
631
  constructor(client: HttpClient);
384
632
  /** Confirmed changes from this SDK instance only. Unsubscribe when a view unmounts or its account changes. */
385
633
  onSubscriptionChange(listener: (change: SocialSubscriptionChange) => void): () => void;
386
634
  private subscriptionChanged;
635
+ /** Confirmed safety choices from this authenticated SDK instance. Unsubscribe when its account or view changes. */
636
+ onSourceControlChange(listener: (change: SocialSourceControlChange) => void): () => void;
387
637
  publicNetworks(): Promise<SingleResponse<SocialNetwork[]>>;
388
638
  /** Query the live public network. It does not import users or posts. */
389
639
  searchPublicProfiles(params: SearchPublicProfilesParams): Promise<SingleResponse<SocialProfilePage>>;
@@ -419,17 +669,113 @@ export declare class ProtocolsResource {
419
669
  limit?: number;
420
670
  cursor?: string;
421
671
  }): Promise<SingleResponse<SocialTrendFeed>>;
672
+ /** Prepare one exact shared reply preview. Keep the mutation ID across uncertain responses. */
673
+ prepareNativeReply(input: NativeReplyPrepareInput): Promise<SingleResponse<NativeReplyBatch>>;
674
+ /** Send only explicitly approved delivery IDs from this preview. Repeated submissions reconcile; they do not replay a write. */
675
+ submitNativeReply(id: string, input: {
676
+ approve: true;
677
+ deliveryIds: string[];
678
+ }): Promise<SingleResponse<NativeReplyBatch & {
679
+ errors: Record<string, {
680
+ code: string;
681
+ message: string;
682
+ }>;
683
+ }>>;
684
+ /** Read saved delivery results after refresh. This never publishes or contacts a source network. */
685
+ nativeReplyStatus(id: string): Promise<SingleResponse<NativeReplyBatch>>;
686
+ /** Page through this app user's saved delegated drafts/results. This never publishes, refreshes credentials or queries a source network. */
687
+ nativeReplyHistory(params?: {
688
+ limit?: number;
689
+ cursor?: string;
690
+ }): Promise<SingleResponse<NativeReplyHistory>>;
691
+ /** Review the current own posts before removal. A new preview replaces the previous approval. */
692
+ prepareNativeReplyRemoval(id: string, input: {
693
+ deliveryIds: string[];
694
+ }): Promise<SingleResponse<NativeReplyRemovalPreview>>;
695
+ /** Remove only selected, freshly reviewed destinations. Repeating approval never repeats a native delete. */
696
+ removeNativeReply(id: string, input: {
697
+ approve: true;
698
+ removals: Array<{
699
+ deliveryId: string;
700
+ removalId: string;
701
+ }>;
702
+ }): Promise<SingleResponse<NativeReplyBatch & {
703
+ errors: Record<string, {
704
+ code: string;
705
+ message: string;
706
+ }>;
707
+ }>>;
708
+ /** List this app user's delegated accounts. A connection is not approval to post. */
709
+ nativeSocialAccounts(): Promise<SingleResponse<{
710
+ enabled: boolean;
711
+ accounts: NativeSocialAccount[];
712
+ }>>;
713
+ /** Private source controls in this app, with bounded pagination. */
714
+ sourceControls(params?: {
715
+ limit?: number;
716
+ cursor?: string;
717
+ }): Promise<SingleResponse<{
718
+ enabled: boolean;
719
+ items: SocialSourceControl[];
720
+ cursor: string | null;
721
+ }>>;
722
+ /** Block/mute an actor or hide a post in Minds. This does not impersonate a native network block. */
723
+ setSourceControl(input: {
724
+ source: SocialSourceRef;
725
+ control: 'block' | 'mute' | 'hide';
726
+ enabled: boolean;
727
+ scope: 'minds';
728
+ }): Promise<SingleResponse<SocialSourceControlChange>>;
729
+ /** File a report in the app's existing moderation queue, using server-read public source evidence. */
730
+ reportSource(input: {
731
+ source: SocialSourceRef;
732
+ readRef?: string;
733
+ reason: string;
734
+ details?: string;
735
+ scope: 'minds';
736
+ }): Promise<SingleResponse<{
737
+ id: string;
738
+ status: string;
739
+ existing: boolean;
740
+ scope: 'minds';
741
+ }>>;
742
+ /** Open the returned authorization URL in the user's browser; never submit passwords or tokens. */
743
+ startNativeSocialConnection(input: {
744
+ protocol: NativeSocialProtocol;
745
+ identifier: string;
746
+ }): Promise<SingleResponse<{
747
+ protocol: NativeSocialProtocol;
748
+ authorizationUrl: string;
749
+ }>>;
750
+ /** Complete a single-use callback with the same signed-in app user. Remove its query from browser history; do not log codes. */
751
+ finishNativeSocialConnection(input: NativeSocialCallbackInput): Promise<SingleResponse<NativeSocialAccount>>;
752
+ /** Disable this app's saved authority even when the provider cannot acknowledge revocation. */
753
+ disconnectNativeSocialAccount(id: string): Promise<SingleResponse<{
754
+ disconnected: true;
755
+ providerRevocation: 'acknowledged' | 'unconfirmed';
756
+ }>>;
757
+ /** Current Minds-hosted ActivityPub state for this app user; never provisions. */
758
+ federationIdentity(): Promise<SingleResponse<FederationIdentityView>>;
759
+ /** Explicit opt-in; the platform provisions once and never rotates keys or re-enables a disabled identity on retry. */
760
+ enableFederationIdentity(): Promise<SingleResponse<FederationIdentityView>>;
761
+ /** Stops serving and signing for the identity; its stable URL and keys are retained by the platform. */
762
+ disableFederationIdentity(): Promise<SingleResponse<{
763
+ disabled: true;
764
+ identity: FederationIdentity;
765
+ }>>;
422
766
  /** Native writes are off unless enabled for this exact app; public readers are independent. */
423
767
  nativeNostrAccount(): Promise<SingleResponse<NativeNostrAccount>>;
424
768
  /** Prepare an exact event for review. A timeout never automatically creates another signing request. */
425
769
  prepareNativeNostr(id: string, input: NativeNostrPrepareInput): Promise<SingleResponse<NativeNostrPrepared>>;
426
- /** Pass only the public signed event. After a timeout, read the prepared postId (delete) or eventId (publish).
770
+ /** Pass only the public signed event. After a timeout, read the prepared postId (delete) or eventId (publish/reply/linked_post).
427
771
  * Any user-selected retry keeps the same intent/event. */
428
772
  submitNativeNostr(id: string, input: {
429
773
  intent: string;
430
774
  event: HostedNostrIdentityProof;
431
775
  approve: true;
432
776
  }): Promise<SingleResponse<NativeNostrReceipt>>;
777
+ /** Check only the event bound to this private saved result; never sign, prepare or resend it. */
778
+ nativeNostrReplyResult(id: string): Promise<SingleResponse<NativeNostrReplyResult>>;
433
779
  getNativeNostrPost(id: string, postId: string): Promise<SingleResponse<NativeNostrPostState>>;
434
780
  /** Save a private reading subscription after resolving the immutable native ID. */
435
781
  subscribeToProfile(input: {
@@ -471,6 +817,21 @@ export declare class ProtocolsResource {
471
817
  * unsupported deployed route) propagate; they are never an empty page.
472
818
  */
473
819
  listCandidates(params?: ListProtocolCandidatesParams): Promise<ProtocolCandidateListResponse>;
820
+ /**
821
+ * Run fail-closed screening on a candidate's current revision. Requires the
822
+ * app's content_intelligence_admin binding (404 otherwise) and the same
823
+ * project-admin authorization as listCandidates. A failed or unconfigured
824
+ * classifier records hold, never pass.
825
+ */
826
+ screenCandidate(id: string): Promise<SingleResponse<ProtocolQuarantinedCandidate>>;
827
+ /** Screening verdict history for a candidate, newest first. */
828
+ listCandidateVerdicts(id: string): Promise<SingleResponse<ProtocolCandidateVerdict[]>>;
829
+ /**
830
+ * Record a decision on the candidate's current revision. Approval needs a
831
+ * signed-in human project admin and a pass verdict on the current digest;
832
+ * an agent key may only reject or escalate; escalation needs a reason.
833
+ */
834
+ decideCandidate(id: string, input: DecideProtocolCandidateInput): Promise<SingleResponse<ProtocolCandidateDecisionResult>>;
474
835
  /**
475
836
  * Read one quarantined candidate under the same authorization as listCandidates.
476
837
  * Empty and URL dot-segment IDs reject with ValidationError before any request;
@@ -37,6 +37,7 @@ function hostedIdentityPath(id) {
37
37
  export class ProtocolsResource {
38
38
  client;
39
39
  subscriptionListeners = new Set();
40
+ sourceControlListeners = new Set();
40
41
  constructor(client) {
41
42
  this.client = client;
42
43
  }
@@ -54,6 +55,11 @@ export class ProtocolsResource {
54
55
  catch { /* Other mounted views still receive the change. */ }
55
56
  }
56
57
  }
58
+ /** Confirmed safety choices from this authenticated SDK instance. Unsubscribe when its account or view changes. */
59
+ onSourceControlChange(listener) {
60
+ this.sourceControlListeners.add(listener);
61
+ return () => { this.sourceControlListeners.delete(listener); };
62
+ }
57
63
  publicNetworks() {
58
64
  return this.client.get('/protocols/social/networks');
59
65
  }
@@ -90,6 +96,98 @@ export class ProtocolsResource {
90
96
  getPublicTrendFeed(params) {
91
97
  return this.client.get('/protocols/social/trend-feed', { protocol: params.protocol, topic: params.topic, limit: params.limit, cursor: params.cursor });
92
98
  }
99
+ /** Prepare one exact shared reply preview. Keep the mutation ID across uncertain responses. */
100
+ prepareNativeReply(input) {
101
+ return this.client.post('/protocols/social/replies/prepare', { clientMutationId: input.clientMutationId, text: input.text,
102
+ parent: { protocol: input.parent.protocol, nativeId: input.parent.nativeId,
103
+ ...(input.parent.readRef !== undefined ? { readRef: input.parent.readRef } : {}) }, accountIds: input.accountIds,
104
+ ...(input.linkedPostAccountIds !== undefined ? { linkedPostAccountIds: input.linkedPostAccountIds } : {}),
105
+ ...(input.recoverFrom !== undefined ? { recoverFrom: { batchId: input.recoverFrom.batchId, deliveryIds: input.recoverFrom.deliveryIds } } : {}) }, { retry: false });
106
+ }
107
+ /** Send only explicitly approved delivery IDs from this preview. Repeated submissions reconcile; they do not replay a write. */
108
+ submitNativeReply(id, input) {
109
+ hostedIdentityPath(id);
110
+ return this.client.post(`/protocols/social/replies/${encodeURIComponent(id)}/submit`, { approve: input.approve, deliveryIds: input.deliveryIds }, { retry: false });
111
+ }
112
+ /** Read saved delivery results after refresh. This never publishes or contacts a source network. */
113
+ nativeReplyStatus(id) {
114
+ hostedIdentityPath(id);
115
+ return this.client.get(`/protocols/social/replies/${encodeURIComponent(id)}`);
116
+ }
117
+ /** Page through this app user's saved delegated drafts/results. This never publishes, refreshes credentials or queries a source network. */
118
+ nativeReplyHistory(params = {}) {
119
+ return this.client.get('/protocols/social/replies', { limit: params.limit, cursor: params.cursor });
120
+ }
121
+ /** Review the current own posts before removal. A new preview replaces the previous approval. */
122
+ prepareNativeReplyRemoval(id, input) {
123
+ hostedIdentityPath(id);
124
+ return this.client.post(`/protocols/social/replies/${encodeURIComponent(id)}/removals/prepare`, { deliveryIds: input.deliveryIds }, { retry: false });
125
+ }
126
+ /** Remove only selected, freshly reviewed destinations. Repeating approval never repeats a native delete. */
127
+ removeNativeReply(id, input) {
128
+ hostedIdentityPath(id);
129
+ return this.client.post(`/protocols/social/replies/${encodeURIComponent(id)}/removals/submit`, { approve: input.approve,
130
+ removals: input.removals.map(value => ({ deliveryId: value.deliveryId, removalId: value.removalId })) }, { retry: false });
131
+ }
132
+ /** List this app user's delegated accounts. A connection is not approval to post. */
133
+ nativeSocialAccounts() {
134
+ return this.client.get('/protocols/social/accounts');
135
+ }
136
+ /** Private source controls in this app, with bounded pagination. */
137
+ sourceControls(params = {}) {
138
+ return this.client.get('/protocols/social/controls', { limit: params.limit, cursor: params.cursor });
139
+ }
140
+ /** Block/mute an actor or hide a post in Minds. This does not impersonate a native network block. */
141
+ async setSourceControl(input) {
142
+ const choice = { source: { protocol: input.source.protocol, kind: input.source.kind, nativeId: input.source.nativeId },
143
+ control: input.control, enabled: input.enabled, scope: input.scope };
144
+ const response = await this.client.post('/protocols/social/controls', choice, { retry: false });
145
+ const saved = response.data;
146
+ if (!saved || saved.scope !== choice.scope || saved.enabled !== choice.enabled || saved.control !== choice.control
147
+ || saved.source?.protocol !== choice.source.protocol || saved.source.kind !== choice.source.kind || saved.source.nativeId !== choice.source.nativeId) {
148
+ throw new ValidationError({ type: 'validation_error', code: 'source_control_unconfirmed', message: 'The saved safety choice could not be confirmed. Check your safety settings before trying again.' });
149
+ }
150
+ for (const listener of [...this.sourceControlListeners]) {
151
+ // A broken view must not turn a confirmed preference into a failed write.
152
+ try {
153
+ listener({ ...choice, source: { ...choice.source } });
154
+ }
155
+ catch { /* Continue notifying the other views. */ }
156
+ }
157
+ return response;
158
+ }
159
+ /** File a report in the app's existing moderation queue, using server-read public source evidence. */
160
+ reportSource(input) {
161
+ return this.client.post('/protocols/social/reports', { source: { protocol: input.source.protocol, kind: input.source.kind, nativeId: input.source.nativeId },
162
+ readRef: input.readRef, reason: input.reason, details: input.details, scope: input.scope }, { retry: false });
163
+ }
164
+ /** Open the returned authorization URL in the user's browser; never submit passwords or tokens. */
165
+ startNativeSocialConnection(input) {
166
+ return this.client.post('/protocols/social/oauth/start', { protocol: input.protocol, identifier: input.identifier }, { retry: false });
167
+ }
168
+ /** Complete a single-use callback with the same signed-in app user. Remove its query from browser history; do not log codes. */
169
+ finishNativeSocialConnection(input) {
170
+ const { protocol, state, code, iss, error } = input;
171
+ return this.client.post('/protocols/social/oauth/callback', { protocol, state,
172
+ ...(code !== undefined ? { code } : {}), ...(iss !== undefined ? { iss } : {}), ...(error !== undefined ? { error } : {}) }, { retry: false });
173
+ }
174
+ /** Disable this app's saved authority even when the provider cannot acknowledge revocation. */
175
+ disconnectNativeSocialAccount(id) {
176
+ hostedIdentityPath(id);
177
+ return this.client.delete(`/protocols/social/accounts/${encodeURIComponent(id)}`);
178
+ }
179
+ /** Current Minds-hosted ActivityPub state for this app user; never provisions. */
180
+ federationIdentity() {
181
+ return this.client.get('/protocols/social/federation/identity');
182
+ }
183
+ /** Explicit opt-in; the platform provisions once and never rotates keys or re-enables a disabled identity on retry. */
184
+ enableFederationIdentity() {
185
+ return this.client.post('/protocols/social/federation/identity', { consent: true }, { retry: false });
186
+ }
187
+ /** Stops serving and signing for the identity; its stable URL and keys are retained by the platform. */
188
+ disableFederationIdentity() {
189
+ return this.client.delete('/protocols/social/federation/identity');
190
+ }
93
191
  /** Native writes are off unless enabled for this exact app; public readers are independent. */
94
192
  nativeNostrAccount() {
95
193
  return this.client.get('/protocols/social/nostr/account');
@@ -97,10 +195,15 @@ export class ProtocolsResource {
97
195
  /** Prepare an exact event for review. A timeout never automatically creates another signing request. */
98
196
  prepareNativeNostr(id, input) {
99
197
  hostedIdentityPath(id);
100
- const body = input.action === 'publish' ? { action: input.action, content: input.content } : { action: input.action, postId: input.postId };
198
+ const body = input.action === 'publish' ? { action: input.action, content: input.content }
199
+ : input.action === 'reply' ? { action: input.action, content: input.content, parentId: input.parentId }
200
+ : input.action === 'linked_post' ? { action: input.action, content: input.content,
201
+ source: { protocol: input.source.protocol, nativeId: input.source.nativeId,
202
+ ...(input.source.readRef !== undefined ? { readRef: input.source.readRef } : {}) } }
203
+ : { action: input.action, postId: input.postId };
101
204
  return this.client.post(`/protocols/social/nostr/${encodeURIComponent(id)}/prepare`, body, { retry: false });
102
205
  }
103
- /** Pass only the public signed event. After a timeout, read the prepared postId (delete) or eventId (publish).
206
+ /** Pass only the public signed event. After a timeout, read the prepared postId (delete) or eventId (publish/reply/linked_post).
104
207
  * Any user-selected retry keeps the same intent/event. */
105
208
  submitNativeNostr(id, input) {
106
209
  hostedIdentityPath(id);
@@ -109,6 +212,11 @@ export class ProtocolsResource {
109
212
  intent: input.intent, event: { id: eventId, pubkey, sig, kind, content, created_at, tags }, approve: input.approve,
110
213
  }, { retry: false });
111
214
  }
215
+ /** Check only the event bound to this private saved result; never sign, prepare or resend it. */
216
+ nativeNostrReplyResult(id) {
217
+ hostedIdentityPath(id);
218
+ return this.client.get(`/protocols/social/nostr/replies/${encodeURIComponent(id)}`);
219
+ }
112
220
  getNativeNostrPost(id, postId) {
113
221
  hostedIdentityPath(id);
114
222
  if (!/^[0-9a-f]{64}$/.test(postId))
@@ -174,24 +282,39 @@ export class ProtocolsResource {
174
282
  listCandidates(params) {
175
283
  return this.client.get('/protocols/candidates', params ? {
176
284
  protocol: params.protocol,
285
+ state: params.state,
177
286
  limit: params.limit,
178
287
  cursor: params.cursor,
179
288
  } : undefined);
180
289
  }
290
+ /**
291
+ * Run fail-closed screening on a candidate's current revision. Requires the
292
+ * app's content_intelligence_admin binding (404 otherwise) and the same
293
+ * project-admin authorization as listCandidates. A failed or unconfigured
294
+ * classifier records hold, never pass.
295
+ */
296
+ async screenCandidate(id) {
297
+ return this.client.post(`/protocols/candidates/${encodeURIComponent(assertCandidateId(id))}/screen`, {});
298
+ }
299
+ /** Screening verdict history for a candidate, newest first. */
300
+ async listCandidateVerdicts(id) {
301
+ return this.client.get(`/protocols/candidates/${encodeURIComponent(assertCandidateId(id))}/verdicts`);
302
+ }
303
+ /**
304
+ * Record a decision on the candidate's current revision. Approval needs a
305
+ * signed-in human project admin and a pass verdict on the current digest;
306
+ * an agent key may only reject or escalate; escalation needs a reason.
307
+ */
308
+ async decideCandidate(id, input) {
309
+ return this.client.post(`/protocols/candidates/${encodeURIComponent(assertCandidateId(id))}/decision`, input);
310
+ }
181
311
  /**
182
312
  * Read one quarantined candidate under the same authorization as listCandidates.
183
313
  * Empty and URL dot-segment IDs reject with ValidationError before any request;
184
314
  * all other IDs are encoded as one path segment and validated by the API.
185
315
  */
186
316
  async getCandidate(id) {
187
- if (typeof id !== 'string' || !id.trim() || id === '.' || id === '..') {
188
- throw new ValidationError({
189
- type: 'validation_error',
190
- code: 'invalid_candidate_id',
191
- message: 'Candidate ID must be a non-empty identifier, not a URL dot segment.',
192
- });
193
- }
194
- return this.client.get(`/protocols/candidates/${encodeURIComponent(id)}`);
317
+ return this.client.get(`/protocols/candidates/${encodeURIComponent(assertCandidateId(id))}`);
195
318
  }
196
319
  /** Cross-protocol full-text search */
197
320
  search(params) {
@@ -210,3 +333,13 @@ export class ProtocolsResource {
210
333
  return this.client.put('/protocols/settings', input);
211
334
  }
212
335
  }
336
+ function assertCandidateId(id) {
337
+ if (typeof id !== 'string' || !id.trim() || id === '.' || id === '..') {
338
+ throw new ValidationError({
339
+ type: 'validation_error',
340
+ code: 'invalid_candidate_id',
341
+ message: 'Candidate ID must be a non-empty identifier, not a URL dot segment.',
342
+ });
343
+ }
344
+ return id;
345
+ }
@@ -1,9 +1,10 @@
1
1
  import type { HttpClient } from '../client.js';
2
2
  import type { PaginatedResponse, SingleResponse } from '../types.js';
3
- export type ReportTargetType = 'post' | 'user' | 'community' | 'message';
4
- export type ReportStatus = 'pending' | 'resolved' | 'dismissed';
3
+ export type ReportTargetType = 'post' | 'user' | 'community' | 'message' | 'source_actor' | 'source_post';
4
+ export type ReportStatus = 'pending' | 'in_review' | 'escalated' | 'resolved' | 'dismissed';
5
5
  export interface CreateReportInput {
6
- target_type: ReportTargetType;
6
+ /** External sources require protocols.reportSource so evidence is verified. */
7
+ target_type: Exclude<ReportTargetType, 'source_actor' | 'source_post'>;
7
8
  target_id: string;
8
9
  reason: string;
9
10
  details?: string;
package/llm.md CHANGED
@@ -619,8 +619,9 @@ if (subscription.active) {
619
619
  - `r.admin` — Admin operations (user management, stats, network settings); content reports: `listReports({ status? })`, `resolveReport(id)`, `dismissReport(id)` (project-bound keys see only their app's reports)
620
620
  - `r.inviteCodes` / `r.inviteCodesAdmin` — Invite code management
621
621
  - `r.organizationSettings` / `r.organizationSecurity` — Org configuration
622
- - `r.protocols` — 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`.
623
- - 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.
622
+ - 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, 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 and exact `wireText`; connection is not publication approval. `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.
623
+ - `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. 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`.
624
+ - 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.
624
625
  - 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.
625
626
  - `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.
626
627
  - 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`.
@@ -795,3 +796,12 @@ catch (e) { if (e.status === 404) { /* private or doesn't exist */ } }
795
796
  9. **`r.memory` rejects secrets.** Facts containing patterns like `sk_`, `api_key=`, `password=`, or private keys are rejected on write. Store secrets in environment variables, not memory.
796
797
  10. **Memory is network-scoped.** Facts and decisions are isolated per network (tenant). Two teams on different networks cannot see each other's memory.
797
798
  11. **`r.memory.context()` has a token budget.** Default is 2000 tokens (~8000 chars). Pass `max_tokens` to control how much context is returned.
799
+
800
+ Nostr replies and linked posts are saved in the same `protocols.nativeReplyHistory()`
801
+ feed after signed submission. Items with `type: 'nostr'` use
802
+ `protocols.nativeNostrReplyResult(id)` for a read-only check of the saved event and
803
+ its author's deletion requests on the recorded relay. Other history items use
804
+ `nativeReplyStatus(id)`. Do not turn an unknown result into a fresh preparation or
805
+ publication; history stores no signature, signing key or intent. Keep the acting
806
+ user/app-bound result IDs and discard UI state after an account switch. The last
807
+ read-back observation is separate from retained publication/deletion evidence.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@recursiv/sdk",
3
- "version": "0.7.26",
3
+ "version": "0.7.28",
4
4
  "description": "TypeScript SDK for the Recursiv API",
5
5
  "license": "FSL-1.1-ALv2",
6
6
  "type": "module",