@botiverse/porthole 0.0.0-stage → 0.1.0

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 (150) hide show
  1. package/LICENSE +217 -0
  2. package/dist/bridge/bridge.d.ts +39 -0
  3. package/dist/bridge/bridge.js +184 -0
  4. package/dist/bridge/types.d.ts +67 -0
  5. package/dist/bridge/types.js +1 -0
  6. package/dist/compile/compile.d.ts +35 -0
  7. package/dist/compile/compile.js +30 -0
  8. package/dist/compile/policy.d.ts +55 -0
  9. package/dist/compile/policy.js +106 -0
  10. package/dist/config/capabilities.d.ts +154 -0
  11. package/dist/config/capabilities.js +63 -0
  12. package/dist/config/schema.d.ts +131 -0
  13. package/dist/config/schema.js +1 -0
  14. package/dist/config/validate.d.ts +20 -0
  15. package/dist/config/validate.js +230 -0
  16. package/dist/conformance/behavior.d.ts +2 -0
  17. package/dist/conformance/behavior.js +376 -0
  18. package/dist/conformance/index.d.ts +95 -0
  19. package/dist/conformance/index.js +4 -0
  20. package/dist/conformance/kit.d.ts +42 -0
  21. package/dist/conformance/kit.js +24 -0
  22. package/dist/conformance/oracles.d.ts +35 -0
  23. package/dist/conformance/oracles.js +77 -0
  24. package/dist/conformance/probes.d.ts +9 -0
  25. package/dist/conformance/probes.js +865 -0
  26. package/dist/conformance/served.d.ts +68 -0
  27. package/dist/conformance/served.js +311 -0
  28. package/dist/guide/examples.d.ts +70 -0
  29. package/dist/guide/examples.js +69 -0
  30. package/dist/guide/guide.d.ts +16 -0
  31. package/dist/guide/guide.js +261 -0
  32. package/dist/host/create-host.d.ts +8 -0
  33. package/dist/host/create-host.js +248 -0
  34. package/dist/host/locate.d.ts +8 -0
  35. package/dist/host/locate.js +11 -0
  36. package/dist/host/types.d.ts +95 -0
  37. package/dist/host/types.js +1 -0
  38. package/dist/index.d.ts +23 -0
  39. package/dist/index.js +19 -0
  40. package/dist/isolation/served.d.ts +32 -0
  41. package/dist/isolation/served.js +97 -0
  42. package/dist/isolation/types.d.ts +87 -0
  43. package/dist/isolation/types.js +1 -0
  44. package/dist/mcp/adapter.d.ts +43 -0
  45. package/dist/mcp/adapter.js +327 -0
  46. package/dist/mcp/index.d.ts +27 -0
  47. package/dist/mcp/index.js +25 -0
  48. package/dist/mcp/proxy.d.ts +25 -0
  49. package/dist/mcp/proxy.js +106 -0
  50. package/dist/mcp/view.d.ts +31 -0
  51. package/dist/mcp/view.js +72 -0
  52. package/dist/platform/types.d.ts +44 -0
  53. package/dist/platform/types.js +1 -0
  54. package/dist/platform/web.d.ts +8 -0
  55. package/dist/platform/web.js +72 -0
  56. package/dist/presets/presets.d.ts +65 -0
  57. package/dist/presets/presets.js +125 -0
  58. package/dist/protocol/adapter.d.ts +73 -0
  59. package/dist/protocol/adapter.js +1 -0
  60. package/dist/protocol/jsonrpc.d.ts +43 -0
  61. package/dist/protocol/jsonrpc.js +50 -0
  62. package/dist/protocol/mcp-apps.d.ts +79 -0
  63. package/dist/protocol/mcp-apps.js +99 -0
  64. package/dist/protocol/shell-adapter.d.ts +5 -0
  65. package/dist/protocol/shell-adapter.js +228 -0
  66. package/dist/react/dialog.d.ts +20 -0
  67. package/dist/react/dialog.js +98 -0
  68. package/dist/react/frame.d.ts +75 -0
  69. package/dist/react/frame.js +221 -0
  70. package/dist/react/index.d.ts +13 -0
  71. package/dist/react/index.js +13 -0
  72. package/dist/react/live.d.ts +24 -0
  73. package/dist/react/live.js +57 -0
  74. package/dist/server/index.d.ts +27 -0
  75. package/dist/server/index.js +26 -0
  76. package/dist/server/keys.d.ts +19 -0
  77. package/dist/server/keys.js +78 -0
  78. package/dist/server/pages.d.ts +6 -0
  79. package/dist/server/pages.js +20 -0
  80. package/dist/server/proxy.d.ts +21 -0
  81. package/dist/server/proxy.js +107 -0
  82. package/dist/server/server.d.ts +7 -0
  83. package/dist/server/server.js +518 -0
  84. package/dist/server/tickets.d.ts +8 -0
  85. package/dist/server/tickets.js +28 -0
  86. package/dist/server/types.d.ts +97 -0
  87. package/dist/server/types.js +1 -0
  88. package/dist/shell/document.d.ts +28 -0
  89. package/dist/shell/document.js +84 -0
  90. package/dist/shell/plan.d.ts +35 -0
  91. package/dist/shell/plan.js +34 -0
  92. package/dist/shell/runtime.d.ts +28 -0
  93. package/dist/shell/runtime.js +832 -0
  94. package/dist/theme/variables.d.ts +73 -0
  95. package/dist/theme/variables.js +171 -0
  96. package/dist/transport/post-message.d.ts +9 -0
  97. package/dist/transport/post-message.js +38 -0
  98. package/dist/transport/types.d.ts +15 -0
  99. package/dist/transport/types.js +1 -0
  100. package/package.json +68 -3
  101. package/src/bridge/bridge.ts +256 -0
  102. package/src/bridge/types.ts +77 -0
  103. package/src/compile/compile.ts +68 -0
  104. package/src/compile/policy.ts +133 -0
  105. package/src/config/capabilities.ts +138 -0
  106. package/src/config/schema.ts +140 -0
  107. package/src/config/validate.ts +284 -0
  108. package/src/conformance/behavior.ts +430 -0
  109. package/src/conformance/index.ts +102 -0
  110. package/src/conformance/kit.ts +63 -0
  111. package/src/conformance/oracles.ts +126 -0
  112. package/src/conformance/probes.ts +1093 -0
  113. package/src/conformance/served.ts +472 -0
  114. package/src/guide/examples.ts +74 -0
  115. package/src/guide/guide.ts +424 -0
  116. package/src/host/create-host.ts +290 -0
  117. package/src/host/locate.ts +20 -0
  118. package/src/host/types.ts +95 -0
  119. package/src/index.ts +118 -0
  120. package/src/isolation/served.ts +127 -0
  121. package/src/isolation/types.ts +77 -0
  122. package/src/mcp/adapter.ts +441 -0
  123. package/src/mcp/index.ts +50 -0
  124. package/src/mcp/proxy.ts +117 -0
  125. package/src/mcp/view.ts +97 -0
  126. package/src/platform/types.ts +48 -0
  127. package/src/platform/web.ts +78 -0
  128. package/src/presets/presets.ts +193 -0
  129. package/src/protocol/adapter.ts +67 -0
  130. package/src/protocol/jsonrpc.ts +83 -0
  131. package/src/protocol/mcp-apps.ts +138 -0
  132. package/src/protocol/shell-adapter.ts +261 -0
  133. package/src/react/dialog.tsx +158 -0
  134. package/src/react/frame.tsx +422 -0
  135. package/src/react/index.ts +20 -0
  136. package/src/react/live.ts +76 -0
  137. package/src/server/index.ts +37 -0
  138. package/src/server/keys.ts +100 -0
  139. package/src/server/pages.ts +31 -0
  140. package/src/server/proxy.ts +131 -0
  141. package/src/server/server.ts +617 -0
  142. package/src/server/tickets.ts +30 -0
  143. package/src/server/types.ts +104 -0
  144. package/src/shell/document.ts +120 -0
  145. package/src/shell/plan.ts +66 -0
  146. package/src/shell/runtime.ts +832 -0
  147. package/src/theme/variables.ts +190 -0
  148. package/src/transport/post-message.ts +45 -0
  149. package/src/transport/types.ts +16 -0
  150. package/README.md +0 -3
