@botiverse/porthole 0.1.3 → 0.1.4
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/dist/compile/policy.d.ts +6 -0
- package/dist/compile/policy.js +9 -1
- package/dist/config/schema.d.ts +11 -0
- package/dist/config/validate.js +12 -0
- package/dist/conformance/served.d.ts +8 -1
- package/dist/conformance/served.js +12 -8
- package/dist/guide/guide.js +3 -1
- package/dist/host/create-host.js +24 -7
- package/dist/host/types.d.ts +44 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/presets/presets.d.ts +3 -1
- package/dist/presets/presets.js +1 -0
- package/dist/react/frame.d.ts +34 -4
- package/dist/react/frame.js +80 -18
- package/dist/server/naming.d.ts +6 -0
- package/dist/server/naming.js +38 -0
- package/dist/server/pages.js +2 -0
- package/dist/server/server.js +49 -17
- package/dist/server/types.d.ts +6 -3
- package/dist/shell/annotate.js +31 -0
- package/dist/shell/document.d.ts +6 -0
- package/dist/shell/document.js +8 -2
- package/dist/shell/runtime.js +33 -1
- package/package.json +1 -1
- package/src/compile/policy.ts +10 -1
- package/src/config/schema.ts +12 -0
- package/src/config/validate.ts +15 -0
- package/src/conformance/served.ts +24 -9
- package/src/guide/guide.ts +3 -1
- package/src/host/create-host.ts +26 -7
- package/src/host/types.ts +39 -2
- package/src/index.ts +2 -0
- package/src/presets/presets.ts +4 -0
- package/src/react/frame.tsx +125 -23
- package/src/server/naming.ts +41 -0
- package/src/server/pages.ts +2 -0
- package/src/server/server.ts +61 -20
- package/src/server/types.ts +6 -3
- package/src/shell/annotate.ts +31 -0
- package/src/shell/document.ts +15 -2
- package/src/shell/runtime.ts +33 -1
package/dist/compile/policy.d.ts
CHANGED
|
@@ -55,3 +55,9 @@ export declare function embedderFrameSources(config: PortholeConfig): string[];
|
|
|
55
55
|
* engine Porthole supports reads frames from `frame-src`, P038).
|
|
56
56
|
*/
|
|
57
57
|
export declare function embedderCsp(configs: readonly PortholeConfig[]): string;
|
|
58
|
+
/**
|
|
59
|
+
* The `frame-src` sources the embedding page must allow for every kind of display it shows, as a
|
|
60
|
+
* list, for a host that writes its own policy around them (empty: it frames nothing of
|
|
61
|
+
* Porthole's but `srcdoc`, which needs none).
|
|
62
|
+
*/
|
|
63
|
+
export declare function embedderFrameSourcesOf(configs: readonly PortholeConfig[]): string[];
|
package/dist/compile/policy.js
CHANGED
|
@@ -102,6 +102,14 @@ export function embedderFrameSources(config) {
|
|
|
102
102
|
* engine Porthole supports reads frames from `frame-src`, P038).
|
|
103
103
|
*/
|
|
104
104
|
export function embedderCsp(configs) {
|
|
105
|
-
const sources =
|
|
105
|
+
const sources = embedderFrameSourcesOf(configs);
|
|
106
106
|
return serializeCsp([["frame-src", ...(sources.length === 0 ? ["'none'"] : sources)]]);
|
|
107
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* The `frame-src` sources the embedding page must allow for every kind of display it shows, as a
|
|
110
|
+
* list, for a host that writes its own policy around them (empty: it frames nothing of
|
|
111
|
+
* Porthole's but `srcdoc`, which needs none).
|
|
112
|
+
*/
|
|
113
|
+
export function embedderFrameSourcesOf(configs) {
|
|
114
|
+
return unique(configs.flatMap(embedderFrameSources));
|
|
115
|
+
}
|
package/dist/config/schema.d.ts
CHANGED
|
@@ -157,4 +157,15 @@ export interface PortholeConfig {
|
|
|
157
157
|
readonly sandbox?: readonly UrlSandboxExtra[];
|
|
158
158
|
/** Url mode only: features delegated to the frame (`URL_PERMISSIONS`); none by default. */
|
|
159
159
|
readonly permissions?: readonly UrlPermission[];
|
|
160
|
+
/**
|
|
161
|
+
* Domain isolation: how far a display's storage reaches, for the manual. Storage is per origin,
|
|
162
|
+
* and the host picks each display's origin (`InstanceRef.scope`); a host that gives several
|
|
163
|
+
* displays one scope (all of a conversation's widgets) names it here (`{ shared:
|
|
164
|
+
* "conversation" }`), so the manual says storage is shared across it and keys need a prefix.
|
|
165
|
+
*/
|
|
166
|
+
readonly storage?: StorageConfig;
|
|
167
|
+
}
|
|
168
|
+
export interface StorageConfig {
|
|
169
|
+
/** What the displays sharing one origin have in common, as a noun ("conversation"). */
|
|
170
|
+
readonly shared?: string;
|
|
160
171
|
}
|
package/dist/config/validate.js
CHANGED
|
@@ -111,6 +111,7 @@ export function validateConfig(input) {
|
|
|
111
111
|
"embedder",
|
|
112
112
|
"sandbox",
|
|
113
113
|
"permissions",
|
|
114
|
+
"storage",
|
|
114
115
|
], "config");
|
|
115
116
|
if (!MODES.includes(config.mode)) {
|
|
116
117
|
fail("config.mode", `must be one of ${MODES.join(", ")}`);
|
|
@@ -258,6 +259,17 @@ export function validateConfig(input) {
|
|
|
258
259
|
}
|
|
259
260
|
});
|
|
260
261
|
}
|
|
262
|
+
if (config.storage !== undefined) {
|
|
263
|
+
const storage = object(config.storage, "config.storage");
|
|
264
|
+
only(storage, ["shared"], "config.storage");
|
|
265
|
+
if (config.isolation !== "domain") {
|
|
266
|
+
fail("config.storage", "only domain isolation keeps storage across displays");
|
|
267
|
+
}
|
|
268
|
+
if (storage.shared !== undefined &&
|
|
269
|
+
(typeof storage.shared !== "string" || !/^[a-z][a-z -]{0,39}$/u.test(storage.shared))) {
|
|
270
|
+
fail("config.storage.shared", 'must be a short lowercase noun ("conversation")');
|
|
271
|
+
}
|
|
272
|
+
}
|
|
261
273
|
if (config.permissions !== undefined) {
|
|
262
274
|
if (config.mode !== "url") {
|
|
263
275
|
fail("config.permissions", "only url mode may be delegated features");
|
|
@@ -35,8 +35,15 @@ export interface ServedConformanceHost {
|
|
|
35
35
|
* Shows `instance` in `page` as the host does: a fresh ticket from its backend, framed under
|
|
36
36
|
* the config's sandbox, in an embedding page that sends the config's `embedderCsp`. The same page
|
|
37
37
|
* may show several, one after another.
|
|
38
|
+
*
|
|
39
|
+
* For a served shell the suite gives `options.content`, a line of HTML: a host that speaks to
|
|
40
|
+
* the shell (`createHost`, the React binding) sends it as the display's content, since a shell
|
|
41
|
+
* draws nothing (and reports no height) until it has content, and the cases click inside it. A
|
|
42
|
+
* host whose frame has a height of its own may ignore it.
|
|
38
43
|
*/
|
|
39
|
-
show(page: Page, instance: InstanceRef
|
|
44
|
+
show(page: Page, instance: InstanceRef, options?: {
|
|
45
|
+
readonly content?: string;
|
|
46
|
+
}): Promise<ServedDisplay>;
|
|
40
47
|
/** A fresh ticketed location for `instance`, as the host's backend issues it. */
|
|
41
48
|
locate(instance: InstanceRef): Promise<FrameLocation>;
|
|
42
49
|
/** Navigates `page` to a page on an origin that is not one of the host's, framing `url`. */
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
import { expect } from "@playwright/test";
|
|
15
15
|
import { MCP_APPS_METHODS } from "../protocol/mcp-apps.js";
|
|
16
16
|
const SETTLE_MS = 800;
|
|
17
|
+
/** What a served shell is given to draw (`ServedConformanceHost.show`'s `content`). */
|
|
18
|
+
const SHELL_CONTENT = '<p id="porthole-probe-content">Porthole conformance probe</p>';
|
|
17
19
|
/** What the frame can see of its host: everything here must be closed to it. */
|
|
18
20
|
async function reachIntoHost(frame) {
|
|
19
21
|
return frame.evaluate(() => {
|
|
@@ -109,21 +111,23 @@ export function servedTests(host, options = {}) {
|
|
|
109
111
|
["domain", (scope) => host.shell(scope)],
|
|
110
112
|
["url", (scope) => host.app(scope)],
|
|
111
113
|
];
|
|
114
|
+
// A served shell draws (and has a height to click in) once its host gives it content.
|
|
115
|
+
const showMode = (page, mode, instance) => host.show(page, instance, mode === "domain" ? { content: SHELL_CONTENT } : {});
|
|
112
116
|
for (const [mode, instance] of modes) {
|
|
113
117
|
define(`served.${mode}.origin`, "content runs on its own origin, never the host's", async ({ page }) => {
|
|
114
|
-
const display = await
|
|
118
|
+
const display = await showMode(page, mode, instance(`${mode}-origin`));
|
|
115
119
|
const frame = await display.frame();
|
|
116
120
|
const origin = await frame.evaluate(() => window.origin);
|
|
117
121
|
expect(origin).toBe(display.origin);
|
|
118
122
|
expect(origin).not.toBe(new URL(page.url()).origin);
|
|
119
123
|
});
|
|
120
124
|
define(`served.${mode}.no-reach`, "content can't reach the host's DOM, storage, cookies or URL", async ({ page }) => {
|
|
121
|
-
const display = await
|
|
125
|
+
const display = await showMode(page, mode, instance(`${mode}-reach`));
|
|
122
126
|
await plantHostSecrets(page);
|
|
123
127
|
expect(await reachIntoHost(await display.frame())).toEqual(CLOSED);
|
|
124
128
|
});
|
|
125
129
|
define(`served.${mode}.no-top-navigation`, "content can't navigate the host's page, even after a click", async ({ page }) => {
|
|
126
|
-
const display = await
|
|
130
|
+
const display = await showMode(page, mode, instance(`${mode}-top`));
|
|
127
131
|
const frame = await display.frame();
|
|
128
132
|
const before = page.url();
|
|
129
133
|
const target = await host.canary(`${mode}-top`);
|
|
@@ -153,7 +157,7 @@ export function servedTests(host, options = {}) {
|
|
|
153
157
|
expect(await host.canaryHits(`${mode}-top`)).toBe(0);
|
|
154
158
|
});
|
|
155
159
|
define(`served.${mode}.no-popups`, "content can't open a window, even after a click", async ({ page }) => {
|
|
156
|
-
const display = await
|
|
160
|
+
const display = await showMode(page, mode, instance(`${mode}-popup`));
|
|
157
161
|
const frame = await display.frame();
|
|
158
162
|
const target = await host.canary(`${mode}-popup`);
|
|
159
163
|
const opened = [];
|
|
@@ -186,13 +190,13 @@ export function servedTests(host, options = {}) {
|
|
|
186
190
|
});
|
|
187
191
|
}
|
|
188
192
|
define("served.domain.storage-scope", "storage persists within a scope and is never shared across scopes", async ({ page }) => {
|
|
189
|
-
const first = await host.show(page, host.shell("scope-a"));
|
|
193
|
+
const first = await host.show(page, host.shell("scope-a"), { content: SHELL_CONTENT });
|
|
190
194
|
await (await first.frame()).evaluate(() => localStorage.setItem("kept", "a"));
|
|
191
195
|
await first.destroy();
|
|
192
|
-
const again = await host.show(page, host.shell("scope-a"));
|
|
196
|
+
const again = await host.show(page, host.shell("scope-a"), { content: SHELL_CONTENT });
|
|
193
197
|
expect(await (await again.frame()).evaluate(() => localStorage.getItem("kept"))).toBe("a");
|
|
194
198
|
expect(again.origin).toBe(first.origin);
|
|
195
|
-
const other = await host.show(page, host.shell("scope-b"));
|
|
199
|
+
const other = await host.show(page, host.shell("scope-b"), { content: SHELL_CONTENT });
|
|
196
200
|
expect(other.origin).not.toBe(first.origin);
|
|
197
201
|
expect(await (await other.frame()).evaluate(() => localStorage.getItem("kept"))).toBeNull();
|
|
198
202
|
});
|
|
@@ -247,7 +251,7 @@ export function servedTests(host, options = {}) {
|
|
|
247
251
|
}
|
|
248
252
|
});
|
|
249
253
|
define("served.domain.egress", "the CSP in the document's headers closes egress", async ({ page }) => {
|
|
250
|
-
const display = await host.show(page, host.shell("egress"));
|
|
254
|
+
const display = await host.show(page, host.shell("egress"), { content: SHELL_CONTENT });
|
|
251
255
|
const frame = await display.frame();
|
|
252
256
|
const fetched = await frame.evaluate(async (url) => {
|
|
253
257
|
try {
|
package/dist/guide/guide.js
CHANGED
|
@@ -206,7 +206,9 @@ function interactive(c) {
|
|
|
206
206
|
.filter((entry) => entry !== "");
|
|
207
207
|
const state = config.isolation === "opaque"
|
|
208
208
|
? `- State isn't saved: localStorage works but lasts only while the ${noun} is on screen, and a reload or another device starts fresh. Keep what matters in the ${noun}'s code.`
|
|
209
|
-
:
|
|
209
|
+
: config.storage?.shared === undefined
|
|
210
|
+
? `- localStorage is kept for this ${noun} in this browser; another device starts fresh.`
|
|
211
|
+
: `- localStorage is shared by every ${noun} in this ${config.storage.shared}, in this browser (another device starts fresh): prefix your keys with a name of this ${noun}'s own ("budget:filters", not "filters"), or ${noun}s overwrite each other.`;
|
|
210
212
|
const none = entries.length === 0
|
|
211
213
|
? `- It can only draw: it has no way to send anything to you or the app.`
|
|
212
214
|
: null;
|
package/dist/host/create-host.js
CHANGED
|
@@ -57,6 +57,16 @@ export function createHost(config, options = {}) {
|
|
|
57
57
|
listener(detail);
|
|
58
58
|
}
|
|
59
59
|
};
|
|
60
|
+
// What the content did wrong (the agent's to hear and fix), and what failed on the host's
|
|
61
|
+
// side (its own to handle), each on its own event and on `error` with its source.
|
|
62
|
+
const contentError = (message) => {
|
|
63
|
+
emit("contentError", { message });
|
|
64
|
+
emit("error", { message, source: "content" });
|
|
65
|
+
};
|
|
66
|
+
const hostError = (message, kind) => {
|
|
67
|
+
emit("hostError", { message, kind });
|
|
68
|
+
emit("error", { message, source: "host" });
|
|
69
|
+
};
|
|
60
70
|
let scheme = mount.scheme;
|
|
61
71
|
let height = mount.initialHeightPx ?? 0;
|
|
62
72
|
let surface = null;
|
|
@@ -228,7 +238,7 @@ export function createHost(config, options = {}) {
|
|
|
228
238
|
return;
|
|
229
239
|
}
|
|
230
240
|
case "error":
|
|
231
|
-
|
|
241
|
+
contentError(event.message);
|
|
232
242
|
return;
|
|
233
243
|
case "refused":
|
|
234
244
|
emit("refused", { capability: event.capability, refusal: event.refusal });
|
|
@@ -260,8 +270,9 @@ export function createHost(config, options = {}) {
|
|
|
260
270
|
allow: compiled.allow,
|
|
261
271
|
referrerPolicy: compiled.referrerPolicy,
|
|
262
272
|
colorScheme: scheme,
|
|
263
|
-
// An app reports no size: its frame fills the host's container, at least the minimum
|
|
264
|
-
|
|
273
|
+
// An app reports no size: its frame fills the host's container, at least the minimum;
|
|
274
|
+
// so does any display the host mounts to fill (its content scrolls inside).
|
|
275
|
+
...(speaks && mount.fill !== true
|
|
265
276
|
? { initialHeightPx: height }
|
|
266
277
|
: { fill: true, initialHeightPx: config.limits.minHeightPx }),
|
|
267
278
|
});
|
|
@@ -327,9 +338,7 @@ export function createHost(config, options = {}) {
|
|
|
327
338
|
}
|
|
328
339
|
else {
|
|
329
340
|
stop();
|
|
330
|
-
|
|
331
|
-
message: "The content kept navigating its frame away; it was stopped.",
|
|
332
|
-
});
|
|
341
|
+
contentError("The content kept navigating its frame away; it was stopped.");
|
|
333
342
|
}
|
|
334
343
|
});
|
|
335
344
|
};
|
|
@@ -345,6 +354,7 @@ export function createHost(config, options = {}) {
|
|
|
345
354
|
html: compiled.shellDocument({
|
|
346
355
|
origins: [container.ownerDocument.defaultView?.location.origin ?? ""],
|
|
347
356
|
title: mount.title,
|
|
357
|
+
...nonceOf(options.nonce),
|
|
348
358
|
...(mount.document === undefined ? {} : { document: mount.document }),
|
|
349
359
|
}),
|
|
350
360
|
});
|
|
@@ -365,7 +375,9 @@ export function createHost(config, options = {}) {
|
|
|
365
375
|
}
|
|
366
376
|
}, (error) => {
|
|
367
377
|
if (mine === generation && !controller.signal.aborted) {
|
|
368
|
-
|
|
378
|
+
// The host's own failure (its backend down, access gone): never the content's.
|
|
379
|
+
hostError(error instanceof Error ? error.message : String(error), "locate");
|
|
380
|
+
emit("outcome", { state: "blocked", reason: "locate" });
|
|
369
381
|
}
|
|
370
382
|
});
|
|
371
383
|
};
|
|
@@ -431,3 +443,8 @@ export function createHost(config, options = {}) {
|
|
|
431
443
|
},
|
|
432
444
|
};
|
|
433
445
|
}
|
|
446
|
+
/** The host's nonce option, read now (`HostOptions.nonce`). */
|
|
447
|
+
function nonceOf(nonce) {
|
|
448
|
+
const value = typeof nonce === "function" ? nonce() : nonce;
|
|
449
|
+
return value === undefined || value === "" ? {} : { nonce: value };
|
|
450
|
+
}
|
package/dist/host/types.d.ts
CHANGED
|
@@ -35,6 +35,13 @@ export interface HostOptions<C extends AdapterConnection = ShellConnection> {
|
|
|
35
35
|
readonly platform?: PlatformHost;
|
|
36
36
|
/** Required for domain and external isolation: where an instance's frame loads from. */
|
|
37
37
|
readonly locator?: InstanceLocator;
|
|
38
|
+
/**
|
|
39
|
+
* The embedding page's CSP nonce, where its `script-src` is nonce-based (P054): an opaque frame
|
|
40
|
+
* (`srcdoc`) inherits the page's policy, so the shell and the content's scripts carry it. A
|
|
41
|
+
* function is read each time a frame starts (a page whose nonce changes). Served frames inherit
|
|
42
|
+
* nothing and need none.
|
|
43
|
+
*/
|
|
44
|
+
readonly nonce?: string | (() => string | undefined);
|
|
38
45
|
}
|
|
39
46
|
export interface MountOptions {
|
|
40
47
|
/** The host's id for this display: what its height is remembered by, and calls name. */
|
|
@@ -50,7 +57,21 @@ export interface MountOptions {
|
|
|
50
57
|
readonly document?: string;
|
|
51
58
|
/** The height to start at, before the content reports its own. */
|
|
52
59
|
readonly initialHeightPx?: number;
|
|
60
|
+
/**
|
|
61
|
+
* Fill the container (give it a height) instead of taking the content's height: the content
|
|
62
|
+
* scrolls inside its frame (an artifact viewer's page). The content's sizes are still reported
|
|
63
|
+
* (`resize`). A url-mode app always fills.
|
|
64
|
+
*/
|
|
65
|
+
readonly fill?: boolean;
|
|
53
66
|
}
|
|
67
|
+
/** What failed on the host's side (`hostError`). */
|
|
68
|
+
export type HostErrorKind =
|
|
69
|
+
/** The locator rejected: the host's backend didn't give a location. */
|
|
70
|
+
"locate"
|
|
71
|
+
/** A display couldn't be made (the React binding: a bad config, a missing handler). */
|
|
72
|
+
| "mount"
|
|
73
|
+
/** An annotation call was refused (an invalid mark, an unknown id). */
|
|
74
|
+
| "annotations";
|
|
54
75
|
/** What a display reports, by event name. */
|
|
55
76
|
/** Why a display's frame showed nothing of its content (`FrameOutcome`). */
|
|
56
77
|
export type FrameBlock =
|
|
@@ -63,7 +84,9 @@ export type FrameBlock =
|
|
|
63
84
|
/** Its app didn't answer. */
|
|
64
85
|
| "upstream"
|
|
65
86
|
/** The host's own check refused it (revoked, no longer a member): `code` says which. */
|
|
66
|
-
| "denied"
|
|
87
|
+
| "denied"
|
|
88
|
+
/** The host's locator failed (its backend down, access gone): see the `hostError` event. */
|
|
89
|
+
| "locate" | "not-found" | "error";
|
|
67
90
|
/**
|
|
68
91
|
* How a display's document went: it loaded (a shell or view finished its handshake; a url-mode
|
|
69
92
|
* app's document loaded), it was blocked (and why), or nothing settled in
|
|
@@ -92,9 +115,28 @@ export interface DisplayEvents {
|
|
|
92
115
|
readonly height: number;
|
|
93
116
|
readonly width?: number;
|
|
94
117
|
};
|
|
95
|
-
/**
|
|
118
|
+
/**
|
|
119
|
+
* What the content did wrong: what it threw, what the sandbox blocked (its own words:
|
|
120
|
+
* untrusted), or that it kept navigating its frame away. The agent's to hear and fix.
|
|
121
|
+
*/
|
|
122
|
+
readonly contentError: {
|
|
123
|
+
readonly message: string;
|
|
124
|
+
};
|
|
125
|
+
/**
|
|
126
|
+
* What failed on the host's side (`kind`): its locator, for now. The host's to handle; never
|
|
127
|
+
* the agent's. The display's `outcome` says `blocked: locate` too.
|
|
128
|
+
*/
|
|
129
|
+
readonly hostError: {
|
|
130
|
+
readonly message: string;
|
|
131
|
+
readonly kind: HostErrorKind;
|
|
132
|
+
};
|
|
133
|
+
/**
|
|
134
|
+
* Both of the above, with their `source`: for a host that listened here before they were
|
|
135
|
+
* split (0.1.3). Prefer `contentError` and `hostError`.
|
|
136
|
+
*/
|
|
96
137
|
readonly error: {
|
|
97
138
|
readonly message: string;
|
|
139
|
+
readonly source: "content" | "host";
|
|
98
140
|
};
|
|
99
141
|
/** The content navigated its frame away; the frame was started again from its content. */
|
|
100
142
|
readonly navigated: undefined;
|
package/dist/index.d.ts
CHANGED
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
* frame's policies, the shell, and the agent manual; the host mounts displays under it.
|
|
4
4
|
*/
|
|
5
5
|
export { CAPABILITIES, CAPABILITY_NAMES, MAX_COPY_CHARS, capabilitySpec, isCapabilityName, type CapabilityArgs, type CapabilityName, type CapabilityResults, type CapabilitySpec, type Gate, type HandledCapability, } from "./config/capabilities.ts";
|
|
6
|
-
export type { CapabilityConfig, CapabilityMap, ColorScheme, DisplayMode, EgressConfig, EmbedderConfig, GuideConfig, IsolationKind, LayoutConfig, Limits, PortholeConfig, ThemeConfig, ThemeValue, } from "./config/schema.ts";
|
|
6
|
+
export type { CapabilityConfig, CapabilityMap, ColorScheme, DisplayMode, EgressConfig, EmbedderConfig, GuideConfig, IsolationKind, LayoutConfig, Limits, PortholeConfig, StorageConfig, ThemeConfig, ThemeValue, } from "./config/schema.ts";
|
|
7
7
|
export { BUILT_IN_TOPICS, ConfigError, isOriginSource, originSourceProblem, validateConfig, } from "./config/validate.ts";
|
|
8
8
|
export { URL_PERMISSIONS, URL_SANDBOX_EXTRAS, type UrlPermission, type UrlSandboxExtra, } from "./config/schema.ts";
|
|
9
9
|
export { hostOriginProblem, isHostOrigin } from "./isolation/served.ts";
|
|
10
10
|
export { FRAME_STATUS_METHOD } from "./isolation/types.ts";
|
|
11
11
|
export { LIBRARY_CDNS, presets, type PresetName, type PresetOptions, type UrlPresetOptions, } from "./presets/presets.ts";
|
|
12
12
|
export { compile, type CompiledConfig } from "./compile/compile.ts";
|
|
13
|
-
export { allEgressOrigins, declaredCapabilities, effectiveGates, egressOrigins, embedderCsp, type CspDirective, type EgressKind, } from "./compile/policy.ts";
|
|
13
|
+
export { allEgressOrigins, declaredCapabilities, effectiveGates, egressOrigins, embedderCsp, embedderFrameSourcesOf, type CspDirective, type EgressKind, } from "./compile/policy.ts";
|
|
14
14
|
export { shellPlan, type ShellPlan } from "./shell/plan.ts";
|
|
15
15
|
export type { ShellDocumentOptions } from "./shell/document.ts";
|
|
16
16
|
export { generateGuide, generateToolDescription, type BuiltInTopic, type GuideTopic, } from "./guide/guide.ts";
|
package/dist/index.js
CHANGED
|
@@ -12,7 +12,7 @@ export { FRAME_STATUS_METHOD } from "./isolation/types.js";
|
|
|
12
12
|
export { LIBRARY_CDNS, presets, } from "./presets/presets.js";
|
|
13
13
|
// The compiler.
|
|
14
14
|
export { compile } from "./compile/compile.js";
|
|
15
|
-
export { allEgressOrigins, declaredCapabilities, effectiveGates, egressOrigins, embedderCsp, } from "./compile/policy.js";
|
|
15
|
+
export { allEgressOrigins, declaredCapabilities, effectiveGates, egressOrigins, embedderCsp, embedderFrameSourcesOf, } from "./compile/policy.js";
|
|
16
16
|
export { shellPlan } from "./shell/plan.js";
|
|
17
17
|
export { generateGuide, generateToolDescription, } from "./guide/guide.js";
|
|
18
18
|
export { GUIDE_EXAMPLES } from "./guide/examples.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { CapabilityName } from "../config/capabilities.ts";
|
|
2
|
-
import type { CapabilityConfig, EgressConfig, EmbedderConfig, GuideConfig, IsolationKind, LayoutConfig, Limits, PortholeConfig, ThemeValue, UrlPermission, UrlSandboxExtra } from "../config/schema.ts";
|
|
2
|
+
import type { CapabilityConfig, EgressConfig, EmbedderConfig, GuideConfig, IsolationKind, LayoutConfig, Limits, PortholeConfig, StorageConfig, ThemeValue, UrlPermission, UrlSandboxExtra } from "../config/schema.ts";
|
|
3
3
|
/**
|
|
4
4
|
* The preset policy layer (P001): named configs with safe defaults. Every preset starts with
|
|
5
5
|
* egress closed and only the capabilities that carry nothing out of the frame (its size, its
|
|
@@ -30,6 +30,8 @@ export interface PresetOptions {
|
|
|
30
30
|
readonly isolation?: Exclude<IsolationKind, "external">;
|
|
31
31
|
readonly contentOrigins?: readonly string[];
|
|
32
32
|
readonly embedder?: EmbedderConfig;
|
|
33
|
+
/** Domain isolation: what displays sharing an origin share (`StorageConfig`), for the manual. */
|
|
34
|
+
readonly storage?: StorageConfig;
|
|
33
35
|
}
|
|
34
36
|
/** The url preset's options: where the app is served, and the embedding page's extra frames. */
|
|
35
37
|
export interface UrlPresetOptions {
|
package/dist/presets/presets.js
CHANGED
|
@@ -102,6 +102,7 @@ function build(mode, defaults, options) {
|
|
|
102
102
|
guide: options.guide ?? {},
|
|
103
103
|
contentOrigins: options.contentOrigins ?? [],
|
|
104
104
|
embedder: options.embedder ?? {},
|
|
105
|
+
...(options.storage === undefined ? {} : { storage: options.storage }),
|
|
105
106
|
});
|
|
106
107
|
}
|
|
107
108
|
export const presets = {
|
package/dist/react/frame.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { AnnotateMode, AnnotationMark, AnnotationReport } from "../annotate
|
|
|
3
3
|
import type { CapabilityHandlers, Confirm, Refusal } from "../bridge/types.ts";
|
|
4
4
|
import type { CapabilityName } from "../config/capabilities.ts";
|
|
5
5
|
import type { ColorScheme, PortholeConfig } from "../config/schema.ts";
|
|
6
|
-
import type { FrameOutcome } from "../host/types.ts";
|
|
6
|
+
import type { FrameOutcome, HostErrorKind } from "../host/types.ts";
|
|
7
7
|
import type { InstanceLocator, InstanceRef } from "../isolation/types.ts";
|
|
8
8
|
/** The height a display holds before it has reported its own. */
|
|
9
9
|
export declare const PLACEHOLDER_HEIGHT_PX = 96;
|
|
@@ -24,6 +24,11 @@ export interface PortholeProviderProps {
|
|
|
24
24
|
readonly dialogClassName?: string;
|
|
25
25
|
/** A class for the default copy toast, to restyle it. */
|
|
26
26
|
readonly toastClassName?: string;
|
|
27
|
+
/**
|
|
28
|
+
* The page's CSP nonce, where its `script-src` is nonce-based: opaque frames inherit the page's
|
|
29
|
+
* policy, so their shell carries it (P054).
|
|
30
|
+
*/
|
|
31
|
+
readonly nonce?: string;
|
|
27
32
|
readonly children?: ReactNode;
|
|
28
33
|
}
|
|
29
34
|
export declare function PortholeProvider(props: PortholeProviderProps): ReactElement;
|
|
@@ -60,12 +65,31 @@ export interface PortholeFrameProps {
|
|
|
60
65
|
readonly generation?: number;
|
|
61
66
|
/** Run at once, without waiting to near the screen (it still counts toward the page's most). */
|
|
62
67
|
readonly eager?: boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Fill the box (give it, or its parent, a height) and let the content scroll inside, instead of
|
|
70
|
+
* growing with the content: an artifact viewer's page. A url-mode app always fills.
|
|
71
|
+
*/
|
|
72
|
+
readonly fill?: boolean;
|
|
63
73
|
readonly onInitialized?: () => void;
|
|
64
74
|
readonly onResize?: (size: {
|
|
65
75
|
readonly height: number;
|
|
66
76
|
readonly width?: number;
|
|
67
77
|
}) => void;
|
|
68
|
-
|
|
78
|
+
/**
|
|
79
|
+
* What the content did wrong (what it threw, what the sandbox blocked, that it kept navigating
|
|
80
|
+
* away): the agent's to hear and fix.
|
|
81
|
+
*/
|
|
82
|
+
readonly onContentError?: (message: string) => void;
|
|
83
|
+
/**
|
|
84
|
+
* What failed on the host's side: its locator (`locate`), a display that couldn't be made
|
|
85
|
+
* (`mount`), an annotation call refused (`annotations`). Never the agent's.
|
|
86
|
+
*/
|
|
87
|
+
readonly onHostError?: (error: {
|
|
88
|
+
readonly message: string;
|
|
89
|
+
readonly kind: HostErrorKind;
|
|
90
|
+
}) => void;
|
|
91
|
+
/** Both, with their source (as before 0.1.4). Prefer `onContentError` and `onHostError`. */
|
|
92
|
+
readonly onError?: (message: string, source: "content" | "host") => void;
|
|
69
93
|
/** The content navigated its frame away; it was started again (or stopped, after a few). */
|
|
70
94
|
readonly onNavigated?: () => void;
|
|
71
95
|
readonly onRefused?: (capability: CapabilityName, refusal: Refusal) => void;
|
|
@@ -94,8 +118,14 @@ export interface PortholeFrameProps {
|
|
|
94
118
|
readonly annotating?: AnnotateMode | null;
|
|
95
119
|
/** The anchors to mark in the frame (the host's comments). Keep it stable (memoized). */
|
|
96
120
|
readonly annotations?: readonly AnnotationMark[];
|
|
97
|
-
/**
|
|
98
|
-
|
|
121
|
+
/**
|
|
122
|
+
* The mark to bring into view and make the active one; each new id focuses it. To focus the
|
|
123
|
+
* same mark again (a second click on its comment), give `{ id, key }` with a new key.
|
|
124
|
+
*/
|
|
125
|
+
readonly focusedAnnotation?: string | {
|
|
126
|
+
readonly id: string;
|
|
127
|
+
readonly key: string | number;
|
|
128
|
+
} | null;
|
|
99
129
|
/**
|
|
100
130
|
* What the frame reported: a selection (an anchor and where it is), its clearing, a cancelled
|
|
101
131
|
* pick, where marks are. The content's word: UI hints, never permissions.
|