@recursiv/sdk 0.7.83 → 0.7.89

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.
Files changed (38) hide show
  1. package/README.md +85 -1
  2. package/dist/assistant/index.d.ts +11 -3
  3. package/dist/assistant/index.js +11 -3
  4. package/dist/assistant/render.d.ts +25 -2
  5. package/dist/assistant/render.js +197 -16
  6. package/dist/assistant/subjectBundle.js +20 -3
  7. package/dist/assistant/turnCaps.d.ts +3 -0
  8. package/dist/assistant/turnCaps.js +141 -0
  9. package/dist/assistant/types.d.ts +223 -7
  10. package/dist/assistant/types.js +15 -0
  11. package/dist/index.d.ts +10 -4
  12. package/dist/index.js +7 -1
  13. package/dist/privacy/facts.generated.d.ts +79 -0
  14. package/dist/privacy/facts.generated.js +93 -0
  15. package/dist/privacy/index.d.ts +48 -0
  16. package/dist/privacy/index.js +42 -0
  17. package/dist/resources/discovery-merge.d.ts +17 -1
  18. package/dist/resources/discovery-merge.js +84 -4
  19. package/dist/resources/discovery-progressive.d.ts +4 -3
  20. package/dist/resources/discovery-progressive.js +11 -8
  21. package/dist/resources/discovery.d.ts +9 -1
  22. package/dist/resources/discovery.js +62 -15
  23. package/dist/resources/feeds.d.ts +15 -6
  24. package/dist/resources/feeds.js +2 -1
  25. package/dist/resources/integrations.d.ts +3 -0
  26. package/dist/resources/privacy.d.ts +13 -0
  27. package/dist/resources/privacy.js +14 -0
  28. package/dist/resources/protocols.d.ts +99 -11
  29. package/dist/resources/protocols.js +45 -0
  30. package/dist/resources/sealed.d.ts +127 -7
  31. package/dist/resources/sealed.js +33 -6
  32. package/dist/resources/settings.d.ts +3 -1
  33. package/dist/resources/settings.js +3 -1
  34. package/dist/social-protocol-ids.d.ts +28 -0
  35. package/dist/social-protocol-ids.js +30 -0
  36. package/dist/types.d.ts +37 -0
  37. package/llm.md +16 -5
  38. package/package.json +6 -1
@@ -1,4 +1,5 @@
1
1
  import type { HttpClient } from '../client.js';
2
+ import { type SocialProtocolId } from '../social-protocol-ids.js';
2
3
  import type { CompassFilterMeta, StoryOutlets } from '../types.js';
3
4
  /**
4
5
  * The top of a feed: what the member (or a visitor) is most likely to engage with, rotated by what they have seen.
@@ -8,9 +9,17 @@ import type { CompassFilterMeta, StoryOutlets } from '../types.js';
8
9
  * Every tap target is in the app: a Minds post by id, an open-network post by `/social/post/<protocol>/<id>`, a story
9
10
  * by id. Nothing here is a URL on another network.
10
11
  */
11
- /** The open networks whose lanes a top carries. */
12
- export type FeedTopNetwork = 'bluesky' | 'activitypub' | 'nostr';
12
+ /**
13
+ * The open networks whose lanes a top may carry: every SOCIAL_PROTOCOL_IDS network (0.7.87: Farcaster before any top
14
+ * carries a Farcaster lane or item).
15
+ */
16
+ export type FeedTopNetwork = SocialProtocolId;
13
17
  export declare const FEED_TOP_NETWORKS: readonly FeedTopNetwork[];
18
+ /**
19
+ * A top's lanes, or their states, by network: Bluesky, ActivityPub and Nostr are always named; a network listed later
20
+ * (Farcaster) only once the API carries its lane, so read `networks.farcaster ?? []`.
21
+ */
22
+ export type FeedTopNetworkMap<T> = Record<'bluesky' | 'activitypub' | 'nostr', T> & Partial<Record<FeedTopNetwork, T>>;
14
23
  /** Where a tap on an item goes, always in the app. `path` is the app's own page of a network post, never its URL there. */
