@recursiv/sdk 0.7.121 → 0.7.122
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 +37 -0
- package/dist/assistant-surfaces.d.ts +1 -1
- package/dist/assistant-surfaces.js +3 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/resources/agents.d.ts +1 -1
- package/dist/resources/agents.js +9 -0
- package/dist/resources/sealed.d.ts +110 -0
- package/dist/resources/sealed.js +46 -0
- package/dist/sealed/index.d.ts +19 -0
- package/dist/sealed/index.js +16 -0
- package/dist/sealed-frame/host.d.ts +98 -0
- package/dist/sealed-frame/host.js +379 -0
- package/dist/sealed-frame/index.d.ts +15 -0
- package/dist/sealed-frame/index.js +13 -0
- package/dist/sealed-frame/protocol.d.ts +493 -0
- package/dist/sealed-frame/protocol.js +276 -0
- package/dist/sealed-frame/wipe.d.ts +25 -0
- package/dist/sealed-frame/wipe.js +67 -0
- package/llm.md +7 -0
- package/package.json +11 -1
package/README.md
CHANGED
|
@@ -104,6 +104,7 @@ for await (const chunk of r.agents.chatStream(agent.id, { message: 'Hello!' }))
|
|
|
104
104
|
| `r.tags` | `create`, `list` |
|
|
105
105
|
| `r.signals` | `batch` (up to 100 engagement signals, such as views, dwell and hides, for the member's own ranking; `keepalive` for a page-leave flush), `record` (one signal) (0.7.71); `clearViewingHistory` ("Clear my viewing history"); typed `SignalMetadata`, an id per event so a retry is stored once, and `duplicates`/`limited`/`rejected` in the answer (0.7.93) |
|
|
106
106
|
| `r.privacy` | `capabilities` (the platform privacy facts the deployed API's build enforces, and its commit); pair with `@recursiv/sdk/privacy` (`PRIVACY_FACTS`, `isFactLive`, `liveFacts`) (unreleased) |
|
|
107
|
+
| `r.sealed` | Sealed mode: `config`, `attestation`, `send`; for web Sealed's frame (unreleased): `frameToken` (a short-lived `sft_` credential for the frame), `me` (the member's own age view and Sealed adult setting), `report`; pair with `@recursiv/sdk/sealed` (the resource alone) and `@recursiv/sdk/sealed-frame` (below) |
|
|
107
108
|
| `r.curator` | `today` (World Today; a world story can carry `story_key` and `discussion`, its count across networks), `storyPosts` / `memberStoryPosts` (one page of a story's posts, keyless or under the member's key), `storyPostPages` (walk every page), `forYou`, `run`, `refreshIfStale`, `seedDeck` |
|
|
108
109
|
| `r.uploads` | `uploadMedia` — upload post/chat bytes end to end |
|
|
109
110
|
| `r.judgments` | Jev's labels of a post (0.7.71): `get({ kind, id })` (the author and staff get every judgment and the safety verdict; other members the category and the compass band in words; `status: 'unavailable'` is returned, not thrown), keyless `getPublic(projectId, { kind, id })` (the category only), `dispute(judgmentId, { question_key, proposed, note? })` (the author's dispute is an appeal; a repeat returns `existing: true`), staff `review(judgmentId, { outcome, proposed?, note?, apply_to_shared? })` (sent once; 202 `awaiting_reviewer` until a compass label's second reviewer, or an agreeing Jev re-judge; `isJudgmentSharedWordsError` for the 409) |
|
|
@@ -478,6 +479,42 @@ if (match) {
|
|
|
478
479
|
- `election`: only clear logistics (how, where or when to vote, registering, deadlines, polling places, mail or absentee ballots, voter ID, checking registration or a ballot). Never candidates, results, polls, fraud claims or votes on a post or poll. The US card points to vote.gov and CanIVote.org and the state election office; elsewhere the country's election authority, or a generic line.
|
|
479
480
|
- English and Spanish. Every number and address has a cited source and an `as_of` date (`CRISIS_LINES`, `ELECTION_SITES`); a country with no checked entry gets the generic card, never a guessed number. `fixedCardFor(kind, options)` gives a card without a message.
|
|
480
481
|
|
|
482
|
+
## Sealed frame (`@recursiv/sdk/sealed-frame`)
|
|
483
|
+
|
|
484
|
+
Web Sealed runs in its own frame, served from its own origin (https://sealed.minds.com), so the page that embeds it can't read it. This browser-only subpath is protocol v1 for both sides: the host page uses `mountSealedFrame()` and `wipeSealedFrame()`, and the frame uses the same message types, builders and validators.
|
|
485
|
+
|
|
486
|
+
```typescript
|
|
487
|
+
import { mountSealedFrame, wipeSealedFrame } from '@recursiv/sdk/sealed-frame'
|
|
488
|
+
|
|
489
|
+
const frame = mountSealedFrame({
|
|
490
|
+
container, // size it with CSS; the frame never reports its height
|
|
491
|
+
frameOrigin: 'https://sealed.minds.com',
|
|
492
|
+
hostKind: 'minds', // or 'build'
|
|
493
|
+
scheme: 'dark',
|
|
494
|
+
layout: 'column', // or 'workspace'
|
|
495
|
+
account: member.id, // null when signed out: the frame wipes this browser's Sealed data
|
|
496
|
+
mintToken: () => r.sealed.frameToken(), // called only when the frame asks, once per token life
|
|
497
|
+
onState: (state) => setSealedState(state), // { state: 'ready' } or { state: 'unavailable', reason }
|
|
498
|
+
onNavigate: (to) => go(to), // 'plans' | 'settings_ai' | 'sign_in' | 'get_app'
|
|
499
|
+
})
|
|
500
|
+
|
|
501
|
+
frame.setTheme('light')
|
|
502
|
+
await frame.signedOut() // 'wiped' | 'timeout' (1.5 s)
|
|
503
|
+
frame.destroy()
|
|
504
|
+
|
|
505
|
+
// Any sign-out path, with or without a frame on screen:
|
|
506
|
+
await wipeSealedFrame({ frameOrigin: 'https://sealed.minds.com', account: member.id })
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
- The iframe is sandboxed (`allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox`), may only write to the clipboard, sends no referrer and is never `credentialless`.
|
|
510
|
+
- On every load the host posts `sealed/hello` to the frame origin only with a MessagePort, and then reads that port only: a window message is never read, whatever its origin or source.
|
|
511
|
+
- Every message is validated: an unknown type, a missing or extra field, a value outside its enum or pattern, or anything over 4 KB is dropped. Nothing the member types or reads ever crosses.
|
|
512
|
+
- Tokens: one shared mint in flight, a token reused until 60 s before it expires, at least 30 s between two mints whatever the browser's clock says, and a failed mint held back 2 s doubling to 60 s (`onTokenError` gets the error; `isSealedWebOff(error)` means web Sealed is off). The cache belongs to one mount, so keep one mount while the page shows Sealed.
|
|
513
|
+
- `signedOut()` always also runs the sign-out page, which records the wipe first; a frame that has had its hello but not said `ready` is reloaded, so its next hello is signed out.
|
|
514
|
+
- The frame must not use `<form>` submission: the sandbox has no `allow-forms` (the frame CSP sets `form-action 'none'`), so an embedded form never fires `submit`. Handle Enter and the send button with key and click handlers.
|
|
515
|
+
|
|
516
|
+
`@recursiv/sdk/sealed` exports `HttpClient`, `SealedResource` and the errors and nothing else, for a bundle that must hold only Sealed.
|
|
517
|
+
|
|
481
518
|
## Error handling
|
|
482
519
|
|
|
483
520
|
```typescript
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* Every assistant surface an app may show, one switch each. All default off. Step numbers are the George plan's §8.
|
|
18
18
|
* New ids are appended; an id is never renamed.
|
|
19
19
|
*/
|
|
20
|
-
export declare const ASSISTANT_SURFACE_IDS: readonly ["post_button", "answer_sheet", "search_row", "top_bar", "inbox_row", "mentions", "chats", "e2ee_asks", "groups_catch_up", "translate", "compose", "profile_facts", "why_seeing", "daily_brief", "notes", "george_tab", "sealed_in_tab", "thread_summary", "selection", "topic_catch_up", "describe_image", "alt_text", "feed_steering", "share_answer"];
|
|
20
|
+
export declare const ASSISTANT_SURFACE_IDS: readonly ["post_button", "answer_sheet", "search_row", "top_bar", "inbox_row", "mentions", "chats", "e2ee_asks", "groups_catch_up", "translate", "compose", "profile_facts", "why_seeing", "daily_brief", "notes", "george_tab", "sealed_in_tab", "thread_summary", "selection", "topic_catch_up", "describe_image", "alt_text", "feed_steering", "share_answer", "sealed_coming"];
|
|
21
21
|
export type AssistantSurfaceId = (typeof ASSISTANT_SURFACE_IDS)[number];
|
|
22
22
|
/** Every surface id with its switch, as the API sends it under `assistant.surfaces`. */
|
|
23
23
|
export type AssistantSurfaces = Record<AssistantSurfaceId, boolean>;
|
|
@@ -70,6 +70,9 @@ export const ASSISTANT_SURFACE_IDS = [
|
|
|
70
70
|
'feed_steering',
|
|
71
71
|
// Sharing an answer as your own post. G27, not before 2026-11-04.
|
|
72
72
|
'share_answer',
|
|
73
|
+
// The "Sealed · Coming" teaser (a pill and the sheet it opens) in the assistant tab. A Sealed launch item, not a
|
|
74
|
+
// George step: phone builds show it only while this is on, so it can be taken down without a new build.
|
|
75
|
+
'sealed_coming',
|
|
73
76
|
];
|
|
74
77
|
const SURFACE_IDS = new Set(ASSISTANT_SURFACE_IDS);
|
|
75
78
|
/** True for a surface id this SDK knows. */
|
package/dist/index.d.ts
CHANGED
|
@@ -236,8 +236,8 @@ export type { BookmarkCollection, BookmarkCollectionInput, BookmarkCollectionKin
|
|
|
236
236
|
export { AccountsResource } from './resources/accounts.js';
|
|
237
237
|
export { SIGNAL_BATCH_MAX, SignalsResource, newClientEventId } from './resources/signals.js';
|
|
238
238
|
export type { ClearViewingHistoryResult, SignalBatchResult, SignalEventInput, SignalEventType, SignalExit, SignalMetadata, SignalNotRecordedReason, SignalRecordResult, SignalRejection, SignalSendOptions, SignalSurface, } from './resources/signals.js';
|
|
239
|
-
export { SEALED_ALLOWANCE_REMAINING_HEADER, SEALED_REFUSAL_HEADER, SealedResource, SealedTransportError, } from './resources/sealed.js';
|
|
240
|
-
export type { SealedAllowance, SealedAttestationInput, SealedConfig, SealedModel, SealedPlan, SealedPurpose, SealedQuotaDetails, SealedSendInput, SealedTarget, SealedTargetKind, SealedTransportErrorCode, } from './resources/sealed.js';
|
|
239
|
+
export { SEALED_ALLOWANCE_REMAINING_HEADER, SEALED_REFUSAL_HEADER, SealedResource, SealedTransportError, SEALED_WEB_OFF, isSealedWebOff, } from './resources/sealed.js';
|
|
240
|
+
export type { SealedAllowance, SealedAttestationInput, SealedConfig, SealedModel, SealedPlan, SealedPurpose, SealedQuotaDetails, SealedSendInput, SealedTarget, SealedTargetKind, SealedTransportErrorCode, SealedFrameToken, SealedMe, SealedMeBody, SealedOwnContent, SealedOwnPrivacy, SealedRefusalCode, SealedReportInput, SealedReportResult, } from './resources/sealed.js';
|
|
241
241
|
export type { SiblingAccount, SiblingAccountsResponse, SwitchAccountResult, } from './resources/accounts.js';
|
|
242
242
|
export type { LinkPreview, LinkPreviewResponse, LinkPreviewSource, } from './resources/link-preview.js';
|
|
243
243
|
export type { AgentCapabilityConsentRequirement, AgentCapabilityDataSensitivity, AgentCapabilityDomain, AgentCapabilityExportBoundary, AgentCapabilityRetentionClass, AgentCapabilitySubdomain, AgentCapabilitySubject, AgentCapabilityTrustTier, AgentToolAudience, AgentToolScope, BrowserUseRunTaskInput, ProjectBrainSettings, ProjectToolBundleClassification, ProjectToolBundleKey, ProjectToolPermission, ProjectToolPermissionOverrides, UpdateProjectSettingsInput, } from './resources/project-brain.js';
|
package/dist/index.js
CHANGED
|
@@ -289,7 +289,7 @@ export { BOOKMARK_MERGE_MAX_ITEMS, BookmarkCollectionsResource, BookmarksResourc
|
|
|
289
289
|
export { FEED_TOP_NETWORKS, FeedsResource } from './resources/feeds.js';
|
|
290
290
|
export { AccountsResource } from './resources/accounts.js';
|
|
291
291
|
export { SIGNAL_BATCH_MAX, SignalsResource, newClientEventId } from './resources/signals.js';
|
|
292
|
-
export { SEALED_ALLOWANCE_REMAINING_HEADER, SEALED_REFUSAL_HEADER, SealedResource, SealedTransportError, } from './resources/sealed.js';
|
|
292
|
+
export { SEALED_ALLOWANCE_REMAINING_HEADER, SEALED_REFUSAL_HEADER, SealedResource, SealedTransportError, SEALED_WEB_OFF, isSealedWebOff, } from './resources/sealed.js';
|
|
293
293
|
export { OrganizationSettingsResource } from './resources/organization-settings.js';
|
|
294
294
|
export { ProjectSettingsResource } from './resources/project-settings.js';
|
|
295
295
|
export { OrganizationSecurityResource } from './resources/organization-security.js';
|
|
@@ -165,7 +165,7 @@ export interface SalesLead {
|
|
|
165
165
|
* Validation happens server-side too; this list lets apps validate or
|
|
166
166
|
* surface choices client-side without round-tripping the API.
|
|
167
167
|
*/
|
|
168
|
-
export declare const SUPPORTED_MODELS: readonly ["anthropic/claude-fable-5", "anthropic/claude-fable-5.1", "anthropic/claude-opus-4.6", "anthropic/claude-opus-4.8", "anthropic/claude-opus-5", "anthropic/claude-opus-5.5", "anthropic/claude-sonnet-4.6", "anthropic/claude-sonnet-5", "deepseek/deepseek-v3.2", "deepseek/deepseek-v4-pro", "google/gemini-3-flash-preview", "google/gemini-3.1-pro-preview", "google/gemini-3.5-flash", "google/gemini-3.8-flash", "minimax/minimax-m2.5", "minimax/minimax-m3", "moonshotai/kimi-k3", "openai/gpt-4o-mini", "openai/gpt-5.5", "openai/gpt-5.6-sol", "openai/gpt-5.6-terra", "openai/gpt-6-astra", "openai/gpt-6-sol", "x-ai/grok-4.3"];
|
|
168
|
+
export declare const SUPPORTED_MODELS: readonly ["anthropic/claude-fable-5", "anthropic/claude-fable-5.1", "anthropic/claude-haiku-5.5", "anthropic/claude-opus-4.6", "anthropic/claude-opus-4.8", "anthropic/claude-opus-5", "anthropic/claude-opus-5.5", "anthropic/claude-sonnet-4.6", "anthropic/claude-sonnet-5", "anthropic/claude-sonnet-5.5", "deepseek/deepseek-v3.2", "deepseek/deepseek-v4-pro", "deepseek/deepseek-v4-pro-0813", "deepseek/deepseek-v4.1-flash", "google/gemini-3-flash-preview", "google/gemini-3.1-pro-preview", "google/gemini-3.5-flash", "google/gemini-3.8-flash", "meta-llama/llama-4-maverick", "minimax/minimax-m2.5", "minimax/minimax-m3", "moonshotai/kimi-k3", "openai/gpt-4o-mini", "openai/gpt-5.5", "openai/gpt-5.6-sol", "openai/gpt-5.6-terra", "openai/gpt-6-astra", "openai/gpt-6-luna", "openai/gpt-6-sol", "openai/gpt-6.1-sol", "qwen/qwen3.8-2.4t-a95b", "x-ai/grok-4.3", "x-ai/grok-4.7"];
|
|
169
169
|
export type SupportedModel = (typeof SUPPORTED_MODELS)[number];
|
|
170
170
|
export interface EnsurePersonalAgentOverrides {
|
|
171
171
|
/** Override the agent's display name. Defaults to 'Agent'. */
|
package/dist/resources/agents.js
CHANGED
|
@@ -18,18 +18,23 @@ export const SUPPORTED_MODELS = [
|
|
|
18
18
|
// <<< GENERATED: openrouter-sync — do not edit by hand (npm run models:sync)
|
|
19
19
|
'anthropic/claude-fable-5',
|
|
20
20
|
'anthropic/claude-fable-5.1',
|
|
21
|
+
'anthropic/claude-haiku-5.5',
|
|
21
22
|
'anthropic/claude-opus-4.6',
|
|
22
23
|
'anthropic/claude-opus-4.8',
|
|
23
24
|
'anthropic/claude-opus-5',
|
|
24
25
|
'anthropic/claude-opus-5.5',
|
|
25
26
|
'anthropic/claude-sonnet-4.6',
|
|
26
27
|
'anthropic/claude-sonnet-5',
|
|
28
|
+
'anthropic/claude-sonnet-5.5',
|
|
27
29
|
'deepseek/deepseek-v3.2',
|
|
28
30
|
'deepseek/deepseek-v4-pro',
|
|
31
|
+
'deepseek/deepseek-v4-pro-0813',
|
|
32
|
+
'deepseek/deepseek-v4.1-flash',
|
|
29
33
|
'google/gemini-3-flash-preview',
|
|
30
34
|
'google/gemini-3.1-pro-preview',
|
|
31
35
|
'google/gemini-3.5-flash',
|
|
32
36
|
'google/gemini-3.8-flash',
|
|
37
|
+
'meta-llama/llama-4-maverick',
|
|
33
38
|
'minimax/minimax-m2.5',
|
|
34
39
|
'minimax/minimax-m3',
|
|
35
40
|
'moonshotai/kimi-k3',
|
|
@@ -38,8 +43,12 @@ export const SUPPORTED_MODELS = [
|
|
|
38
43
|
'openai/gpt-5.6-sol',
|
|
39
44
|
'openai/gpt-5.6-terra',
|
|
40
45
|
'openai/gpt-6-astra',
|
|
46
|
+
'openai/gpt-6-luna',
|
|
41
47
|
'openai/gpt-6-sol',
|
|
48
|
+
'openai/gpt-6.1-sol',
|
|
49
|
+
'qwen/qwen3.8-2.4t-a95b',
|
|
42
50
|
'x-ai/grok-4.3',
|
|
51
|
+
'x-ai/grok-4.7',
|
|
43
52
|
// >>> GENERATED
|
|
44
53
|
];
|
|
45
54
|
export class AgentsResource {
|
|
@@ -76,6 +76,12 @@ export interface SealedConfig {
|
|
|
76
76
|
* a config you could not read, means off.
|
|
77
77
|
*/
|
|
78
78
|
memory?: boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Server switch (SEALED_GATEWAY_WEB): Sealed in a browser, through the Sealed frame. While it is off no frame token is
|
|
81
|
+
* minted or accepted (403 `sealed_web_off`), and `me()` and `report()` refuse every caller. Absent (an older gateway),
|
|
82
|
+
* or a config you could not read, means off: hide web Sealed.
|
|
83
|
+
*/
|
|
84
|
+
web?: boolean;
|
|
79
85
|
/** The attestation policy version the app's verifier must pin; an older pinned policy means the app needs updating. */
|
|
80
86
|
verifier_policy_version: number;
|
|
81
87
|
/** The member's allowance: its daily numbers are their plan's (`plan`). */
|
|
@@ -204,6 +210,81 @@ export declare class SealedTransportError extends Error {
|
|
|
204
210
|
readonly code: SealedTransportErrorCode;
|
|
205
211
|
constructor(code: SealedTransportErrorCode, message: string);
|
|
206
212
|
}
|
|
213
|
+
/**
|
|
214
|
+
* The `code` of a refusal the gateway wrote itself (RecursivError.code, and the X-Sealed-Gateway-Refusal header). The
|
|
215
|
+
* API's own answers (`invalid_api_key`, `rate_limited`, a 404 while Sealed is off for the app) carry other codes.
|
|
216
|
+
*/
|
|
217
|
+
export type SealedRefusalCode = 'sealed_unavailable' | 'sealed_member_key_required' | 'sealed_query_not_allowed' | 'sealed_enclave_required' | 'sealed_enclave_not_allowed' | 'sealed_body_required' | 'sealed_content_type' | 'sealed_request_too_large' | 'sealed_too_many_requests' | 'sealed_allowance_used' | 'sealed_upstream_unreachable' | 'sealed_upstream_unsealed' | 'sealed_method_not_allowed' | 'sealed_attestation_nonce_invalid' | 'sealed_attestation_target_not_allowed' | 'sealed_attestation_unavailable' | 'sealed_purpose_unknown' | 'sealed_search_mark_invalid' | 'sealed_search_off' | 'sealed_check_allowance_used' | 'sealed_search_allowance_used' | 'sealed_paused_today' | 'sealed_paused_budget'
|
|
218
|
+
/** 403 while Sealed in a browser is off (SEALED_GATEWAY_WEB): no frame token is minted or accepted. */
|
|
219
|
+
| 'sealed_web_off'
|
|
220
|
+
/** 403: a frame token on a route the frame does not use (the mint route among them). */
|
|
221
|
+
| 'sealed_frame_not_allowed'
|
|
222
|
+
/** 403: a frame token sent from any origin but the frame origin it was minted for. */
|
|
223
|
+
| 'sealed_frame_origin'
|
|
224
|
+
/** 400: a report body that is not `{ reason, details? }`. */
|
|
225
|
+
| 'sealed_report_invalid';
|
|
226
|
+
/** The refusal code while Sealed in a browser is off (403, AuthorizationError). */
|
|
227
|
+
export declare const SEALED_WEB_OFF = "sealed_web_off";
|
|
228
|
+
/** Whether an error is the gateway's 403 `sealed_web_off`: Sealed in a browser is off, so hide web Sealed. */
|
|
229
|
+
export declare function isSealedWebOff(error: unknown): boolean;
|
|
230
|
+
/** What `frameToken()` returns: a short-lived credential for the Sealed frame, never an API key. */
|
|
231
|
+
export interface SealedFrameToken {
|
|
232
|
+
/**
|
|
233
|
+
* The opaque frame token (`sft_…`). Hand it to the Sealed frame and nothing else: the frame keeps it in memory only.
|
|
234
|
+
* It is accepted only from `origin`, only on the frame's routes, only while the key that minted it is active, and
|
|
235
|
+
* never on the mint route, so a frame cannot renew itself.
|
|
236
|
+
*/
|
|
237
|
+
token: string;
|
|
238
|
+
/** When it stops working, ISO 8601: the gateway grants 10 minutes by default, 15 at most. */
|
|
239
|
+
expiresAt: string;
|
|
240
|
+
/**
|
|
241
|
+
* How many seconds it lives, from the gateway's `expires_in`, so a host can time it without trusting the browser's
|
|
242
|
+
* clock. Absent while the gateway does not send `expires_in` (none does yet); mountSealedFrame() then uses `expiresAt`.
|
|
243
|
+
*/
|
|
244
|
+
expiresIn?: number;
|
|
245
|
+
/** The frame origin it works from, e.g. `https://sealed.minds.com`. */
|
|
246
|
+
origin: string;
|
|
247
|
+
}
|
|
248
|
+
/** The member's own age view, as GET /settings/preferences gives it (`privacy`). */
|
|
249
|
+
export interface SealedOwnPrivacy {
|
|
250
|
+
/** True only when the member answered the age question 18 or older. */
|
|
251
|
+
age_is_adult: boolean;
|
|
252
|
+
age_declared: boolean;
|
|
253
|
+
who_can_message_locked: boolean;
|
|
254
|
+
}
|
|
255
|
+
/** The member's own Sealed adult setting (D17). Through a frame token it is always false: D17 is off on the web. */
|
|
256
|
+
export interface SealedOwnContent {
|
|
257
|
+
sealed_adult: boolean;
|
|
258
|
+
}
|
|
259
|
+
/** GET /sealed/me: the member's id, their own age view and their Sealed adult setting, and nothing else. */
|
|
260
|
+
export interface SealedMeBody {
|
|
261
|
+
/** The member's user id: the frame checks it against the account its host named. */
|
|
262
|
+
user_id: string;
|
|
263
|
+
privacy: SealedOwnPrivacy;
|
|
264
|
+
content: SealedOwnContent;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* What `me()` returns: the body, and the same fields again under `data`, the shape of `settings.getPreferences()`, so
|
|
268
|
+
* a reader written for that (minds lib/knownAge.ts, lib/sealedAdultSetting.ts) reads it unchanged.
|
|
269
|
+
*/
|
|
270
|
+
export interface SealedMe extends SealedMeBody {
|
|
271
|
+
data: SealedMeBody;
|
|
272
|
+
}
|
|
273
|
+
export interface SealedReportInput {
|
|
274
|
+
/** Why the member reports the answer: 1 to 100 characters. */
|
|
275
|
+
reason: string;
|
|
276
|
+
/** What the member chose to show Minds of the exchange: at most 2000 characters. */
|
|
277
|
+
details?: string;
|
|
278
|
+
}
|
|
279
|
+
/** What `report()` returns. */
|
|
280
|
+
export interface SealedReportResult {
|
|
281
|
+
/** The report's id, or null when the reports service did not say. */
|
|
282
|
+
id: string | null;
|
|
283
|
+
/** True when it joined the member's open report of their own account. */
|
|
284
|
+
existing: boolean;
|
|
285
|
+
/** False when it joined an open report and its details did not fit there in full: the reason still reached staff. */
|
|
286
|
+
details_kept: boolean;
|
|
287
|
+
}
|
|
207
288
|
/** Set on a refusal the gateway wrote itself; a relayed router answer never carries it. */
|
|
208
289
|
export declare const SEALED_REFUSAL_HEADER = "x-sealed-gateway-refusal";
|
|
209
290
|
/**
|
|
@@ -218,6 +299,35 @@ export declare class SealedResource {
|
|
|
218
299
|
constructor(client: HttpClient);
|
|
219
300
|
/** The targets, models, verifier policy version and allowance the app needs before it seals anything. */
|
|
220
301
|
config(): Promise<SealedConfig>;
|
|
302
|
+
/**
|
|
303
|
+
* A frame token for the Sealed frame (POST /sealed/frame-token), minted from this client's key, which must be the
|
|
304
|
+
* signed-in member's own key of a listed app. The host page calls it when its frame asks for a token and hands the
|
|
305
|
+
* token to the frame; `@recursiv/sdk/sealed-frame`'s mountSealedFrame() does that, minting at most once per token
|
|
306
|
+
* life. Asked once, never retried: past 30 a minute with `me()` the gateway answers 429.
|
|
307
|
+
*
|
|
308
|
+
* Throws AuthorizationError `sealed_web_off` while Sealed in a browser is off (`isSealedWebOff`),
|
|
309
|
+
* `sealed_member_key_required` for a key that may not mint one (an operator's, an IP- or origin-limited one) and
|
|
310
|
+
* `sealed_frame_not_allowed` when called with a frame token; RateLimitError `sealed_too_many_requests`; a
|
|
311
|
+
* RecursivError with status 503 `sealed_unavailable` while the gateway's frame settings are missing; NotFoundError
|
|
312
|
+
* while Sealed is off or not on for this app.
|
|
313
|
+
*/
|
|
314
|
+
frameToken(): Promise<SealedFrameToken>;
|
|
315
|
+
/**
|
|
316
|
+
* The member's own id, age view and Sealed adult setting (GET /sealed/me): what Sealed reads before it opens a
|
|
317
|
+
* session, for the Sealed frame, which has no settings scope. Read with a frame token or the member's own key, only
|
|
318
|
+
* while Sealed in a browser is on (403 `sealed_web_off` otherwise). Through a frame token `content.sealed_adult` is
|
|
319
|
+
* always false. Returns the body and the same fields under `data`, so a `settings.getPreferences()` reader takes it
|
|
320
|
+
* as it is: `{ settings: { getPreferences: () => sealed.me() } }`. Asked once, never retried.
|
|
321
|
+
*/
|
|
322
|
+
me(): Promise<SealedMe>;
|
|
323
|
+
/**
|
|
324
|
+
* Reports a Sealed answer (POST /sealed/reports), filed on the member's own account whatever the body names: the
|
|
325
|
+
* report carries only what the member chose to put in it. Only while Sealed in a browser is on (403
|
|
326
|
+
* `sealed_web_off`); every report spends the member's report budget (RateLimitError past it). Throws
|
|
327
|
+
* ValidationError `sealed_report_invalid` for a reason that is not 1 to 100 characters or details over 2000. Sent
|
|
328
|
+
* once, never retried, so a report is never filed twice by the SDK.
|
|
329
|
+
*/
|
|
330
|
+
report(input: SealedReportInput): Promise<SealedReportResult>;
|
|
221
331
|
/**
|
|
222
332
|
* A target's attestation document, fetched through the gateway so Tinfoil sees the gateway's address rather than the
|
|
223
333
|
* phone's. Returns the document's bytes exactly as the target served them, for the verifier (packages/e2ee attest),
|
package/dist/resources/sealed.js
CHANGED
|
@@ -7,6 +7,12 @@ export class SealedTransportError extends Error {
|
|
|
7
7
|
this.code = code;
|
|
8
8
|
}
|
|
9
9
|
}
|
|
10
|
+
/** The refusal code while Sealed in a browser is off (403, AuthorizationError). */
|
|
11
|
+
export const SEALED_WEB_OFF = 'sealed_web_off';
|
|
12
|
+
/** Whether an error is the gateway's 403 `sealed_web_off`: Sealed in a browser is off, so hide web Sealed. */
|
|
13
|
+
export function isSealedWebOff(error) {
|
|
14
|
+
return error instanceof RecursivError && error.status === 403 && error.code === SEALED_WEB_OFF;
|
|
15
|
+
}
|
|
10
16
|
/** Set on a refusal the gateway wrote itself; a relayed router answer never carries it. */
|
|
11
17
|
export const SEALED_REFUSAL_HEADER = 'x-sealed-gateway-refusal';
|
|
12
18
|
/**
|
|
@@ -37,6 +43,46 @@ export class SealedResource {
|
|
|
37
43
|
async config() {
|
|
38
44
|
return this.client.get('/sealed/config');
|
|
39
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* A frame token for the Sealed frame (POST /sealed/frame-token), minted from this client's key, which must be the
|
|
48
|
+
* signed-in member's own key of a listed app. The host page calls it when its frame asks for a token and hands the
|
|
49
|
+
* token to the frame; `@recursiv/sdk/sealed-frame`'s mountSealedFrame() does that, minting at most once per token
|
|
50
|
+
* life. Asked once, never retried: past 30 a minute with `me()` the gateway answers 429.
|
|
51
|
+
*
|
|
52
|
+
* Throws AuthorizationError `sealed_web_off` while Sealed in a browser is off (`isSealedWebOff`),
|
|
53
|
+
* `sealed_member_key_required` for a key that may not mint one (an operator's, an IP- or origin-limited one) and
|
|
54
|
+
* `sealed_frame_not_allowed` when called with a frame token; RateLimitError `sealed_too_many_requests`; a
|
|
55
|
+
* RecursivError with status 503 `sealed_unavailable` while the gateway's frame settings are missing; NotFoundError
|
|
56
|
+
* while Sealed is off or not on for this app.
|
|
57
|
+
*/
|
|
58
|
+
async frameToken() {
|
|
59
|
+
const body = await this.client.post('/sealed/frame-token', {}, { retry: false });
|
|
60
|
+
const expiresIn = typeof body.expires_in === 'number' && Number.isFinite(body.expires_in) ? body.expires_in : undefined;
|
|
61
|
+
return { token: body.token, expiresAt: body.expires_at, ...(expiresIn !== undefined ? { expiresIn } : {}), origin: body.origin };
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The member's own id, age view and Sealed adult setting (GET /sealed/me): what Sealed reads before it opens a
|
|
65
|
+
* session, for the Sealed frame, which has no settings scope. Read with a frame token or the member's own key, only
|
|
66
|
+
* while Sealed in a browser is on (403 `sealed_web_off` otherwise). Through a frame token `content.sealed_adult` is
|
|
67
|
+
* always false. Returns the body and the same fields under `data`, so a `settings.getPreferences()` reader takes it
|
|
68
|
+
* as it is: `{ settings: { getPreferences: () => sealed.me() } }`. Asked once, never retried.
|
|
69
|
+
*/
|
|
70
|
+
async me() {
|
|
71
|
+
const body = await this.client.get('/sealed/me', undefined, { retry: false });
|
|
72
|
+
const own = { user_id: body.user_id, privacy: body.privacy, content: body.content };
|
|
73
|
+
return { ...own, data: { ...own } };
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Reports a Sealed answer (POST /sealed/reports), filed on the member's own account whatever the body names: the
|
|
77
|
+
* report carries only what the member chose to put in it. Only while Sealed in a browser is on (403
|
|
78
|
+
* `sealed_web_off`); every report spends the member's report budget (RateLimitError past it). Throws
|
|
79
|
+
* ValidationError `sealed_report_invalid` for a reason that is not 1 to 100 characters or details over 2000. Sent
|
|
80
|
+
* once, never retried, so a report is never filed twice by the SDK.
|
|
81
|
+
*/
|
|
82
|
+
async report(input) {
|
|
83
|
+
const body = { reason: input.reason, ...(input.details !== undefined ? { details: input.details } : {}) };
|
|
84
|
+
return this.client.post('/sealed/reports', body, { retry: false });
|
|
85
|
+
}
|
|
40
86
|
/**
|
|
41
87
|
* A target's attestation document, fetched through the gateway so Tinfoil sees the gateway's address rather than the
|
|
42
88
|
* phone's. Returns the document's bytes exactly as the target served them, for the verifier (packages/e2ee attest),
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@recursiv/sdk/sealed`: the Sealed resource alone, for a bundle that must hold nothing else, such as the Sealed frame
|
|
3
|
+
* (https://sealed.minds.com), whose every file is listed and reviewed by hand. It carries HttpClient, SealedResource and
|
|
4
|
+
* the errors, and no other resource: the main entry builds every resource the API has.
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* import { HttpClient, SealedResource } from '@recursiv/sdk/sealed'
|
|
8
|
+
* const sealed = new SealedResource(new HttpClient({ apiKey: frameToken, baseUrl: 'https://api.minds.com/api/v1' }))
|
|
9
|
+
* ```
|
|
10
|
+
*
|
|
11
|
+
* HttpClient fixes its key when it is made, so a frame that renews its token builds a new SealedResource each time.
|
|
12
|
+
* A bundle test (src/__tests__/sealed-subpath.bundle.test.ts) fails if this entry reaches any other resource.
|
|
13
|
+
*/
|
|
14
|
+
export { HttpClient } from '../client.js';
|
|
15
|
+
export { isSealedWebOff, SEALED_ALLOWANCE_REMAINING_HEADER, SEALED_REFUSAL_HEADER, SEALED_WEB_OFF, SealedResource, SealedTransportError, } from '../resources/sealed.js';
|
|
16
|
+
export type { SealedAllowance, SealedAttestationInput, SealedConfig, SealedFrameToken, SealedMe, SealedMeBody, SealedModel, SealedOwnContent, SealedOwnPrivacy, SealedPlan, SealedPurpose, SealedQuotaDetails, SealedRefusalCode, SealedReportInput, SealedReportResult, SealedSendInput, SealedTarget, SealedTargetKind, SealedTransportErrorCode, } from '../resources/sealed.js';
|
|
17
|
+
export { AuthenticationError, AuthorizationError, ConflictError, InsufficientCreditsError, NotFoundError, RateLimitError, RecursivError, ValidationError, } from '../errors.js';
|
|
18
|
+
export type { RateLimitDetails } from '../errors.js';
|
|
19
|
+
export type { RecursivConfig } from '../types.js';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@recursiv/sdk/sealed`: the Sealed resource alone, for a bundle that must hold nothing else, such as the Sealed frame
|
|
3
|
+
* (https://sealed.minds.com), whose every file is listed and reviewed by hand. It carries HttpClient, SealedResource and
|
|
4
|
+
* the errors, and no other resource: the main entry builds every resource the API has.
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* import { HttpClient, SealedResource } from '@recursiv/sdk/sealed'
|
|
8
|
+
* const sealed = new SealedResource(new HttpClient({ apiKey: frameToken, baseUrl: 'https://api.minds.com/api/v1' }))
|
|
9
|
+
* ```
|
|
10
|
+
*
|
|
11
|
+
* HttpClient fixes its key when it is made, so a frame that renews its token builds a new SealedResource each time.
|
|
12
|
+
* A bundle test (src/__tests__/sealed-subpath.bundle.test.ts) fails if this entry reaches any other resource.
|
|
13
|
+
*/
|
|
14
|
+
export { HttpClient } from '../client.js';
|
|
15
|
+
export { isSealedWebOff, SEALED_ALLOWANCE_REMAINING_HEADER, SEALED_REFUSAL_HEADER, SEALED_WEB_OFF, SealedResource, SealedTransportError, } from '../resources/sealed.js';
|
|
16
|
+
export { AuthenticationError, AuthorizationError, ConflictError, InsufficientCreditsError, NotFoundError, RateLimitError, RecursivError, ValidationError, } from '../errors.js';
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { type SealedFrameHostKind, type SealedFrameLayout, type SealedFrameNavigateTarget, type SealedFrameScheme, type SealedFrameUnavailableReason } from './protocol.js';
|
|
2
|
+
/**
|
|
3
|
+
* What the host shows for the frame: the frame's own `state`, or one the host decides itself:
|
|
4
|
+
* - `update` when the frame speaks another protocol version ("Reload the page");
|
|
5
|
+
* - `no_answer` when the frame did not answer its hello in time (it failed to load, or is not the Sealed frame).
|
|
6
|
+
*/
|
|
7
|
+
export type SealedFrameHostState = {
|
|
8
|
+
state: 'ready';
|
|
9
|
+
} | {
|
|
10
|
+
state: 'unavailable';
|
|
11
|
+
reason: SealedFrameUnavailableReason | 'no_answer';
|
|
12
|
+
};
|
|
13
|
+
/** What `mintToken` resolves to: `sdk.sealed.frameToken()`'s answer fits. */
|
|
14
|
+
export interface SealedFrameMintedToken {
|
|
15
|
+
token: string;
|
|
16
|
+
/** When it expires: an ISO 8601 time, ms since the epoch, or a Date. Read against this browser's clock. */
|
|
17
|
+
expiresAt: string | number | Date;
|
|
18
|
+
/**
|
|
19
|
+
* How many seconds it lives, as the API counted when it minted it. When given, the host times the token from when it
|
|
20
|
+
* asked for it instead of from `expiresAt`, so a browser clock that runs fast or slow does not matter.
|
|
21
|
+
*/
|
|
22
|
+
expiresIn?: number;
|
|
23
|
+
/** The frame origin the token works from. When given, it must be this frame's origin, or the token is not used. */
|
|
24
|
+
origin?: string;
|
|
25
|
+
}
|
|
26
|
+
export interface MountSealedFrameOptions {
|
|
27
|
+
/** The element the frame fills. Size it with CSS: the frame never says how tall its content is. */
|
|
28
|
+
container: HTMLElement;
|
|
29
|
+
/** The frame origin, e.g. `https://sealed.minds.com` (staging `https://sealed-staging.minds.com`). */
|
|
30
|
+
frameOrigin: string;
|
|
31
|
+
hostKind: SealedFrameHostKind;
|
|
32
|
+
scheme: SealedFrameScheme;
|
|
33
|
+
layout: SealedFrameLayout;
|
|
34
|
+
/** The signed-in member's user id, or null when no one is signed in (the frame then wipes this browser's Sealed data). */
|
|
35
|
+
account: string | null;
|
|
36
|
+
/**
|
|
37
|
+
* Mints a frame token for the signed-in member, e.g. `() => sdk.sealed.frameToken()`. Called only when the frame asks
|
|
38
|
+
* and the last token is within 60 s of expiring (or was refused), never twice at once (concurrent asks share one
|
|
39
|
+
* call), and at least 30 s after the last mint that worked, whatever the browser's clock says. A failure is passed to
|
|
40
|
+
* `onTokenError`, and asks are then held back, 2 s doubling to 60 s, before the next call.
|
|
41
|
+
*/
|
|
42
|
+
mintToken: () => Promise<SealedFrameMintedToken>;
|
|
43
|
+
onState?: (state: SealedFrameHostState) => void;
|
|
44
|
+
onNavigate?: (to: SealedFrameNavigateTarget) => void;
|
|
45
|
+
/** A mint that failed or gave something unusable: `isSealedWebOff(error)` from `@recursiv/sdk` means hide web Sealed. */
|
|
46
|
+
onTokenError?: (error: unknown) => void;
|
|
47
|
+
/** The iframe's accessible name. `Sealed chat` by default. */
|
|
48
|
+
title?: string;
|
|
49
|
+
/**
|
|
50
|
+
* How long the frame has to answer, from the mount and again from each load, before the host reports `no_answer`.
|
|
51
|
+
* 15 s by default.
|
|
52
|
+
*/
|
|
53
|
+
readyTimeoutMs?: number;
|
|
54
|
+
}
|
|
55
|
+
export interface SealedFrameHandle {
|
|
56
|
+
/** The frame's iframe, already in `container`. */
|
|
57
|
+
readonly iframe: HTMLIFrameElement;
|
|
58
|
+
setTheme(scheme: SealedFrameScheme): void;
|
|
59
|
+
setLayout(layout: SealedFrameLayout): void;
|
|
60
|
+
/** The member switched accounts: the frame drops its session and token, and the host its cached token. */
|
|
61
|
+
setAccount(account: string): void;
|
|
62
|
+
/**
|
|
63
|
+
* The member signed out: the frame wipes the account's Sealed data. Always also runs the sign-out page
|
|
64
|
+
* (wipeSealedFrame), which records the wipe first so one cut short (the frame mid-reload, or the page reloading now)
|
|
65
|
+
* finishes on the frame's next load. Resolves `wiped` when either says so, or `timeout` after `timeoutMs` (1500 ms
|
|
66
|
+
* by default). A frame that has had its hello but not said `ready` is reloaded, so its next hello is signed out (which
|
|
67
|
+
* wipes every account's Sealed data) and it never runs on as the account its first hello named. The host forgets the
|
|
68
|
+
* account and its token at once either way.
|
|
69
|
+
*/
|
|
70
|
+
signedOut(options?: {
|
|
71
|
+
timeoutMs?: number;
|
|
72
|
+
}): Promise<'wiped' | 'timeout'>;
|
|
73
|
+
newChat(): void;
|
|
74
|
+
/** Removes the frame and closes its port. Every method is a no-op afterwards; a pending signedOut() gets `timeout`. */
|
|
75
|
+
destroy(): void;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* A cached token is handed out until this long before it expires (later only to a frame load that does not hold it,
|
|
79
|
+
* while the next mint is held back); the frame asks again then.
|
|
80
|
+
*/
|
|
81
|
+
export declare const SEALED_FRAME_TOKEN_REFRESH_MS = 60000;
|
|
82
|
+
/**
|
|
83
|
+
* Embeds the Sealed frame in `container` and runs the host's side of protocol v1 (protocol.ts). The iframe is
|
|
84
|
+
* sandboxed (SEALED_FRAME_SANDBOX), may only write to the clipboard, sends no referrer, and is never `credentialless`
|
|
85
|
+
* (that would give the frame a throwaway storage partition and lose the member's Sealed chats).
|
|
86
|
+
*
|
|
87
|
+
* On every load of the iframe the host posts `sealed/hello` to the frame origin only, with a new MessagePort, and from
|
|
88
|
+
* then on reads that port only: a window message is never read, whatever its origin or source, and a port from an
|
|
89
|
+
* earlier load is closed. Nothing counts until the frame answers `ready`.
|
|
90
|
+
*
|
|
91
|
+
* `need-token` is answered through `mintToken`, minting at most once per token life and never twice in 30 s (see
|
|
92
|
+
* MountSealedFrameOptions), so reloading the frame, a frame that asks too often or a wrong browser clock never storms
|
|
93
|
+
* the API. The token cache belongs to one mount: keep one mount for as long as the page shows Sealed, since each new
|
|
94
|
+
* mount mints afresh on its frame's first ask.
|
|
95
|
+
*
|
|
96
|
+
* Browser only: call it in an effect, and `destroy()` on unmount.
|
|
97
|
+
*/
|
|
98
|
+
export declare function mountSealedFrame(options: MountSealedFrameOptions): SealedFrameHandle;
|