@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.
Files changed (42) hide show
  1. package/dist/compile/policy.d.ts +6 -0
  2. package/dist/compile/policy.js +9 -1
  3. package/dist/config/schema.d.ts +11 -0
  4. package/dist/config/validate.js +12 -0
  5. package/dist/conformance/served.d.ts +8 -1
  6. package/dist/conformance/served.js +12 -8
  7. package/dist/guide/guide.js +3 -1
  8. package/dist/host/create-host.js +24 -7
  9. package/dist/host/types.d.ts +44 -2
  10. package/dist/index.d.ts +2 -2
  11. package/dist/index.js +1 -1
  12. package/dist/presets/presets.d.ts +3 -1
  13. package/dist/presets/presets.js +1 -0
  14. package/dist/react/frame.d.ts +34 -4
  15. package/dist/react/frame.js +80 -18
  16. package/dist/server/naming.d.ts +6 -0
  17. package/dist/server/naming.js +38 -0
  18. package/dist/server/pages.js +2 -0
  19. package/dist/server/server.js +49 -17
  20. package/dist/server/types.d.ts +6 -3
  21. package/dist/shell/annotate.js +31 -0
  22. package/dist/shell/document.d.ts +6 -0
  23. package/dist/shell/document.js +8 -2
  24. package/dist/shell/runtime.js +33 -1
  25. package/package.json +1 -1
  26. package/src/compile/policy.ts +10 -1
  27. package/src/config/schema.ts +12 -0
  28. package/src/config/validate.ts +15 -0
  29. package/src/conformance/served.ts +24 -9
  30. package/src/guide/guide.ts +3 -1
  31. package/src/host/create-host.ts +26 -7
  32. package/src/host/types.ts +39 -2
  33. package/src/index.ts +2 -0
  34. package/src/presets/presets.ts +4 -0
  35. package/src/react/frame.tsx +125 -23
  36. package/src/server/naming.ts +41 -0
  37. package/src/server/pages.ts +2 -0
  38. package/src/server/server.ts +61 -20
  39. package/src/server/types.ts +6 -3
  40. package/src/shell/annotate.ts +31 -0
  41. package/src/shell/document.ts +15 -2
  42. package/src/shell/runtime.ts +33 -1
@@ -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[];
@@ -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 = unique(configs.flatMap(embedderFrameSources));
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
+ }
@@ -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
  }
@@ -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): Promise<ServedDisplay>;
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 host.show(page, instance(`${mode}-origin`));
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 host.show(page, instance(`${mode}-reach`));
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 host.show(page, instance(`${mode}-top`));
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 host.show(page, instance(`${mode}-popup`));
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 {
@@ -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
- : `- localStorage is kept for this ${noun} in this browser; another device starts fresh.`;
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;
@@ -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
- emit("error", { message: event.message });
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
- ...(speaks
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
- emit("error", {
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
- emit("error", { message: error instanceof Error ? error.message : String(error) });
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
+ }
@@ -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" | "not-found" | "error";
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
- /** What the content threw, or what the sandbox blocked (its own words: untrusted). */
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 {
@@ -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 = {
@@ -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
- readonly onError?: (message: string) => void;
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
- /** The mark to bring into view and make the active one; each new id focuses it. */
98
- readonly focusedAnnotation?: string | null;
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.