@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.
- package/README.md +85 -1
- package/dist/assistant/index.d.ts +11 -3
- package/dist/assistant/index.js +11 -3
- package/dist/assistant/render.d.ts +25 -2
- package/dist/assistant/render.js +197 -16
- package/dist/assistant/subjectBundle.js +20 -3
- package/dist/assistant/turnCaps.d.ts +3 -0
- package/dist/assistant/turnCaps.js +141 -0
- package/dist/assistant/types.d.ts +223 -7
- package/dist/assistant/types.js +15 -0
- package/dist/index.d.ts +10 -4
- package/dist/index.js +7 -1
- package/dist/privacy/facts.generated.d.ts +79 -0
- package/dist/privacy/facts.generated.js +93 -0
- package/dist/privacy/index.d.ts +48 -0
- package/dist/privacy/index.js +42 -0
- package/dist/resources/discovery-merge.d.ts +17 -1
- package/dist/resources/discovery-merge.js +84 -4
- package/dist/resources/discovery-progressive.d.ts +4 -3
- package/dist/resources/discovery-progressive.js +11 -8
- package/dist/resources/discovery.d.ts +9 -1
- package/dist/resources/discovery.js +62 -15
- package/dist/resources/feeds.d.ts +15 -6
- package/dist/resources/feeds.js +2 -1
- package/dist/resources/integrations.d.ts +3 -0
- package/dist/resources/privacy.d.ts +13 -0
- package/dist/resources/privacy.js +14 -0
- package/dist/resources/protocols.d.ts +99 -11
- package/dist/resources/protocols.js +45 -0
- package/dist/resources/sealed.d.ts +127 -7
- package/dist/resources/sealed.js +33 -6
- package/dist/resources/settings.d.ts +3 -1
- package/dist/resources/settings.js +3 -1
- package/dist/social-protocol-ids.d.ts +28 -0
- package/dist/social-protocol-ids.js +30 -0
- package/dist/types.d.ts +37 -0
- package/llm.md +16 -5
- 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
|
-
/**
|
|
12
|
-
|
|
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' |
|
|
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:
|
|
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:
|
|
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:
|
|
191
|
+
networks: FeedTopNetworkMap<FeedTopNetworkEntry[]> | null;
|
|
183
192
|
lead: FeedStory | null;
|
|
184
193
|
meta: PublicFeedTopMeta;
|
|
185
194
|
}
|
package/dist/resources/feeds.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { NotFoundError, RecursivError } from '../errors.js';
|
|
2
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
106
|
-
* no other, and the router cannot set it, so an error without it is one the API (or an
|
|
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
|
|
139
|
-
* `
|
|
140
|
-
*
|
|
141
|
-
* is
|
|
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
|
}
|
package/dist/resources/sealed.js
CHANGED
|
@@ -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
|
-
*
|
|
14
|
-
* no other, and the router cannot set it, so an error without it is one the API (or an
|
|
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
|
|
99
|
-
* `
|
|
100
|
-
*
|
|
101
|
-
* is
|
|
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,
|