@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
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Sealed frame's protocol, version 1: the messages between a host page (minds.com/ai, build.minds.com) and the
|
|
3
|
+
* Sealed frame it embeds (https://sealed.minds.com), and between a host and the frame's sign-out page (/wipe.html).
|
|
4
|
+
* Both sides use these types, builders and validators, so there is one definition of what may cross.
|
|
5
|
+
*
|
|
6
|
+
* How the two talk (the Sealed frame design of 2026-10-13, section 5):
|
|
7
|
+
* 1. On every load event of the frame's iframe, the host posts `sealed/hello` with a transferred MessagePort to the
|
|
8
|
+
* frame origin only (`targetOrigin`). The frame must add its window message listener as soon as its module runs, and
|
|
9
|
+
* accepts the first hello whose `event.origin` is one of its compiled hosts and whose `event.source` is
|
|
10
|
+
* `window.parent`. After that both sides use the port only.
|
|
11
|
+
* 2. The frame answers `ready` on the port, naming the protocol it speaks; a host that speaks another shows "Reload
|
|
12
|
+
* the page". Nothing else counts before `ready`.
|
|
13
|
+
* 3. Host to frame, on the port: `token`, `theme`, `layout`, `account`, `signed-out`, `new-chat`.
|
|
14
|
+
* Frame to host, on the port: `need-token`, `state`, `navigate`, `wiped`.
|
|
15
|
+
*
|
|
16
|
+
* Every field is an enum, a boolean, an id, a time or the opaque token. Never sent, either way: typed words, answers,
|
|
17
|
+
* chat ids, titles or counts, failure details, or when sends happen. There are no size messages: the host sizes the
|
|
18
|
+
* frame with CSS, since a height would say how long the transcript is.
|
|
19
|
+
*
|
|
20
|
+
* Every message is checked before it is read. A validator returns a fresh copy holding only the message's own fields,
|
|
21
|
+
* or null for anything else: a type it does not know, a missing, extra or wrongly typed field, a string outside its
|
|
22
|
+
* enum or pattern, a getter or symbol key, or a message over 4 KB of JSON. The receiver ignores null. Builders check
|
|
23
|
+
* their own output the same way and throw a TypeError rather than send a message the other side would drop.
|
|
24
|
+
*/
|
|
25
|
+
/** The protocol this SDK speaks, which `ready` names. */
|
|
26
|
+
export const SEALED_FRAME_PROTOCOL = 1;
|
|
27
|
+
/** Every message's format version (`v`). */
|
|
28
|
+
export const SEALED_FRAME_MESSAGE_VERSION = 1;
|
|
29
|
+
/** The most a message may be, as UTF-8 JSON. Anything larger is dropped unread. */
|
|
30
|
+
export const SEALED_FRAME_MESSAGE_MAX_BYTES = 4096;
|
|
31
|
+
/**
|
|
32
|
+
* The frame's iframe sandbox. It protects the host, not the frame: the frame's protection is the same-origin policy and
|
|
33
|
+
* its own CSP. `allow-same-origin` keeps the frame's own storage; popups let crisis and help links open in a new tab.
|
|
34
|
+
* There is no `allow-forms`, in line with the frame CSP's `form-action 'none'`: embedded, the browser stops a form
|
|
35
|
+
* submission before its `submit` event fires, so the frame must not use `<form>` submission (handle Enter and the send
|
|
36
|
+
* button with key and click handlers). Standalone at its own origin a form would work, which is why tests miss it.
|
|
37
|
+
*/
|
|
38
|
+
export const SEALED_FRAME_SANDBOX = 'allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox';
|
|
39
|
+
/** The sign-out page's sandbox: it runs a script against the frame origin's storage and opens nothing. */
|
|
40
|
+
export const SEALED_WIPE_SANDBOX = 'allow-scripts allow-same-origin';
|
|
41
|
+
/** The frame's permissions policy: only writing to the clipboard (copy an answer). */
|
|
42
|
+
export const SEALED_FRAME_ALLOW = 'clipboard-write';
|
|
43
|
+
/** No referrer leaves the host for the frame. */
|
|
44
|
+
export const SEALED_FRAME_REFERRER_POLICY = 'no-referrer';
|
|
45
|
+
/** The frame's sign-out page, on the frame origin. */
|
|
46
|
+
export const SEALED_WIPE_PATH = '/wipe.html';
|
|
47
|
+
/** Which host embeds the frame. */
|
|
48
|
+
export const SEALED_FRAME_HOST_KINDS = ['minds', 'build'];
|
|
49
|
+
export const SEALED_FRAME_SCHEMES = ['light', 'dark'];
|
|
50
|
+
/** `column`: a phone-width column. `workspace`: the desktop thread area, where the frame lists its own Sealed chats. */
|
|
51
|
+
export const SEALED_FRAME_LAYOUTS = ['column', 'workspace'];
|
|
52
|
+
/** Why the frame cannot run Sealed now (`state`, `unavailable`). */
|
|
53
|
+
export const SEALED_FRAME_UNAVAILABLE_REASONS = [
|
|
54
|
+
'unsupported',
|
|
55
|
+
'tab_busy',
|
|
56
|
+
'off',
|
|
57
|
+
'words',
|
|
58
|
+
'signed_out',
|
|
59
|
+
'update',
|
|
60
|
+
'no_account',
|
|
61
|
+
];
|
|
62
|
+
/** Where the frame asks the host to take the member (`navigate`). */
|
|
63
|
+
export const SEALED_FRAME_NAVIGATE_TARGETS = ['plans', 'settings_ai', 'sign_in', 'get_app'];
|
|
64
|
+
/** A member's user id (a UUID today): letters, digits, `-` and `_`, 1 to 64 of them. */
|
|
65
|
+
const ACCOUNT = /^[A-Za-z0-9_-]{1,64}$/;
|
|
66
|
+
/** A frame token: `sft_` + base64url payload + `.` + base64url MAC, the API's format. */
|
|
67
|
+
const FRAME_TOKEN = /^sft_[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/;
|
|
68
|
+
const FRAME_TOKEN_MAX_LENGTH = 2048;
|
|
69
|
+
/** A frame release id: the release's name or source commit. */
|
|
70
|
+
const RELEASE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
71
|
+
function oneOf(values) {
|
|
72
|
+
return (value) => typeof value === 'string' && values.includes(value);
|
|
73
|
+
}
|
|
74
|
+
const isVersion = (value) => value === SEALED_FRAME_MESSAGE_VERSION;
|
|
75
|
+
const isBoolean = (value) => typeof value === 'boolean';
|
|
76
|
+
const isAccount = (value) => typeof value === 'string' && ACCOUNT.test(value);
|
|
77
|
+
const isAccountOrNull = (value) => value === null || isAccount(value);
|
|
78
|
+
const isRelease = (value) => typeof value === 'string' && RELEASE.test(value);
|
|
79
|
+
const isProtocol = (value) => typeof value === 'number' && Number.isSafeInteger(value) && value >= 1 && value <= 65535;
|
|
80
|
+
const isTime = (value) => typeof value === 'number' && Number.isSafeInteger(value) && value > 0;
|
|
81
|
+
/** Whether a value is a frame token in the API's format. */
|
|
82
|
+
export function isSealedFrameToken(value) {
|
|
83
|
+
return typeof value === 'string' && value.length <= FRAME_TOKEN_MAX_LENGTH && FRAME_TOKEN.test(value);
|
|
84
|
+
}
|
|
85
|
+
const field = (rule) => ({ rule });
|
|
86
|
+
const optional = (rule) => ({ rule, optional: true });
|
|
87
|
+
const HELLO_SHAPES = {
|
|
88
|
+
'sealed/hello': {
|
|
89
|
+
v: field(isVersion),
|
|
90
|
+
host: field(oneOf(SEALED_FRAME_HOST_KINDS)),
|
|
91
|
+
scheme: field(oneOf(SEALED_FRAME_SCHEMES)),
|
|
92
|
+
layout: field(oneOf(SEALED_FRAME_LAYOUTS)),
|
|
93
|
+
signedIn: field(isBoolean),
|
|
94
|
+
account: field(isAccountOrNull),
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
const HOST_SHAPES = {
|
|
98
|
+
token: { v: field(isVersion), token: field(isSealedFrameToken), expiresAt: field(isTime), account: field(isAccount) },
|
|
99
|
+
theme: { v: field(isVersion), scheme: field(oneOf(SEALED_FRAME_SCHEMES)) },
|
|
100
|
+
layout: { v: field(isVersion), layout: field(oneOf(SEALED_FRAME_LAYOUTS)) },
|
|
101
|
+
account: { v: field(isVersion), account: field(isAccount) },
|
|
102
|
+
'signed-out': { v: field(isVersion) },
|
|
103
|
+
'new-chat': { v: field(isVersion) },
|
|
104
|
+
};
|
|
105
|
+
const FRAME_SHAPES = {
|
|
106
|
+
ready: { v: field(isVersion), protocol: field(isProtocol), release: field(isRelease) },
|
|
107
|
+
'need-token': { v: field(isVersion) },
|
|
108
|
+
state: {
|
|
109
|
+
v: field(isVersion),
|
|
110
|
+
state: field(oneOf(['ready', 'unavailable'])),
|
|
111
|
+
reason: optional(oneOf(SEALED_FRAME_UNAVAILABLE_REASONS)),
|
|
112
|
+
},
|
|
113
|
+
navigate: { v: field(isVersion), to: field(oneOf(SEALED_FRAME_NAVIGATE_TARGETS)) },
|
|
114
|
+
wiped: { v: field(isVersion) },
|
|
115
|
+
};
|
|
116
|
+
const WIPE_SHAPES = {
|
|
117
|
+
'sealed/wipe': { v: field(isVersion), account: field(isAccountOrNull) },
|
|
118
|
+
};
|
|
119
|
+
/** More keys than any message has. */
|
|
120
|
+
const MAX_KEYS = 8;
|
|
121
|
+
const hasOwn = (object, key) => Object.hasOwn(object, key);
|
|
122
|
+
function utf8Length(text) {
|
|
123
|
+
return new TextEncoder().encode(text).length;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* The message `data` is, checked against `shapes`, as a fresh object with only its own fields; null for anything else.
|
|
127
|
+
* Never throws.
|
|
128
|
+
*/
|
|
129
|
+
function readShape(data, shapes) {
|
|
130
|
+
try {
|
|
131
|
+
// A plain object (from any realm: structured clone makes one in the receiver's), not an array or a class instance.
|
|
132
|
+
if (typeof data !== 'object' || data === null || Array.isArray(data))
|
|
133
|
+
return null;
|
|
134
|
+
if (Object.prototype.toString.call(data) !== '[object Object]')
|
|
135
|
+
return null;
|
|
136
|
+
const proto = Object.getPrototypeOf(data);
|
|
137
|
+
if (proto !== null && Object.getPrototypeOf(proto) !== null)
|
|
138
|
+
return null;
|
|
139
|
+
const keys = Reflect.ownKeys(data);
|
|
140
|
+
if (keys.length > MAX_KEYS)
|
|
141
|
+
return null;
|
|
142
|
+
// No prototype: an own `__proto__` key (JSON.parse and structured clone keep one) is then copied as a plain key and
|
|
143
|
+
// refused below as an extra field, instead of reaching Object.prototype's `__proto__` setter and vanishing.
|
|
144
|
+
const values = Object.create(null);
|
|
145
|
+
for (const key of keys) {
|
|
146
|
+
if (typeof key !== 'string')
|
|
147
|
+
return null;
|
|
148
|
+
const descriptor = Object.getOwnPropertyDescriptor(data, key);
|
|
149
|
+
// Data properties only: a getter could answer differently on each read.
|
|
150
|
+
if (!descriptor || !('value' in descriptor) || !descriptor.enumerable)
|
|
151
|
+
return null;
|
|
152
|
+
const value = descriptor.value;
|
|
153
|
+
if (value !== null && typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean')
|
|
154
|
+
return null;
|
|
155
|
+
if (typeof value === 'string' && value.length > SEALED_FRAME_MESSAGE_MAX_BYTES)
|
|
156
|
+
return null;
|
|
157
|
+
values[key] = value;
|
|
158
|
+
}
|
|
159
|
+
if (utf8Length(JSON.stringify(values)) > SEALED_FRAME_MESSAGE_MAX_BYTES)
|
|
160
|
+
return null;
|
|
161
|
+
const type = values.t;
|
|
162
|
+
if (typeof type !== 'string' || !hasOwn(shapes, type))
|
|
163
|
+
return null;
|
|
164
|
+
const shape = shapes[type];
|
|
165
|
+
for (const key of Object.keys(values)) {
|
|
166
|
+
if (key !== 't' && !hasOwn(shape, key))
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
const message = { t: type };
|
|
170
|
+
for (const [key, { rule, optional: isOptional }] of Object.entries(shape)) {
|
|
171
|
+
if (!hasOwn(values, key)) {
|
|
172
|
+
if (isOptional)
|
|
173
|
+
continue;
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
if (!rule(values[key]))
|
|
177
|
+
return null;
|
|
178
|
+
message[key] = values[key];
|
|
179
|
+
}
|
|
180
|
+
return message;
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
/** A `sealed/hello`, as the frame reads it; null for anything else. */
|
|
187
|
+
export function parseSealedFrameHello(data) {
|
|
188
|
+
const message = readShape(data, HELLO_SHAPES);
|
|
189
|
+
if (!message)
|
|
190
|
+
return null;
|
|
191
|
+
// Signed in exactly when an account is named.
|
|
192
|
+
if ((message.signedIn === true) !== (message.account !== null))
|
|
193
|
+
return null;
|
|
194
|
+
return message;
|
|
195
|
+
}
|
|
196
|
+
/** A host-to-frame port message, as the frame reads it; null for anything else. */
|
|
197
|
+
export function parseSealedHostMessage(data) {
|
|
198
|
+
return readShape(data, HOST_SHAPES);
|
|
199
|
+
}
|
|
200
|
+
/** A frame-to-host port message, as the host reads it; null for anything else. */
|
|
201
|
+
export function parseSealedFrameMessage(data) {
|
|
202
|
+
const message = readShape(data, FRAME_SHAPES);
|
|
203
|
+
if (!message)
|
|
204
|
+
return null;
|
|
205
|
+
// A reason exactly when unavailable.
|
|
206
|
+
if (message.t === 'state' && (message.state === 'unavailable') !== hasOwn(message, 'reason'))
|
|
207
|
+
return null;
|
|
208
|
+
return message;
|
|
209
|
+
}
|
|
210
|
+
/** A `sealed/wipe`, as the sign-out page reads it; null for anything else. */
|
|
211
|
+
export function parseSealedWipeRequest(data) {
|
|
212
|
+
return readShape(data, WIPE_SHAPES);
|
|
213
|
+
}
|
|
214
|
+
// --- builders ------------------------------------------------------------------------------------------------------
|
|
215
|
+
function checked(message, what) {
|
|
216
|
+
if (message === null)
|
|
217
|
+
throw new TypeError(`Not a valid Sealed frame ${what} message. Nothing was sent.`);
|
|
218
|
+
return message;
|
|
219
|
+
}
|
|
220
|
+
/** The host's `sealed/hello`. `account` is the signed-in member's user id, or null when no one is signed in. */
|
|
221
|
+
export function sealedFrameHello(input) {
|
|
222
|
+
const hello = {
|
|
223
|
+
t: 'sealed/hello',
|
|
224
|
+
v: 1,
|
|
225
|
+
host: input.host,
|
|
226
|
+
scheme: input.scheme,
|
|
227
|
+
layout: input.layout,
|
|
228
|
+
signedIn: input.account !== null,
|
|
229
|
+
account: input.account,
|
|
230
|
+
};
|
|
231
|
+
return checked(parseSealedFrameHello(hello), 'hello');
|
|
232
|
+
}
|
|
233
|
+
/** The host's port messages. */
|
|
234
|
+
export const sealedHostMessage = {
|
|
235
|
+
token: (token, expiresAt, account) => checked(parseSealedHostMessage({ t: 'token', v: 1, token, expiresAt, account }), 'token'),
|
|
236
|
+
theme: (scheme) => checked(parseSealedHostMessage({ t: 'theme', v: 1, scheme }), 'theme'),
|
|
237
|
+
layout: (layout) => checked(parseSealedHostMessage({ t: 'layout', v: 1, layout }), 'layout'),
|
|
238
|
+
account: (account) => checked(parseSealedHostMessage({ t: 'account', v: 1, account }), 'account'),
|
|
239
|
+
signedOut: () => checked(parseSealedHostMessage({ t: 'signed-out', v: 1 }), 'signed-out'),
|
|
240
|
+
newChat: () => checked(parseSealedHostMessage({ t: 'new-chat', v: 1 }), 'new-chat'),
|
|
241
|
+
};
|
|
242
|
+
/**
|
|
243
|
+
* The frame's port messages. Each takes only enums, numbers and the frame's compiled release id, so nothing the member
|
|
244
|
+
* typed or read can be put in one.
|
|
245
|
+
*/
|
|
246
|
+
export const sealedFrameMessage = {
|
|
247
|
+
ready: (release) => checked(parseSealedFrameMessage({ t: 'ready', v: 1, protocol: SEALED_FRAME_PROTOCOL, release }), 'ready'),
|
|
248
|
+
needToken: () => checked(parseSealedFrameMessage({ t: 'need-token', v: 1 }), 'need-token'),
|
|
249
|
+
stateReady: () => checked(parseSealedFrameMessage({ t: 'state', v: 1, state: 'ready' }), 'state'),
|
|
250
|
+
unavailable: (reason) => checked(parseSealedFrameMessage({ t: 'state', v: 1, state: 'unavailable', reason }), 'state'),
|
|
251
|
+
navigate: (to) => checked(parseSealedFrameMessage({ t: 'navigate', v: 1, to }), 'navigate'),
|
|
252
|
+
wiped: () => checked(parseSealedFrameMessage({ t: 'wiped', v: 1 }), 'wiped'),
|
|
253
|
+
};
|
|
254
|
+
/** The host's `sealed/wipe` for the sign-out page. `account` null wipes every account's Sealed data in the browser. */
|
|
255
|
+
export function sealedWipeRequest(account) {
|
|
256
|
+
return checked(parseSealedWipeRequest({ t: 'sealed/wipe', v: 1, account }), 'wipe');
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* The frame origin as an exact origin (`https://sealed.minds.com`), or a TypeError. Only https, or http on localhost
|
|
260
|
+
* for development; no path, query, fragment or credentials.
|
|
261
|
+
*/
|
|
262
|
+
export function sealedFrameOrigin(value) {
|
|
263
|
+
let url;
|
|
264
|
+
try {
|
|
265
|
+
url = new URL(value);
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
throw new TypeError('frameOrigin must be an origin such as https://sealed.minds.com');
|
|
269
|
+
}
|
|
270
|
+
const local = url.hostname === 'localhost' || url.hostname === '127.0.0.1' || url.hostname === '[::1]';
|
|
271
|
+
if (url.username || url.password || url.search || url.hash || url.pathname !== '/'
|
|
272
|
+
|| !(url.protocol === 'https:' || (url.protocol === 'http:' && local))) {
|
|
273
|
+
throw new TypeError('frameOrigin must be an https origin with no path, such as https://sealed.minds.com');
|
|
274
|
+
}
|
|
275
|
+
return url.origin;
|
|
276
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface WipeSealedFrameOptions {
|
|
2
|
+
/** The frame origin, e.g. `https://sealed.minds.com`. */
|
|
3
|
+
frameOrigin: string;
|
|
4
|
+
/** The account signing out (its user id); null wipes every account's Sealed data in this browser. */
|
|
5
|
+
account: string | null;
|
|
6
|
+
/** How long to wait in all, page load included, before giving up. 1500 ms by default. */
|
|
7
|
+
timeoutMs?: number;
|
|
8
|
+
/** The document to load the page in; the global one by default. */
|
|
9
|
+
document?: Document;
|
|
10
|
+
}
|
|
11
|
+
/** How long a sign-out waits for the Sealed data to go before it carries on (the frame design, section 7). */
|
|
12
|
+
export declare const SEALED_WIPE_TIMEOUT_MS = 1500;
|
|
13
|
+
/**
|
|
14
|
+
* Clears an account's Sealed chats from this browser at sign-out, whether or not a Sealed frame is open: it loads the
|
|
15
|
+
* frame origin's sign-out page (/wipe.html) in a hidden iframe, posts `sealed/wipe` to that origin only with a
|
|
16
|
+
* transferred MessagePort, and waits for `wiped` on the port. Resolves `wiped`, or `timeout` once `timeoutMs` has passed
|
|
17
|
+
* in all (page load included) so sign-out never hangs on it. Either way the iframe is removed. Call it on every
|
|
18
|
+
* sign-out path before the page reloads.
|
|
19
|
+
*
|
|
20
|
+
* The page writes a pending-wipe record first, so a wipe cut short by the reload finishes on the frame's next load.
|
|
21
|
+
* Without a DOM (React Native, a server render) it resolves `timeout` at once and does nothing.
|
|
22
|
+
*
|
|
23
|
+
* Only the port carries the answer: a window message, from any origin or window, is never read.
|
|
24
|
+
*/
|
|
25
|
+
export declare function wipeSealedFrame(options: WipeSealedFrameOptions): Promise<'wiped' | 'timeout'>;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { parseSealedFrameMessage, SEALED_FRAME_REFERRER_POLICY, SEALED_WIPE_PATH, SEALED_WIPE_SANDBOX, sealedFrameOrigin, sealedWipeRequest, } from './protocol.js';
|
|
2
|
+
/** How long a sign-out waits for the Sealed data to go before it carries on (the frame design, section 7). */
|
|
3
|
+
export const SEALED_WIPE_TIMEOUT_MS = 1500;
|
|
4
|
+
/**
|
|
5
|
+
* Clears an account's Sealed chats from this browser at sign-out, whether or not a Sealed frame is open: it loads the
|
|
6
|
+
* frame origin's sign-out page (/wipe.html) in a hidden iframe, posts `sealed/wipe` to that origin only with a
|
|
7
|
+
* transferred MessagePort, and waits for `wiped` on the port. Resolves `wiped`, or `timeout` once `timeoutMs` has passed
|
|
8
|
+
* in all (page load included) so sign-out never hangs on it. Either way the iframe is removed. Call it on every
|
|
9
|
+
* sign-out path before the page reloads.
|
|
10
|
+
*
|
|
11
|
+
* The page writes a pending-wipe record first, so a wipe cut short by the reload finishes on the frame's next load.
|
|
12
|
+
* Without a DOM (React Native, a server render) it resolves `timeout` at once and does nothing.
|
|
13
|
+
*
|
|
14
|
+
* Only the port carries the answer: a window message, from any origin or window, is never read.
|
|
15
|
+
*/
|
|
16
|
+
export function wipeSealedFrame(options) {
|
|
17
|
+
const frameOrigin = sealedFrameOrigin(options.frameOrigin);
|
|
18
|
+
const request = sealedWipeRequest(options.account);
|
|
19
|
+
const timeoutMs = options.timeoutMs ?? SEALED_WIPE_TIMEOUT_MS;
|
|
20
|
+
const doc = options.document ?? (typeof document === 'undefined' ? undefined : document);
|
|
21
|
+
if (!doc?.body || typeof MessageChannel === 'undefined')
|
|
22
|
+
return Promise.resolve('timeout');
|
|
23
|
+
return new Promise((resolve) => {
|
|
24
|
+
const iframe = doc.createElement('iframe');
|
|
25
|
+
let port = null;
|
|
26
|
+
let done = false;
|
|
27
|
+
const finish = (result) => {
|
|
28
|
+
if (done)
|
|
29
|
+
return;
|
|
30
|
+
done = true;
|
|
31
|
+
clearTimeout(timer);
|
|
32
|
+
iframe.removeEventListener('load', onLoad);
|
|
33
|
+
if (port) {
|
|
34
|
+
port.onmessage = null;
|
|
35
|
+
port.close();
|
|
36
|
+
}
|
|
37
|
+
iframe.remove();
|
|
38
|
+
resolve(result);
|
|
39
|
+
};
|
|
40
|
+
const onLoad = () => {
|
|
41
|
+
const target = iframe.contentWindow;
|
|
42
|
+
if (done || !target)
|
|
43
|
+
return;
|
|
44
|
+
// A second load (the page reloaded itself) gets a fresh port; the old one is closed and never read again.
|
|
45
|
+
if (port) {
|
|
46
|
+
port.onmessage = null;
|
|
47
|
+
port.close();
|
|
48
|
+
}
|
|
49
|
+
const channel = new MessageChannel();
|
|
50
|
+
port = channel.port1;
|
|
51
|
+
channel.port1.onmessage = (event) => {
|
|
52
|
+
if (parseSealedFrameMessage(event.data)?.t === 'wiped')
|
|
53
|
+
finish('wiped');
|
|
54
|
+
};
|
|
55
|
+
target.postMessage(request, frameOrigin, [channel.port2]);
|
|
56
|
+
};
|
|
57
|
+
const timer = setTimeout(() => finish('timeout'), timeoutMs);
|
|
58
|
+
iframe.setAttribute('sandbox', SEALED_WIPE_SANDBOX);
|
|
59
|
+
iframe.setAttribute('referrerpolicy', SEALED_FRAME_REFERRER_POLICY);
|
|
60
|
+
iframe.setAttribute('aria-hidden', 'true');
|
|
61
|
+
iframe.tabIndex = -1;
|
|
62
|
+
iframe.style.display = 'none';
|
|
63
|
+
iframe.addEventListener('load', onLoad);
|
|
64
|
+
iframe.src = `${frameOrigin}${SEALED_WIPE_PATH}`;
|
|
65
|
+
doc.body.appendChild(iframe);
|
|
66
|
+
});
|
|
67
|
+
}
|
package/llm.md
CHANGED
|
@@ -856,6 +856,13 @@ const { data: capabilities } = await r.privacy.capabilities()
|
|
|
856
856
|
const showNoPushLine = isFactLive('sealed.no_push', capabilities)
|
|
857
857
|
const showCheckedLine = isFactLive('sealed.attest_before_send', capabilities, { deviceFacts: ['sealed.attest_before_send'] })
|
|
858
858
|
```
|
|
859
|
+
- `r.sealed` web frame routes (unreleased; server switch `config().web`, SEALED_GATEWAY_WEB) — `frameToken()` → `{ token, expiresAt, expiresIn?, origin }` (`expiresIn` only once the gateway sends `expires_in`): a short-lived `sft_` credential for the Sealed frame, minted from the member's own app key (never an API key; accepted only from `origin`, only on the frame's routes, never to mint another). `me()` → `{ user_id, privacy, content, data }` (GET /sealed/me; `data` repeats the fields so a `settings.getPreferences()` reader takes it unchanged; through a frame token `content.sealed_adult` is always false). `report({ reason, details? })` → `{ id, existing, details_kept }` (filed on the member's own account; `details` at most 2000 characters). All three are sent once, never retried, and throw AuthorizationError `sealed_web_off` while web Sealed is off (`isSealedWebOff(error)`). `@recursiv/sdk/sealed` carries only `HttpClient`, `SealedResource` and the errors, for the frame's bundle. `@recursiv/sdk/sealed-frame` (browser only) is protocol v1 for both sides: the message types, builders and validators (`parseSealedFrameHello`, `parseSealedHostMessage`, `parseSealedFrameMessage`, `parseSealedWipeRequest`; null for anything unknown, mistyped, outside its enum or over 4 KB), `mountSealedFrame({ container, frameOrigin, hostKind, scheme, layout, account, mintToken, onState, onNavigate, onTokenError? })` → `{ iframe, setTheme, setLayout, setAccount, signedOut, newChat, destroy }` (sandboxed iframe, hello + MessagePort on every load, window messages never read, one shared mint cached until 60 s before expiry, at least 30 s between mints whatever the browser clock says; `signedOut()` also runs the sign-out page and reloads a frame that has not said ready), and `wipeSealedFrame({ frameOrigin, account, timeoutMs? })` → `'wiped' | 'timeout'` for every sign-out path.
|
|
860
|
+
|
|
861
|
+
```typescript
|
|
862
|
+
import { mountSealedFrame, wipeSealedFrame } from '@recursiv/sdk/sealed-frame'
|
|
863
|
+
const frame = mountSealedFrame({ container, frameOrigin: 'https://sealed.minds.com', hostKind: 'minds', scheme: 'dark', layout: 'column', account: me.id, mintToken: () => r.sealed.frameToken(), onState: setSealedState, onNavigate: go })
|
|
864
|
+
// sign-out: await frame.signedOut() (no frame mounted: await wipeSealedFrame({ frameOrigin, account: me.id }))
|
|
865
|
+
```
|
|
859
866
|
- `r.billing` — Usage tracking, checkout sessions, portal sessions, metered billing
|
|
860
867
|
- `r.appSubscriptions` — Project-scoped consumer plans: `checkout` (409 `subscription_exists` when the member already pays), `status`, `createPortalSession({ return_url, flow?: 'cancel' })` (`flow: 'cancel'` opens Stripe's cancel confirmation; 409 `no_subscription` when there is no plan to cancel), `previewTierChange({ tier })`, `changeTier({ tier, proration_date? })` (in-place, prorated, never a second subscription)
|
|
861
868
|
- `r.boosts` — Promoted posts (micro-USD): `create`, `mine`, `balance`, `pause`, `resume`, `cancel`, `recordEvent`; buy credit with `topup({ amount_cents, return_url })` then `confirmTopup(sessionId)` (credits once); admin `listPendingReview`, `review`, `grant`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@recursiv/sdk",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.122",
|
|
4
4
|
"description": "TypeScript SDK for the Recursiv API",
|
|
5
5
|
"license": "FSL-1.1-ALv2",
|
|
6
6
|
"type": "module",
|
|
@@ -32,6 +32,16 @@
|
|
|
32
32
|
"types": "./dist/privacy/index.d.ts",
|
|
33
33
|
"import": "./dist/privacy/index.js",
|
|
34
34
|
"default": "./dist/privacy/index.js"
|
|
35
|
+
},
|
|
36
|
+
"./sealed": {
|
|
37
|
+
"types": "./dist/sealed/index.d.ts",
|
|
38
|
+
"import": "./dist/sealed/index.js",
|
|
39
|
+
"default": "./dist/sealed/index.js"
|
|
40
|
+
},
|
|
41
|
+
"./sealed-frame": {
|
|
42
|
+
"types": "./dist/sealed-frame/index.d.ts",
|
|
43
|
+
"import": "./dist/sealed-frame/index.js",
|
|
44
|
+
"default": "./dist/sealed-frame/index.js"
|
|
35
45
|
}
|
|
36
46
|
},
|
|
37
47
|
"files": [
|