15
24
  export type FeedTopTarget = {
16
25
  kind: 'post';
@@ -103,7 +112,7 @@ export interface FeedStory {
103
112
  * are politics. Hide politics (`compass: 'hide'`) leaves such a story out. Absent from a server older than 0.7.67.
104
113
  */
105
114
  political?: boolean;
106
- sources: Partial<Record<'outlet' | 'hackernews' | 'bluesky' | 'activitypub' | 'nostr' | 'minds', number>>;
115
+ sources: Partial<Record<'outlet' | 'hackernews' | SocialProtocolId | 'minds', number>>;
107
116
  /** How many outlets reported it (StoryOutlets): the number every surface names for the story. Absent from a server older than 0.7.71. */
108
117
  outlets?: StoryOutlets;
109
118
  media: {
@@ -141,13 +150,13 @@ export interface FeedTopMeta {
141
150
  /** 'safe' for a member on safe defaults, 'open' for one who turned them off. */
142
151
  safety: 'safe' | 'open';
143
152
  mindsSource: 'member' | 'anonymous' | 'none' | 'following';
144
- networks: Record<FeedTopNetwork, 'ok' | 'unavailable'> | null;
153
+ networks: FeedTopNetworkMap<'ok' | 'unavailable'> | null;
145
154
  compass_filter?: CompassFilterMeta;
146
155
  }
147
156
  /** GET /feeds/top. `networks` is null on a feed with no lanes (Following, a story); `lead` is null when the feed has no story. */
148
157
  export interface FeedTopResponse {
149
158
  items: FeedTopItem[];
150
- networks: Record<FeedTopNetwork, FeedTopNetworkEntry[]> | null;
159
+ networks: FeedTopNetworkMap<FeedTopNetworkEntry[]> | null;
151
160
  lead: FeedStory | null;
152
161
  meta: FeedTopMeta;
153
162
  }
@@ -179,7 +188,7 @@ export interface PublicFeedTopMeta {
179
188
  /** A visitor's top (`feeds.publicTop`): safe items only, the same for every visitor in one 15-minute bucket. */
180
189
  export interface PublicFeedTopResponse {
181
190
  items: FeedTopItem[];
182
- networks: Record<FeedTopNetwork, FeedTopNetworkEntry[]> | null;
191
+ networks: FeedTopNetworkMap<FeedTopNetworkEntry[]> | null;
183
192
  lead: FeedStory | null;
184
193
  meta: PublicFeedTopMeta;
185
194
  }
@@ -1,5 +1,6 @@
1
1
  import { NotFoundError, RecursivError } from '../errors.js';
2
- export const FEED_TOP_NETWORKS = ['bluesky', 'activitypub', 'nostr'];
2
+ import { SOCIAL_PROTOCOL_IDS } from '../social-protocol-ids.js';
3
+ export const FEED_TOP_NETWORKS = SOCIAL_PROTOCOL_IDS;
3
4
  export class FeedsResource {
4
5
  http;
5
6
  constructor(http) {
@@ -90,6 +90,9 @@ export interface UpdateAgentIntegrationInput {
90
90
  allowed_tools?: string[];
91
91
  }
92
92
  export interface PendingToolExecution {
93
+ /** First-party builder identity is supplied by the server, never inferred from tool name. */
94
+ builder_approval?: true;
95
+ details_withheld?: boolean;
93
96
  id: string;
94
97
  conversation_id: string;
95
98
  agent: {
@@ -0,0 +1,13 @@
1
+ import type { HttpClient } from '../client.js';
2
+ import type { PrivacyCapabilities } from '../privacy/index.js';
3
+ import type { SingleResponse } from '../types.js';
4
+ /**
5
+ * What the deployed API enforces. Pair it with `@recursiv/sdk/privacy`, which says what each fact means and whether
6
+ * the installed SDK marks it enforced: render a privacy sentence only when both do (`isFactLive`).
7
+ */
8
+ export declare class PrivacyResource {
9
+ private client;
10
+ constructor(client: HttpClient);
11
+ /** GET /privacy/capabilities: the platform facts this deployment's build enforces, and its commit. Any API key. */
12
+ capabilities(): Promise<SingleResponse<PrivacyCapabilities>>;
13
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * What the deployed API enforces. Pair it with `@recursiv/sdk/privacy`, which says what each fact means and whether
3
+ * the installed SDK marks it enforced: render a privacy sentence only when both do (`isFactLive`).
4
+ */
5
+ export class PrivacyResource {
6
+ client;
7
+ constructor(client) {
8
+ this.client = client;
9
+ }
10
+ /** GET /privacy/capabilities: the platform facts this deployment's build enforces, and its commit. Any API key. */
11
+ capabilities() {
12
+ return this.client.get('/privacy/capabilities');
13
+ }
14
+ }
@@ -1,6 +1,7 @@
1
1
  import type { HttpClient } from '../client.js';
2
2
  import type { CompassFilterMeta, DiscoveryLayout, PaginatedResponse, PaginationParams, SingleResponse, StoryOutlets } from '../types.js';
3
3
  import type { CuratorStoryPostsPage } from './curator.js';
4
+ import type { NativeWriteProtocol, SocialProtocolId } from '../social-protocol-ids.js';
4
5
  /** Canonical protocol operation names returned by the adapter registry. */
5
6
  export declare const PROTOCOL_CAPABILITY_OPERATIONS: readonly ["discover", "stream", "backfill", "search", "resolveIdentity", "fetchProfile", "fetchThread", "publish", "reply", "react", "repost", "follow", "delete", "health", "checkpoint"];
6
7
  export declare const PROTOCOL_CAPABILITY_STATUSES: readonly ["supported", "planned", "unsupported"];
@@ -426,8 +427,55 @@ export interface FederationIdentityView {
426
427
  origin: string | null;
427
428
  identity: FederationIdentity | null;
428
429
  }
430
+ /** A native network every eligible member is on by rule (Farcaster joins later). */
431
+ export type NativeNetwork = 'activitypub' | 'atproto' | 'nostr';
432
+ /**
433
+ * A member's state on one native network. `ready`: on by rule, set up on their first public post, first interaction
434
+ * there, or `activateNetwork` ("Will be set up when you first post or interact"). `pending`: being set up, or waiting
435
+ * for the setup budget (`retryAt`). `off`: the member turned it off (`reason: 'opted_out'`), it is not available to them
436
+ * yet (`not_rolled_out`), or their profile is private (`private_profile`, AT Protocol and Nostr only; `setNetwork`
437
+ * with `enabled: true` turns it on). `needs_action`: the member must act first (`age_band_required`,
438
+ * `needs_verified_email`). `locked`: held off by a rule the member cannot change.
439
+ */
440
+ export type NativeNetworkState = 'ready' | 'on' | 'pending' | 'off' | 'error' | 'locked' | 'needs_action';
441
+ export interface NativeNetworkPresence {
442
+ network: NativeNetwork;
443
+ state: NativeNetworkState;
444
+ /** Why, as a code: `opted_out`, `not_rolled_out`, `private_profile`, `age_band_required`, `needs_verified_email`, `activation_failed`... */
445
+ reason: string | null;
446
+ /** `hosted` (a Minds-run ActivityPub actor or AT Protocol account), `managed` (a Minds-held Nostr key) or `attached`. */
447
+ mode: 'hosted' | 'managed' | 'attached';
448
+ /** The member's address on the network, set up or not, when the server can say it. */
449
+ handle: string | null;
450
+ /** Which identity a one-tap like, repost, reply or follow uses when the member also linked an outside account. */
451
+ engageDefault: 'native' | 'linked';
452
+ activatedAt: string | null;
453
+ optedOutAt: string | null;
454
+ removePast: boolean;
455
+ }
456
+ export interface NativeNetworkActivation {
457
+ outcome: 'activated' | 'already_on' | 'pending' | 'in_flight' | 'needs_action' | 'error' | 'not_eligible' | 'refused' | 'unavailable';
458
+ presence: NativeNetworkPresence;
459
+ /** When the setup budget frees, for `pending`. */
460
+ retryAt: string | null;
461
+ }
462
+ export interface NativeNetworkOff {
463
+ outcome: 'off';
464
+ presence: NativeNetworkPresence;
465
+ /** `failed`: the member is off, but removing what was shared did not start; call `setNetwork` again to retry it. */
466
+ retraction: 'queued' | 'failed' | 'none';
467
+ }
468
+ export interface SetNativeNetworkInput {
469
+ enabled: boolean;
470
+ /** Turning off: also remove what was already shared there (the Settings sheet checks this by default). */
471
+ removePast?: boolean;
472
+ }
429
473
  export interface SocialSourceRef {
430
- protocol: 'bluesky' | 'activitypub' | 'nostr';
474
+ /**
475
+ * 0.7.87: any SOCIAL_PROTOCOL_IDS network, Farcaster (an FID, or a `<fid>:0x<hash>` CastId) before its reader serves a
476
+ * post. A source to show, mute, hide or report; a native write takes only a NativeWriteProtocol network.
477
+ */
478
+ protocol: SocialProtocolId;
431
479
  kind: 'actor' | 'post';
432
480
  nativeId: string;
433
481
  }
@@ -474,7 +522,7 @@ export interface NativeReplyPrepareInput {
474
522
  clientMutationId: string;
475
523
  text: string;
476
524
  parent: {
477
- protocol: SocialSourceRef['protocol'];
525
+ protocol: NativeWriteProtocol;
478
526
  nativeId: string;
479
527
  readRef?: string;
480
528
  };
@@ -491,7 +539,7 @@ export interface NativeReplyPrepareInput {
491
539
  }
492
540
  export interface NativeFollowSourceInput {
493
541
  accountId: string;
494
- protocol: SocialSourceRef['protocol'];
542
+ protocol: NativeWriteProtocol;
495
543
  nativeId: string;
496
544
  }
497
545
  export interface NativeFollowPrepareInput extends NativeFollowSourceInput {
@@ -536,7 +584,7 @@ export interface NativeFollowState extends NativeFollowReceipt {
536
584
  /** One connected account on the source's network, as the account lists describe it. */
537
585
  export interface NativeFollowAccount {
538
586
  id: string;
539
- protocol: SocialSourceRef['protocol'];
587
+ protocol: NativeWriteProtocol;
540
588
  nativeId: string;
541
589
  handle: string;
542
590
  serverUrl: string;
@@ -545,7 +593,7 @@ export interface NativeFollowAccount {
545
593
  }
546
594
  /** Every connected account's relationship with one source, from one read. */
547
595
  export interface NativeFollowStates {
548
- protocol: SocialSourceRef['protocol'];
596
+ protocol: NativeWriteProtocol;
549
597
  nativeId: string;
550
598
  /** False when native follows are off for this app; accounts are still listed, and no state is read. */
551
599
  enabled: boolean;
@@ -578,7 +626,7 @@ export interface NativeFollowReview {
578
626
  }
579
627
  export interface NativeLikeSourceInput {
580
628
  accountId: string;
581
- protocol: SocialSourceRef['protocol'];
629
+ protocol: NativeWriteProtocol;
582
630
  nativeId: string;
583
631
  }
584
632
  export interface NativeLikePrepareInput extends NativeLikeSourceInput {
@@ -644,7 +692,7 @@ export interface NativeLikeReview {
644
692
  }
645
693
  export interface NativeRepostSourceInput {
646
694
  accountId: string;
647
- protocol: SocialSourceRef['protocol'];
695
+ protocol: NativeWriteProtocol;
648
696
  nativeId: string;
649
697
  }
650
698
  export interface NativeRepostPrepareInput extends NativeRepostSourceInput {
@@ -773,7 +821,7 @@ export interface NativeReplyBatch {
773
821
  serverUrl: string;
774
822
  } | null;
775
823
  parent: {
776
- protocol: SocialSourceRef['protocol'];
824
+ protocol: NativeWriteProtocol;
777
825
  nativeId: string;
778
826
  sourceUrl: string;
779
827
  authorId: string;
@@ -910,7 +958,7 @@ export interface NativeNostrReplyResult {
910
958
  text: string;
911
959
  /** `readRef`: a linked ActivityPub source's public reader id, for the conversation read (replies saved before it existed have none). */
912
960
  source: {
913
- protocol: SocialSourceRef['protocol'];
961
+ protocol: NativeWriteProtocol;
914
962
  nativeId: string;
915
963
  sourceUrl: string;
916
964
  readRef?: string;
@@ -1321,7 +1369,8 @@ export interface SocialTrendFeed extends SocialTrendProvenance {
1321
1369
  };
1322
1370
  }
1323
1371
  /** A member of a radar cluster: a headline or excerpt and a link only, never a body. */
1324
- export type SocialRadarSourceKind = 'outlet' | 'hackernews' | 'bluesky' | 'activitypub' | 'nostr' | 'minds';
1372
+ /** 0.7.87: an open-network member's kind is any SOCIAL_PROTOCOL_IDS network, Farcaster before the radar holds any. */
1373
+ export type SocialRadarSourceKind = 'outlet' | 'hackernews' | SocialProtocolId | 'minds';
1325
1374
  export interface SocialRadarMember {
1326
1375
  kind: SocialRadarSourceKind;
1327
1376
  /** outlet: the article url; hackernews: the `hn:<id>` candidate id; social: JSON `[protocol, nativeId]`; minds: the post id. */
@@ -1748,12 +1797,27 @@ export interface SocialConversation {
1748
1797
  hiddenByViewer?: true;
1749
1798
  viewerControls?: Array<Pick<SocialSourceControl, 'source' | 'control'>>;
1750
1799
  }
1800
+ /**
1801
+ * A followed account's status on a Following page, in the server's order (SocialFollowingFeed.ts
1802
+ * FOLLOWING_SOURCE_STATUSES). `late` (added after 0.7.82): the account's read had not answered at the page's deadline;
1803
+ * it is read again on a later page and is never a failure. APIs before it name such an account `unavailable` with
1804
+ * `late: true`.
1805
+ */
1806
+ export declare const SOCIAL_FOLLOWING_SOURCE_STATUSES: readonly ["ok", "late", "unavailable", "rate_limited", "unsupported_protocol"];
1807
+ export type SocialFollowingSourceStatus = typeof SOCIAL_FOLLOWING_SOURCE_STATUSES[number];
1751
1808
  export interface SocialFollowingSource {
1752
1809
  subscriptionId: string;
1753
1810
  protocol: string;
1754
1811
  nativeId: string;
1755
- status: 'ok' | 'unavailable' | 'rate_limited' | 'unsupported_protocol';
1812
+ /**
1813
+ * `late`: the account's read has not answered yet; it is read again on a later page. `ok` with `late: true`: the
1814
+ * account's last-held first page, served while its fresh read goes on. `unavailable` is a read that failed, or one
1815
+ * still late after its reads again.
1816
+ */
1817
+ status: SocialFollowingSourceStatus;
1756
1818
  moreAvailable: boolean | null;
1819
+ /** The account is read again on a later page: never show it as a failure; read the next page for its posts. */
1820
+ late?: true;
1757
1821
  }
1758
1822
  /** A bounded batch of recent author windows. Continue the cursor to read more followed accounts. */
1759
1823
  export interface SocialFollowingFeed {
@@ -1764,6 +1828,10 @@ export interface SocialFollowingFeed {
1764
1828
  totalAccounts: number;
1765
1829
  postsPerAccount: number;
1766
1830
  coverage: 'recent_original_posts';
1831
+ /** The networks with an account named late on this page (absent from older APIs). */
1832
+ lateNetworks?: string[];
1833
+ /** How long this page waited for an account's first read on each network, in ms (absent from older APIs). */
1834
+ deadlineMs?: number;
1767
1835
  }
1768
1836
  export declare class ProtocolsResource {
1769
1837
  private client;
@@ -2134,6 +2202,26 @@ export declare class ProtocolsResource {
2134
2202
  disconnected: true;
2135
2203
  providerRevocation: 'acknowledged' | 'unconfirmed';
2136
2204
  }>>;
2205
+ /**
2206
+ * The signed-in member's native networks in the key's app (ActivityPub, AT Protocol, Nostr): each one's state, reason
2207
+ * and address. Read-only: it never sets anything up. Needs `users:read`.
2208
+ */
2209
+ networks(): Promise<SingleResponse<{
2210
+ networks: NativeNetworkPresence[];
2211
+ }>>;
2212
+ /**
2213
+ * "Set up now": sets up a network that is `ready` (or `pending`, or failed) at once, instead of on the member's first
2214
+ * post. Never undoes their off switch (409 `network_off`) or a private profile's rule (409 `private_profile`): use
2215
+ * `setNetwork` for those. Over the setup budget it answers `pending` with `retryAt`. Needs `users:write`.
2216
+ */
2217
+ activateNetwork(network: NativeNetwork): Promise<SingleResponse<NativeNetworkActivation>>;
2218
+ /**
2219
+ * The member's switch for one network. `{ enabled: false, removePast: true }` turns it off and removes what was
2220
+ * shared there (reversible: turning it back on serves the same identity; remote followers may need to follow again).
2221
+ * `{ enabled: true }` turns it back on or sets it up. Refusals: 403 `network_locked`, 409 `not_rolled_out` or
2222
+ * `age_band_required`. Needs `users:write`.
2223
+ */
2224
+ setNetwork(network: NativeNetwork, input: SetNativeNetworkInput): Promise<SingleResponse<NativeNetworkActivation | NativeNetworkOff>>;
2137
2225
  /** Current Minds-hosted ActivityPub state for this app user; never provisions. */
2138
2226
  federationIdentity(): Promise<SingleResponse<FederationIdentityView>>;
2139
2227
  /** Explicit opt-in; the platform provisions once and never rotates keys or re-enables a disabled identity on retry. */
@@ -28,6 +28,13 @@ export const PROTOCOL_ADAPTER_MATURITIES = Object.freeze([
28
28
  'beta',
29
29
  'stable',
30
30
  ]);
31
+ const NATIVE_NETWORKS = ['activitypub', 'atproto', 'nostr'];
32
+ function nativeNetworkPath(network) {
33
+ if (!NATIVE_NETWORKS.includes(network)) {
34
+ throw new ValidationError({ type: 'validation_error', code: 'invalid_network', message: 'network must be activitypub, atproto or nostr' });
35
+ }
36
+ return `/protocols/networks/${network}`;
37
+ }
31
38
  function hostedIdentityPath(id) {
32
39
  if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id)) {
33
40
  throw new ValidationError({ type: 'validation_error', code: 'invalid_identity_id', message: 'A valid hosted identity connection ID is required' });
@@ -42,6 +49,19 @@ function hostedIdentityPath(id) {
42
49
  export function viralItemHasVideo(item) {
43
50
  return item.post.media.some(media => media.type === 'video');
44
51
  }
52
+ /**
53
+ * A followed account's status on a Following page, in the server's order (SocialFollowingFeed.ts
54
+ * FOLLOWING_SOURCE_STATUSES). `late` (added after 0.7.82): the account's read had not answered at the page's deadline;
55
+ * it is read again on a later page and is never a failure. APIs before it name such an account `unavailable` with
56
+ * `late: true`.
57
+ */
58
+ export const SOCIAL_FOLLOWING_SOURCE_STATUSES = Object.freeze([
59
+ 'ok',
60
+ 'late',
61
+ 'unavailable',
62
+ 'rate_limited',
63
+ 'unsupported_protocol',
64
+ ]);
45
65
  /** Only the radar's own fields travel; caller scope or destination extras never reach the query string. */
46
66
  /** Forwarded only when given, so a default read makes exactly the call it always made. */
47
67
  const readOptions = (options) => options ? [options] : [];
@@ -488,6 +508,31 @@ export class ProtocolsResource {
488
508
  hostedIdentityPath(id);
489
509
  return this.client.delete(`/protocols/social/accounts/${encodeURIComponent(id)}`);
490
510
  }
511
+ /**
512
+ * The signed-in member's native networks in the key's app (ActivityPub, AT Protocol, Nostr): each one's state, reason
513
+ * and address. Read-only: it never sets anything up. Needs `users:read`.
514
+ */
515
+ networks() {
516
+ return this.client.get('/protocols/networks');
517
+ }
518
+ /**
519
+ * "Set up now": sets up a network that is `ready` (or `pending`, or failed) at once, instead of on the member's first
520
+ * post. Never undoes their off switch (409 `network_off`) or a private profile's rule (409 `private_profile`): use
521
+ * `setNetwork` for those. Over the setup budget it answers `pending` with `retryAt`. Needs `users:write`.
522
+ */
523
+ activateNetwork(network) {
524
+ return this.client.post(`${nativeNetworkPath(network)}/activate`, {});
525
+ }
526
+ /**
527
+ * The member's switch for one network. `{ enabled: false, removePast: true }` turns it off and removes what was
528
+ * shared there (reversible: turning it back on serves the same identity; remote followers may need to follow again).
529
+ * `{ enabled: true }` turns it back on or sets it up. Refusals: 403 `network_locked`, 409 `not_rolled_out` or
530
+ * `age_band_required`. Needs `users:write`.
531
+ */
532
+ setNetwork(network, input) {
533
+ const body = input.enabled ? { enabled: true } : { enabled: false, ...(input.removePast !== undefined ? { remove_past: input.removePast } : {}) };
534
+ return this.client.put(nativeNetworkPath(network), body);
535
+ }
491
536
  /** Current Minds-hosted ActivityPub state for this app user; never provisions. */
492
537
  federationIdentity() {
493
538
  return this.client.get('/protocols/social/federation/identity');
@@ -25,6 +25,12 @@ export interface SealedTarget {
25
25
  /** Exact origin, e.g. `https://router-0.tinfoil.sh`. */
26
26
  origin: string;
27
27
  kind: SealedTargetKind;
28
+ /**
29
+ * Web search works through this target: seal a message that may search only to a target that says true. Set on
30
+ * `SealedConfig.targets` only, never in a model's own list (look the origin up there). Absent (an older gateway)
31
+ * means no.
32
+ */
33
+ search?: boolean;
28
34
  }
29
35
  /** A model the app offers in Sealed. */
30
36
  export interface SealedModel {
@@ -36,6 +42,8 @@ export interface SealedModel {
36
42
  * when one has moved to another target, so read it per model.
37
43
  */
38
44
  targets: SealedTarget[];
45
+ /** The app may send pictures to this model. Absent (an older gateway) means no. */
46
+ vision?: boolean;
39
47
  }
40
48
  /** What `config()` returns. */
41
49
  export interface SealedConfig {
@@ -45,11 +53,73 @@ export interface SealedConfig {
45
53
  attestation_path: string;
46
54
  /** Every target the gateway forwards to, chat and attestation alike. */
47
55
  targets: SealedTarget[];
48
- /** The models the app offers in Sealed, each with the targets that serve it. */
56
+ /** The models the app offers to chat with in Sealed, each with the targets that serve it. */
49
57
  models: SealedModel[];
58
+ /**
59
+ * The models the app's sealed checks run on (a safeguard model for the message and answer checks, a vision model for
60
+ * the picture check), each with the targets that serve it. Never offer one of these as a chat model unless `models`
61
+ * lists it too. Send a check with `send({ purpose: 'check' })`. Absent on an older gateway.
62
+ */
63
+ checks?: SealedModel[];
64
+ /**
65
+ * Server switch (George plan G7a): web search through the router. Send a message that may search with
66
+ * `send({ search: true })`, which marks it `X-Sealed-Search: 1`; the gateway refuses a marked message while this is
67
+ * off (403 `sealed_search_off`), and pauses the member's day when an unmarked one searches. Off unless the gateway
68
+ * says true: treat it absent, or a config you could not read, as off.
69
+ */
70
+ search?: boolean;
71
+ /** Server switch (G7a): pictures from the phone, off unless true. Post pictures need only the model's `vision`. */
72
+ phone_pictures?: boolean;
73
+ /**
74
+ * Server switch: on-phone memory, off unless true. The memory lives on the phone and the gateway never sees it; this
75
+ * is the server's switch for the feature, so it can be turned off for everyone at once. Absent (an older gateway), or
76
+ * a config you could not read, means off.
77
+ */
78
+ memory?: boolean;
50
79
  /** The attestation policy version the app's verifier must pin; an older pinned policy means the app needs updating. */
51
80
  verifier_policy_version: number;
81
+ /** The member's allowance: its daily numbers are their plan's (`plan`). */
52
82
  allowance: SealedAllowance;
83
+ /** The plan the daily numbers in `allowance` follow. Free when the gateway could not read it. Absent on an older gateway. */
84
+ plan?: SealedPlan;
85
+ /** False when the gateway could not read the member's plan and applied Free's numbers: do not tell them they are on Free. */
86
+ plan_known?: boolean;
87
+ /** What is left of each daily allowance now; `checks` only while checks count apart. Absent when it could not be read. */
88
+ remaining_today?: {
89
+ requests: number;
90
+ searches: number;
91
+ checks?: number;
92
+ };
93
+ /**
94
+ * Every plan's daily numbers, so the app can say truthfully whether a plan gives more. It lists plans the gateway
95
+ * knows, Max among them, whether or not anyone sells them: offer only the plans the app sells.
96
+ */
97
+ plan_allowances?: Record<SealedPlan, {
98
+ requests_per_day: number;
99
+ searches_per_day: number;
100
+ checks_per_day?: number;
101
+ }>;
102
+ /**
103
+ * `budget` while the app's monthly Sealed budget is used up (sends get 503 `sealed_paused_budget` until the next UTC
104
+ * month), `unavailable` while the gateway cannot tell (sends get 503 `sealed_unavailable`), null otherwise.
105
+ */
106
+ paused?: 'budget' | 'unavailable' | null;
107
+ }
108
+ /** The plans the gateway knows, lowest first. */
109
+ export type SealedPlan = 'free' | 'plus' | 'pro' | 'max';
110
+ /**
111
+ * `details` on a refused send whose allowance ran out (RateLimitError.details): which limit, the member's plan, whether
112
+ * that plan was read (`plan_known`; false: Free's numbers were applied because the read failed), that plan's daily
113
+ * number (null for `rate`, the per-minute and open-request caps no plan changes), and the plans that give more of it
114
+ * (empty for `rate` and while the plan is not known). Say "upgrade for more" only when a plan the app sells is in
115
+ * `higher_plans`.
116
+ */
117
+ export interface SealedQuotaDetails {
118
+ limit: 'messages' | 'searches' | 'checks' | 'rate';
119
+ plan: SealedPlan;
120
+ plan_known: boolean;
121
+ per_day: number | null;
122
+ higher_plans: SealedPlan[];
53
123
  }
54
124
  export interface SealedAllowance {
55
125
  requests_per_day: number;
@@ -57,7 +127,25 @@ export interface SealedAllowance {
57
127
  max_concurrent: number;
58
128
  /** The cap on one sealed request body, in bytes. */
59
129
  max_request_bytes: number;
130
+ /**
131
+ * Checks (`send({ purpose: 'check' })`) per UTC day, counted instead of `requests_per_day`. Absent while the gateway
132
+ * counts checks as messages (its check allowance is off), and on an older gateway: a check then spends a message.
133
+ */
134
+ checks_per_day?: number;
135
+ /** Checks per minute, counted instead of `requests_per_minute`. Absent when `checks_per_day` is. */
136
+ checks_per_minute?: number;
137
+ /**
138
+ * Messages marked as searches per UTC day. A marked search spends a search and no message; a gateway from before plans
139
+ * counted it against `requests_per_day` as well.
140
+ */
141
+ searches_per_day?: number;
60
142
  }
143
+ /**
144
+ * What a sealed request is for, sent as `X-Sealed-Purpose`, which the gateway reads and never forwards. `check`: one of
145
+ * the app's sealed checks, counted against the check allowance instead of the message allowance while the gateway has
146
+ * one (`config().allowance.checks_per_day`), and as a message otherwise.
147
+ */
148
+ export type SealedPurpose = 'check';
61
149
  export interface SealedSendInput {
62
150
  /** The router the app verified and sealed to: the origin of a `router` target from `config()`, exactly. */
63
151
  enclaveOrigin: string;
@@ -67,6 +155,19 @@ export interface SealedSendInput {
67
155
  body: Uint8Array | ArrayBuffer | ReadableStream<Uint8Array>;
68
156
  /** Ask for a streamed (server-sent events) answer. */
69
157
  stream?: boolean;
158
+ /**
159
+ * `check` for one of the app's sealed checks (a model from `config().checks`): sent as `X-Sealed-Purpose: check`, so
160
+ * it counts against the check allowance while the gateway lists one. Omit it for a message.
161
+ */
162
+ purpose?: SealedPurpose;
163
+ /**
164
+ * True for a message that may search the web: sent as `X-Sealed-Search: 1`, which the gateway reads and never
165
+ * forwards. It counts against the day's searches instead of its messages, and the gateway refuses it while
166
+ * `config().search` is off. Only a marked message may carry search options in its sealed body: the gateway pauses the
167
+ * member's day when an unmarked request searches. Never with `purpose: 'check'` (a check never searches): that is
168
+ * refused before anything is sent.
169
+ */
170
+ search?: boolean;
70
171
  /** The targets from a config already read. When omitted, `send()` reads the config first. */
71
172
  targets?: readonly SealedTarget[];
72
173
  signal?: AbortSignal;
@@ -93,6 +194,10 @@ export type SealedTransportErrorCode =
93
194
  | 'bad_encapsulated_key'
94
195
  /** The attestation nonce is not 32 bytes of hex: nothing was sent. */
95
196
  | 'bad_nonce'
197
+ /** The purpose is not one the gateway takes: nothing was sent. */
198
+ | 'bad_purpose'
199
+ /** The marks do not go together (`search` on a check), or `search` is not a boolean: nothing was sent. */
200
+ | 'bad_marks'
96
201
  /** A 2xx answer without a valid Ehbp-Response-Nonce: its body was discarded unread. */
97
202
  | 'unsealed_answer';
98
203
  export declare class SealedTransportError extends Error {
@@ -102,8 +207,10 @@ export declare class SealedTransportError extends Error {
102
207
  /** Set on a refusal the gateway wrote itself; a relayed router answer never carries it. */
103
208
  export declare const SEALED_REFUSAL_HEADER = "x-sealed-gateway-refusal";
104
209
  /**
105
- * Requests left in the member's allowance today. The gateway sets it on every answer it relayed from the router and on
106
- * no other, and the router cannot set it, so an error without it is one the API (or an edge in front of it) wrote.
210
+ * Messages left in the member's allowance today, on a check as on a message. The gateway sets it on every answer it
211
+ * relayed from the router and on no other, and the router cannot set it, so an error without it is one the API (or an
212
+ * edge in front of it) wrote. A relayed answer to a check also carries `X-Sealed-Checks-Remaining` while the gateway
213
+ * has a check allowance, and one to a marked search `X-Sealed-Searches-Remaining`.
107
214
  */
108
215
  export declare const SEALED_ALLOWANCE_REMAINING_HEADER = "x-sealed-allowance-remaining";
109
216
  export declare class SealedResource {
@@ -135,10 +242,23 @@ export declare class SealedResource {
135
242
  * has no nonce, never to be shown as the model's words.
136
243
  *
137
244
  * Throws a RecursivError for every error the API wrote rather than relayed, whether or not it is marked
138
- * X-Sealed-Gateway-Refusal: RateLimitError for an allowance used up (`code` `sealed_allowance_used` or
139
- * `sealed_too_many_requests`) or the API's own per-IP limit (`rate_limited`), AuthenticationError for a revoked or
140
- * expired key, NotFoundError while Sealed is off or not on for this app. Throws SealedTransportError when the router
141
- * is not listed, the key is malformed, or a 2xx answer came back unsealed.
245
+ * X-Sealed-Gateway-Refusal: RateLimitError for an allowance used up (`code` `sealed_allowance_used`,
246
+ * `sealed_search_allowance_used` or `sealed_too_many_requests`, with SealedQuotaDetails in `details`) or the API's own
247
+ * per-IP limit (`rate_limited`), a RecursivError with status 503 and `code` `sealed_paused_budget` while the app's
248
+ * monthly Sealed budget is used up (until the next UTC month), AuthenticationError for a revoked or expired key,
249
+ * NotFoundError while Sealed is off or not on for this app, AuthorizationError (`sealed_search_off`) for a marked
250
+ * search while search is off. Throws SealedTransportError when the router is not listed, the key, purpose or marks
251
+ * are malformed, or a 2xx answer came back unsealed.
252
+ *
253
+ * With `purpose: 'check'` the request counts against the check allowance while the gateway has one (`code`
254
+ * `sealed_check_allowance_used` when it is used up), and as a message otherwise. With `search: true` it counts
255
+ * against the day's searches (`sealed_search_allowance_used`) and spends no message. While the gateway's day lock is
256
+ * on, a member is refused until midnight UTC (`sealed_paused_today`) after a request of theirs that searched the web
257
+ * unmarked (or marked a check), that the router answered with a model `config()` does not offer for it (a check's must
258
+ * be in `checks`; any other's in `models`, or in `checks` while the gateway lets checks sent unmarked through), or
259
+ * that passed the day's count of answers left before their usage line (searches included). The lists can change on
260
+ * the server: read `config()` again from time to time, not once for the app's whole run, or a member whose app still
261
+ * sends to a model taken out of them is paused.
142
262
  */
143
263
  send(input: SealedSendInput): Promise<Response>;
144
264
  }
@@ -10,8 +10,10 @@ export class SealedTransportError extends Error {
10
10
  /** Set on a refusal the gateway wrote itself; a relayed router answer never carries it. */
11
11
  export const SEALED_REFUSAL_HEADER = 'x-sealed-gateway-refusal';
12
12
  /**
13
- * Requests left in the member's allowance today. The gateway sets it on every answer it relayed from the router and on
14
- * no other, and the router cannot set it, so an error without it is one the API (or an edge in front of it) wrote.
13
+ * Messages left in the member's allowance today, on a check as on a message. The gateway sets it on every answer it
14
+ * relayed from the router and on no other, and the router cannot set it, so an error without it is one the API (or an
15
+ * edge in front of it) wrote. A relayed answer to a check also carries `X-Sealed-Checks-Remaining` while the gateway
16
+ * has a check allowance, and one to a marked search `X-Sealed-Searches-Remaining`.
15
17
  */
16
18
  export const SEALED_ALLOWANCE_REMAINING_HEADER = 'x-sealed-allowance-remaining';
17
19
  const HEX_32_BYTES = /^[0-9a-fA-F]{64}$/;
@@ -95,10 +97,23 @@ export class SealedResource {
95
97
  * has no nonce, never to be shown as the model's words.
96
98
  *
97
99
  * Throws a RecursivError for every error the API wrote rather than relayed, whether or not it is marked
98
- * X-Sealed-Gateway-Refusal: RateLimitError for an allowance used up (`code` `sealed_allowance_used` or
99
- * `sealed_too_many_requests`) or the API's own per-IP limit (`rate_limited`), AuthenticationError for a revoked or
100
- * expired key, NotFoundError while Sealed is off or not on for this app. Throws SealedTransportError when the router
101
- * is not listed, the key is malformed, or a 2xx answer came back unsealed.
100
+ * X-Sealed-Gateway-Refusal: RateLimitError for an allowance used up (`code` `sealed_allowance_used`,
101
+ * `sealed_search_allowance_used` or `sealed_too_many_requests`, with SealedQuotaDetails in `details`) or the API's own
102
+ * per-IP limit (`rate_limited`), a RecursivError with status 503 and `code` `sealed_paused_budget` while the app's
103
+ * monthly Sealed budget is used up (until the next UTC month), AuthenticationError for a revoked or expired key,
104
+ * NotFoundError while Sealed is off or not on for this app, AuthorizationError (`sealed_search_off`) for a marked
105
+ * search while search is off. Throws SealedTransportError when the router is not listed, the key, purpose or marks
106
+ * are malformed, or a 2xx answer came back unsealed.
107
+ *
108
+ * With `purpose: 'check'` the request counts against the check allowance while the gateway has one (`code`
109
+ * `sealed_check_allowance_used` when it is used up), and as a message otherwise. With `search: true` it counts
110
+ * against the day's searches (`sealed_search_allowance_used`) and spends no message. While the gateway's day lock is
111
+ * on, a member is refused until midnight UTC (`sealed_paused_today`) after a request of theirs that searched the web
112
+ * unmarked (or marked a check), that the router answered with a model `config()` does not offer for it (a check's must
113
+ * be in `checks`; any other's in `models`, or in `checks` while the gateway lets checks sent unmarked through), or
114
+ * that passed the day's count of answers left before their usage line (searches included). The lists can change on
115
+ * the server: read `config()` again from time to time, not once for the app's whole run, or a member whose app still
116
+ * sends to a model taken out of them is paused.
102
117
  */
103
118
  async send(input) {
104
119
  // Malformed input is refused here, before the config is read, so it never turns into a network error.
@@ -109,6 +124,15 @@ export class SealedResource {
109
124
  if (!HEX_32_BYTES.test(input.encapsulatedKey)) {
110
125
  throw new SealedTransportError('bad_encapsulated_key', 'The encapsulated key must be 32 bytes of hex. Nothing was sent.');
111
126
  }
127
+ if (input.purpose !== undefined && input.purpose !== 'check') {
128
+ throw new SealedTransportError('bad_purpose', 'The purpose must be "check" or left out. Nothing was sent.');
129
+ }
130
+ if (input.search !== undefined && typeof input.search !== 'boolean') {
131
+ throw new SealedTransportError('bad_marks', 'search must be true, false or left out. Nothing was sent.');
132
+ }
133
+ if (input.search === true && input.purpose === 'check') {
134
+ throw new SealedTransportError('bad_marks', 'A check never searches: search cannot go with purpose "check". Nothing was sent.');
135
+ }
112
136
  const targets = input.targets ?? (await this.config()).targets;
113
137
  if (!targets.some((target) => target.origin === origin && target.kind === 'router')) {
114
138
  throw new SealedTransportError('enclave_not_allowed', 'That router is not one the gateway forwards to. Nothing was sent.');
@@ -137,6 +161,9 @@ export class SealedResource {
137
161
  // receives a request marked Sealed is meant to refuse it. No server path reads this header yet, so nothing
138
162
  // enforces that today; the Sealed gateway strips it before the router.
139
163
  'X-Recursiv-Privacy-Tier': 'sealed',
164
+ // Read by the gateway to pick the allowance, and never forwarded to the router.
165
+ ...(input.purpose === 'check' ? { 'X-Sealed-Purpose': 'check' } : {}),
166
+ ...(input.search === true ? { 'X-Sealed-Search': '1' } : {}),
140
167
  },
141
168
  body: input.body,
142
169
  signal: controller.signal,