@@ -0,0 +1,95 @@
1
+ /**
2
+ * `@botiverse/porthole/conformance`: the leak/conformance suite (P008), runnable by any host
3
+ * against its own integration. A host implements `ConformanceHost` (how to show one display in a
4
+ * page and drive it), then registers the suite in its Playwright project:
5
+ *
6
+ * import { test } from "@playwright/test";
7
+ * import { defineLeakSuite } from "@botiverse/porthole/conformance";
8
+ * defineLeakSuite(test, myHost);
9
+ *
10
+ * Each probe attempts one egress or escape channel (DESIGN.md § Egress hardening) and asserts,
11
+ * with an out-of-band oracle where one exists (an HTTP canary that also counts connections, a UDP
12
+ * listener for ICE), that nothing left. Each oracle has a positive control: where the engine
13
+ * can't make the channel work even unsandboxed, the probe says so instead of passing silently.
14
+ * Porthole runs it against its own reference host on every engine it supports.
15
+ */
16
+ import type { Frame, Page } from "@playwright/test";
17
+ import type { ConfirmRequest } from "../bridge/types.ts";
18
+ import type { CapabilityName } from "../config/capabilities.ts";
19
+ import type { ColorScheme, PortholeConfig } from "../config/schema.ts";
20
+ import type { DisplayEvents } from "../host/types.ts";
21
+ export interface LaunchSpec {
22
+ readonly config: PortholeConfig;
23
+ /**
24
+ * The whole content (a fragment, or a document for artifact mode). Omitted: nothing is sent,
25
+ * and the test drives the content with `LaunchedDisplay.setContent`.
26
+ */
27
+ readonly content?: string;
28
+ /** Content to stream first, each step sent as partial, before the whole. */
29
+ readonly partials?: readonly string[];
30
+ /** A whole document the frame starts with instead (an MCP Apps view). */
31
+ readonly document?: string;
32
+ readonly title?: string;
33
+ readonly scheme?: ColorScheme;
34
+ /** The frame's width, px. */
35
+ readonly widthPx?: number;
36
+ /** The host's live values for theme variables, per scheme (as `HostOptions.theme` answers). */
37
+ readonly hostTheme?: {
38
+ readonly light?: Readonly<Record<string, string>>;
39
+ readonly dark?: Readonly<Record<string, string>>;
40
+ };
41
+ /** Whether the host has a `confirm` function (default true): without one, gated calls are refused. */
42
+ readonly confirms?: boolean;
43
+ }
44
+ export interface RecordedCall {
45
+ readonly capability: CapabilityName;
46
+ readonly args: unknown;
47
+ }
48
+ export type RecordedEvent = {
49
+ [E in keyof DisplayEvents]: {
50
+ readonly type: E;
51
+ readonly detail: DisplayEvents[E];
52
+ };
53
+ }[keyof DisplayEvents];
54
+ export interface LaunchedDisplay {
55
+ /** The display's frame as it is now (a restart makes a new one), to probe from inside. */
56
+ frame(): Promise<Frame>;
57
+ /** The frame element's size on the host's page, px. */
58
+ box(): Promise<{
59
+ readonly width: number;
60
+ readonly height: number;
61
+ }>;
62
+ /** Sends content: partial (drawn inert) any number of times, then whole (runs once). */
63
+ setContent(html: string, options?: {
64
+ readonly partial?: boolean;
65
+ }): Promise<void>;
66
+ setTheme(scheme: ColorScheme): Promise<void>;
67
+ /** Waits for the next confirmation the host is asked for, answers it, and returns it. */
68
+ answerNextConfirmation(accept: boolean, options?: {
69
+ readonly timeoutMs?: number;
70
+ }): Promise<ConfirmRequest>;
71
+ /** The calls that reached the host's handlers, in order. */
72
+ calls(): Promise<readonly RecordedCall[]>;
73
+ /** The errors the display reported. */
74
+ errors(): Promise<readonly string[]>;
75
+ /** Every event the display emitted, in order. */
76
+ events(): Promise<readonly RecordedEvent[]>;
77
+ destroy(): Promise<void>;
78
+ }
79
+ /** What a host implements to run the suite against its integration. */
80
+ export interface ConformanceHost {
81
+ readonly name: string;
82
+ /**
83
+ * Shows one display in `page` and returns handles to drive it. The first call on a page loads
84
+ * the host's page (sending its embedding CSP for `spec.config`); a later call on the same page
85
+ * adds another display beside the first.
86
+ */
87
+ launch(page: Page, spec: LaunchSpec): Promise<LaunchedDisplay>;
88
+ }
89
+ export interface LeakSuiteOptions {
90
+ /** Probe ids to skip, each with the reason (shown in the report). */
91
+ readonly skip?: Readonly<Record<string, string>>;
92
+ }
93
+ export { defineLeakSuite, PROBES, type Probe } from "./probes.ts";
94
+ export { httpOracle, settle, udpOracle, type HttpOracle, type UdpOracle } from "./oracles.ts";
95
+ export { defineServedSuite, type ServedConformanceHost, type ServedDisplay, type ServedSuiteOptions, } from "./served.ts";
@@ -0,0 +1,4 @@
1
+ export { defineLeakSuite, PROBES } from "./probes.js";
2
+ export { httpOracle, settle, udpOracle } from "./oracles.js";
3
+ // The served modes' cases (domain and url mode), with their own host interface.
4
+ export { defineServedSuite, } from "./served.js";
@@ -0,0 +1,42 @@
1
+ import type { Page } from "@playwright/test";
2
+ import { type PresetOptions } from "../presets/presets.ts";
3
+ import type { LaunchedDisplay, LaunchSpec } from "./index.ts";
4
+ import type { HttpOracle, ServedFile, UdpOracle } from "./oracles.ts";
5
+ /** What every probe is given, and the helpers they share. */
6
+ export interface ProbeContext {
7
+ readonly page: Page;
8
+ readonly engine: string;
9
+ launch(spec: LaunchSpec): Promise<LaunchedDisplay>;
10
+ /** A fresh HTTP oracle (serving `files` at their paths), closed after the probe. */
11
+ http(files?: Readonly<Record<string, ServedFile>>): Promise<HttpOracle>;
12
+ /** A fresh UDP oracle, closed after the probe. */
13
+ udp(): Promise<UdpOracle>;
14
+ /** Waits for the content's verdict (`window.portholeProbe`, set by `attack`) and returns it. */
15
+ result(display: LaunchedDisplay, timeoutMs?: number): Promise<Record<string, unknown>>;
16
+ /**
17
+ * The URLs on `origin` that answered the page (any of its frames): requests that left the
18
+ * browser and came back. A load the browser blocked itself never has a response.
19
+ */
20
+ requestsTo(origin: string): string[];
21
+ /** Records a measured engine fact in the report. */
22
+ measured(text: string): void;
23
+ }
24
+ export interface Probe {
25
+ /** Stable id, `<item>.<channel>`: what `LeakSuiteOptions.skip` names. */
26
+ readonly id: string;
27
+ /**
28
+ * The DESIGN.md egress-table item it proves; 0 for the shell's own wiring and interop, 11 for
29
+ * behavior (streaming, sizing, theming), 12 for host-function gates.
30
+ */
31
+ readonly item: number;
32
+ readonly title: string;
33
+ run(context: ProbeContext): Promise<void>;
34
+ }
35
+ /** A JS literal safe inside an inline `<script>` (no `</script>` can end it early). */
36
+ export declare const js: (value: unknown) => string;
37
+ /**
38
+ * Content that runs `code` (an async body with `r`, its verdict, in scope) once the shell has
39
+ * drawn it, then publishes `r` as `window.portholeProbe`. A `#marker` shows the content is still there.
40
+ */
41
+ export declare function attack(code: string, markup?: string): string;
42
+ export declare const inline: (options?: PresetOptions) => import("../index.ts").PortholeConfig;
@@ -0,0 +1,24 @@
1
+ import { presets } from "../presets/presets.js";
2
+ /** A JS literal safe inside an inline `<script>` (no `</script>` can end it early). */
3
+ export const js = (value) => JSON.stringify(value).replace(/</gu, "\\u003c");
4
+ /**
5
+ * Content that runs `code` (an async body with `r`, its verdict, in scope) once the shell has
6
+ * drawn it, then publishes `r` as `window.portholeProbe`. A `#marker` shows the content is still there.
7
+ */
8
+ export function attack(code, markup = "") {
9
+ return `<div id="marker">content</div>${markup}
10
+ <script>
11
+ (async () => {
12
+ const r = {};
13
+ const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
14
+ try {
15
+ ${code}
16
+ } catch (error) {
17
+ r.error = String((error && error.message) || error);
18
+ }
19
+ r.done = true;
20
+ window.portholeProbe = r;
21
+ })();
22
+ </script>`;
23
+ }
24
+ export const inline = (options = {}) => presets.inlineWidget(options);
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Out-of-band oracles: listeners outside the browser that see what actually left it. A probe
3
+ * points content at one and asserts it heard nothing; a positive control points something allowed
4
+ * at one and asserts it heard it, so a silent oracle is never mistaken for a wall.
5
+ */
6
+ export interface HeardRequest {
7
+ readonly method: string;
8
+ readonly path: string;
9
+ readonly origin: string | undefined;
10
+ }
11
+ export interface HttpOracle {
12
+ /** `http://127.0.0.1:<port>`: an exact origin a config can allow or leave out. */
13
+ readonly origin: string;
14
+ /** Every request it answered. */
15
+ requests(): readonly HeardRequest[];
16
+ /** Every TCP connection opened to it, whether or not a request followed (preconnects). */
17
+ connections(): number;
18
+ close(): Promise<void>;
19
+ }
20
+ /** A file an oracle serves at a path, instead of its stand-in answer (a library, say). */
21
+ export interface ServedFile {
22
+ readonly type: string;
23
+ readonly data: string | Buffer;
24
+ }
25
+ export declare function httpOracle(files?: Readonly<Record<string, ServedFile>>): Promise<HttpOracle>;
26
+ export interface UdpOracle {
27
+ /** `stun:127.0.0.1:<port>`, for an ICE server list. */
28
+ readonly stun: string;
29
+ /** The datagrams that reached it. */
30
+ packets(): number;
31
+ close(): Promise<void>;
32
+ }
33
+ export declare function udpOracle(): Promise<UdpOracle>;
34
+ /** Waits `ms`, so a request that was going to leave has time to arrive. */
35
+ export declare const settle: (ms?: number) => Promise<void>;
@@ -0,0 +1,77 @@
1
+ import { createSocket } from "node:dgram";
2
+ import { createServer } from "node:http";
3
+ const PIXEL = Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=", "base64");
4
+ /** What the oracle answers, by the path's extension: real enough for each kind of load to finish. */
5
+ function body(path) {
6
+ if (/\.m?js(\?|$)/u.test(path)) {
7
+ return {
8
+ type: "text/javascript",
9
+ data: "globalThis.__portholeOracleScript = (globalThis.__portholeOracleScript || 0) + 1;",
10
+ };
11
+ }
12
+ if (/\.css(\?|$)/u.test(path)) {
13
+ return { type: "text/css", data: "html { --porthole-oracle: 1; }" };
14
+ }
15
+ if (/\.(png|gif|jpg)(\?|$)/u.test(path)) {
16
+ return { type: "image/png", data: PIXEL };
17
+ }
18
+ if (/\.html(\?|$)/u.test(path)) {
19
+ return {
20
+ type: "text/html",
21
+ data: "<!doctype html><title>oracle</title><p id=oracle>oracle</p>",
22
+ };
23
+ }
24
+ return { type: "text/plain", data: "oracle" };
25
+ }
26
+ export async function httpOracle(files = {}) {
27
+ const heard = [];
28
+ let connections = 0;
29
+ const server = createServer((request, response) => {
30
+ heard.push({
31
+ method: request.method ?? "GET",
32
+ path: request.url ?? "/",
33
+ origin: request.headers.origin,
34
+ });
35
+ const { type, data } = files[request.url ?? "/"] ?? body(request.url ?? "/");
36
+ response.writeHead(200, {
37
+ "content-type": type,
38
+ "access-control-allow-origin": "*",
39
+ "cache-control": "no-store",
40
+ });
41
+ response.end(data);
42
+ });
43
+ server.on("connection", () => {
44
+ connections += 1;
45
+ });
46
+ server.on("upgrade", (request, socket) => {
47
+ heard.push({ method: "UPGRADE", path: request.url ?? "/", origin: request.headers.origin });
48
+ socket.destroy();
49
+ });
50
+ await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
51
+ const { port } = server.address();
52
+ return {
53
+ origin: `http://127.0.0.1:${port}`,
54
+ requests: () => [...heard],
55
+ connections: () => connections,
56
+ close: () => new Promise((resolve) => {
57
+ server.closeAllConnections();
58
+ server.close(() => resolve());
59
+ }),
60
+ };
61
+ }
62
+ export async function udpOracle() {
63
+ let packets = 0;
64
+ const socket = createSocket("udp4");
65
+ socket.on("message", () => {
66
+ packets += 1;
67
+ });
68
+ await new Promise((resolve) => socket.bind(0, "127.0.0.1", resolve));
69
+ const { port } = socket.address();
70
+ return {
71
+ stun: `stun:127.0.0.1:${port}`,
72
+ packets: () => packets,
73
+ close: () => new Promise((resolve) => socket.close(() => resolve())),
74
+ };
75
+ }
76
+ /** Waits `ms`, so a request that was going to leave has time to arrive. */
77
+ export const settle = (ms = 800) => new Promise((resolve) => setTimeout(resolve, ms));
@@ -0,0 +1,9 @@
1
+ import { type PlaywrightTestArgs, type PlaywrightTestOptions, type PlaywrightWorkerArgs, type PlaywrightWorkerOptions, type TestType } from "@playwright/test";
2
+ import type { ConformanceHost, LeakSuiteOptions } from "./index.ts";
3
+ import { type Probe } from "./kit.ts";
4
+ export type { Probe, ProbeContext } from "./kit.ts";
5
+ export { attack } from "./kit.ts";
6
+ export declare const PROBES: readonly Probe[];
7
+ type AnyTest = TestType<PlaywrightTestArgs & PlaywrightTestOptions, PlaywrightWorkerArgs & PlaywrightWorkerOptions>;
8
+ /** Registers every probe as a test, grouped by egress item. */
9
+ export declare function defineLeakSuite(test: AnyTest, host: ConformanceHost, options?: LeakSuiteOptions): void;