@botiverse/porthole 0.0.1 → 0.1.1

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 (152) hide show
  1. package/LICENSE +217 -0
  2. package/dist/bridge/bridge.d.ts +39 -0
  3. package/dist/bridge/bridge.js +198 -0
  4. package/dist/bridge/types.d.ts +83 -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 +103 -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 +152 -0
  13. package/dist/config/schema.js +11 -0
  14. package/dist/config/validate.d.ts +22 -0
  15. package/dist/config/validate.js +261 -0
  16. package/dist/conformance/behavior.d.ts +2 -0
  17. package/dist/conformance/behavior.js +426 -0
  18. package/dist/conformance/index.d.ts +101 -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 +33 -0
  25. package/dist/conformance/probes.js +876 -0
  26. package/dist/conformance/served.d.ts +75 -0
  27. package/dist/conformance/served.js +335 -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 +265 -0
  32. package/dist/host/create-host.d.ts +8 -0
  33. package/dist/host/create-host.js +292 -0
  34. package/dist/host/locate.d.ts +8 -0
  35. package/dist/host/locate.js +16 -0
  36. package/dist/host/outcome.d.ts +24 -0
  37. package/dist/host/outcome.js +134 -0
  38. package/dist/host/types.d.ts +127 -0
  39. package/dist/host/types.js +1 -0
  40. package/dist/index.d.ts +25 -0
  41. package/dist/index.js +21 -0
  42. package/dist/isolation/served.d.ts +35 -0
  43. package/dist/isolation/served.js +117 -0
  44. package/dist/isolation/types.d.ts +96 -0
  45. package/dist/isolation/types.js +2 -0
  46. package/dist/mcp/adapter.d.ts +43 -0
  47. package/dist/mcp/adapter.js +327 -0
  48. package/dist/mcp/index.d.ts +27 -0
  49. package/dist/mcp/index.js +25 -0
  50. package/dist/mcp/proxy.d.ts +25 -0
  51. package/dist/mcp/proxy.js +106 -0
  52. package/dist/mcp/view.d.ts +31 -0
  53. package/dist/mcp/view.js +72 -0
  54. package/dist/platform/types.d.ts +51 -0
  55. package/dist/platform/types.js +1 -0
  56. package/dist/platform/web.d.ts +8 -0
  57. package/dist/platform/web.js +85 -0
  58. package/dist/presets/presets.d.ts +71 -0
  59. package/dist/presets/presets.js +140 -0
  60. package/dist/protocol/adapter.d.ts +73 -0
  61. package/dist/protocol/adapter.js +1 -0
  62. package/dist/protocol/jsonrpc.d.ts +43 -0
  63. package/dist/protocol/jsonrpc.js +50 -0
  64. package/dist/protocol/mcp-apps.d.ts +79 -0
  65. package/dist/protocol/mcp-apps.js +99 -0
  66. package/dist/protocol/shell-adapter.d.ts +5 -0
  67. package/dist/protocol/shell-adapter.js +228 -0
  68. package/dist/react/dialog.d.ts +20 -0
  69. package/dist/react/dialog.js +98 -0
  70. package/dist/react/frame.d.ts +91 -0
  71. package/dist/react/frame.js +255 -0
  72. package/dist/react/index.d.ts +13 -0
  73. package/dist/react/index.js +13 -0
  74. package/dist/react/live.d.ts +24 -0
  75. package/dist/react/live.js +57 -0
  76. package/dist/server/index.d.ts +27 -0
  77. package/dist/server/index.js +26 -0
  78. package/dist/server/keys.d.ts +19 -0
  79. package/dist/server/keys.js +78 -0
  80. package/dist/server/pages.d.ts +11 -0
  81. package/dist/server/pages.js +86 -0
  82. package/dist/server/proxy.d.ts +21 -0
  83. package/dist/server/proxy.js +107 -0
  84. package/dist/server/server.d.ts +7 -0
  85. package/dist/server/server.js +542 -0
  86. package/dist/server/tickets.d.ts +8 -0
  87. package/dist/server/tickets.js +28 -0
  88. package/dist/server/types.d.ts +97 -0
  89. package/dist/server/types.js +1 -0
  90. package/dist/shell/document.d.ts +29 -0
  91. package/dist/shell/document.js +85 -0
  92. package/dist/shell/plan.d.ts +37 -0
  93. package/dist/shell/plan.js +35 -0
  94. package/dist/shell/runtime.d.ts +28 -0
  95. package/dist/shell/runtime.js +848 -0
  96. package/dist/theme/variables.d.ts +73 -0
  97. package/dist/theme/variables.js +171 -0
  98. package/dist/transport/post-message.d.ts +9 -0
  99. package/dist/transport/post-message.js +38 -0
  100. package/dist/transport/types.d.ts +15 -0
  101. package/dist/transport/types.js +1 -0
  102. package/package.json +63 -1
  103. package/src/bridge/bridge.ts +278 -0
  104. package/src/bridge/types.ts +95 -0
  105. package/src/compile/compile.ts +68 -0
  106. package/src/compile/policy.ts +130 -0
  107. package/src/config/capabilities.ts +138 -0
  108. package/src/config/schema.ts +168 -0
  109. package/src/config/validate.ts +318 -0
  110. package/src/conformance/behavior.ts +485 -0
  111. package/src/conformance/index.ts +116 -0
  112. package/src/conformance/kit.ts +63 -0
  113. package/src/conformance/oracles.ts +126 -0
  114. package/src/conformance/probes.ts +1129 -0
  115. package/src/conformance/served.ts +501 -0
  116. package/src/guide/examples.ts +74 -0
  117. package/src/guide/guide.ts +430 -0
  118. package/src/host/create-host.ts +335 -0
  119. package/src/host/locate.ts +25 -0
  120. package/src/host/outcome.ts +159 -0
  121. package/src/host/types.ts +126 -0
  122. package/src/index.ts +128 -0
  123. package/src/isolation/served.ts +151 -0
  124. package/src/isolation/types.ts +91 -0
  125. package/src/mcp/adapter.ts +441 -0
  126. package/src/mcp/index.ts +50 -0
  127. package/src/mcp/proxy.ts +117 -0
  128. package/src/mcp/view.ts +97 -0
  129. package/src/platform/types.ts +55 -0
  130. package/src/platform/web.ts +91 -0
  131. package/src/presets/presets.ts +217 -0
  132. package/src/protocol/adapter.ts +67 -0
  133. package/src/protocol/jsonrpc.ts +83 -0
  134. package/src/protocol/mcp-apps.ts +138 -0
  135. package/src/protocol/shell-adapter.ts +261 -0
  136. package/src/react/dialog.tsx +158 -0
  137. package/src/react/frame.tsx +486 -0
  138. package/src/react/index.ts +20 -0
  139. package/src/react/live.ts +76 -0
  140. package/src/server/index.ts +37 -0
  141. package/src/server/keys.ts +100 -0
  142. package/src/server/pages.ts +117 -0
  143. package/src/server/proxy.ts +131 -0
  144. package/src/server/server.ts +657 -0
  145. package/src/server/tickets.ts +30 -0
  146. package/src/server/types.ts +104 -0
  147. package/src/shell/document.ts +121 -0
  148. package/src/shell/plan.ts +69 -0
  149. package/src/shell/runtime.ts +848 -0
  150. package/src/theme/variables.ts +190 -0
  151. package/src/transport/post-message.ts +45 -0
  152. package/src/transport/types.ts +16 -0
package/src/index.ts ADDED
@@ -0,0 +1,128 @@
1
+ /**
2
+ * `@botiverse/porthole`: the framework-agnostic core. One config (a preset's) compiles into the
3
+ * frame's policies, the shell, and the agent manual; the host mounts displays under it.
4
+ */
5
+
6
+ // The config and its vocabulary.
7
+ export {
8
+ CAPABILITIES,
9
+ CAPABILITY_NAMES,
10
+ capabilitySpec,
11
+ isCapabilityName,
12
+ type CapabilityArgs,
13
+ type CapabilityName,
14
+ type CapabilityResults,
15
+ type CapabilitySpec,
16
+ type Gate,
17
+ type HandledCapability,
18
+ } from "./config/capabilities.ts";
19
+ export type {
20
+ CapabilityConfig,
21
+ CapabilityMap,
22
+ ColorScheme,
23
+ DisplayMode,
24
+ EgressConfig,
25
+ EmbedderConfig,
26
+ GuideConfig,
27
+ IsolationKind,
28
+ LayoutConfig,
29
+ Limits,
30
+ PortholeConfig,
31
+ ThemeConfig,
32
+ ThemeValue,
33
+ } from "./config/schema.ts";
34
+ export {
35
+ BUILT_IN_TOPICS,
36
+ ConfigError,
37
+ isOriginSource,
38
+ originSourceProblem,
39
+ validateConfig,
40
+ } from "./config/validate.ts";
41
+ export { URL_SANDBOX_EXTRAS, type UrlSandboxExtra } from "./config/schema.ts";
42
+ export { hostOriginProblem, isHostOrigin } from "./isolation/served.ts";
43
+
44
+ // Presets.
45
+ export {
46
+ LIBRARY_CDNS,
47
+ presets,
48
+ type PresetName,
49
+ type PresetOptions,
50
+ type UrlPresetOptions,
51
+ } from "./presets/presets.ts";
52
+
53
+ // The compiler.
54
+ export { compile, type CompiledConfig } from "./compile/compile.ts";
55
+ export {
56
+ allEgressOrigins,
57
+ declaredCapabilities,
58
+ effectiveGates,
59
+ egressOrigins,
60
+ embedderCsp,
61
+ type CspDirective,
62
+ type EgressKind,
63
+ } from "./compile/policy.ts";
64
+ export { shellPlan, type ShellPlan } from "./shell/plan.ts";
65
+ export type { ShellDocumentOptions } from "./shell/document.ts";
66
+ export {
67
+ generateGuide,
68
+ generateToolDescription,
69
+ type BuiltInTopic,
70
+ type GuideTopic,
71
+ } from "./guide/guide.ts";
72
+ export { GUIDE_EXAMPLES, type GuideExample } from "./guide/examples.ts";
73
+
74
+ // Theme.
75
+ export {
76
+ CHART_PALETTE,
77
+ DEFAULT_THEME,
78
+ DEFAULT_THEME_EVENT,
79
+ DEFAULT_THEME_VARIABLES,
80
+ resolveTheme,
81
+ themeDefault,
82
+ } from "./theme/variables.ts";
83
+
84
+ // The seams: isolation, platform, transport, protocol, bridge, host.
85
+ export type {
86
+ FrameLocation,
87
+ FrameSource,
88
+ InstanceLocator,
89
+ InstanceRef,
90
+ ServedDocument,
91
+ } from "./isolation/types.ts";
92
+ export type { PlatformHost, Surface, SurfaceSpec } from "./platform/types.ts";
93
+ export type { Transport } from "./transport/types.ts";
94
+ export type {
95
+ AdapterConnection,
96
+ AdapterEvent,
97
+ AdapterSession,
98
+ ProtocolAdapter,
99
+ ShellConnection,
100
+ } from "./protocol/adapter.ts";
101
+ export {
102
+ MCP_APPS_METHODS,
103
+ MCP_APPS_PROTOCOL_VERSION,
104
+ MCP_APPS_STYLE_VARIABLES,
105
+ PORTHOLE_STYLES,
106
+ type HostContext,
107
+ } from "./protocol/mcp-apps.ts";
108
+ export type {
109
+ CallContext,
110
+ CallOutcome,
111
+ CapabilityHandler,
112
+ CapabilityHandlers,
113
+ Confirm,
114
+ ConfirmAnswer,
115
+ ConfirmRequest,
116
+ Proceed,
117
+ HostFunctionBridge,
118
+ Refusal,
119
+ } from "./bridge/types.ts";
120
+ export { createHost } from "./host/create-host.ts";
121
+ export type {
122
+ DisplayControls,
123
+ DisplayEvents,
124
+ DisplayHandle,
125
+ HostOptions,
126
+ MountOptions,
127
+ PortholeHost,
128
+ } from "./host/types.ts";
@@ -0,0 +1,151 @@
1
+ import type { FrameLocation } from "./types.ts";
2
+
3
+ /**
4
+ * Checks for served frames (domain and external isolation), shared by the host's mount and by
5
+ * `/server`: a served origin must be one the config names, and never of the host's own site
6
+ * (P016, P020). Pure, no DOM and no Node, so both sides run the same code.
7
+ */
8
+
9
+ const EXACT_ORIGIN = /^(https?):\/\/([a-z0-9.-]+|\[[0-9a-f:.]+\])(?::(\d{1,5}))?$/u;
10
+ const IPV4 = /^\d{1,3}(\.\d{1,3}){3}$/u;
11
+
12
+ interface ParsedOrigin {
13
+ readonly scheme: string;
14
+ readonly host: string;
15
+ readonly port: string;
16
+ }
17
+
18
+ const DEFAULT_PORTS: Readonly<Record<string, string>> = { http: "80", https: "443" };
19
+
20
+ /** `scheme://host[:port]`, lowercase, nothing else; null for anything that isn't exactly that. */
21
+ export function parseOrigin(origin: string): ParsedOrigin | null {
22
+ const match = EXACT_ORIGIN.exec(origin);
23
+ if (match === null) {
24
+ return null;
25
+ }
26
+ const [, scheme = "", host = "", port] = match;
27
+ return { scheme, host, port: port ?? DEFAULT_PORTS[scheme] ?? "" };
28
+ }
29
+
30
+ /** Whether `origin` is an exact origin (`https://host[:port]`, lowercase, no path). */
31
+ export function isExactOrigin(origin: unknown): origin is string {
32
+ return typeof origin === "string" && parseOrigin(origin) !== null;
33
+ }
34
+
35
+ /**
36
+ * A host page's origin, exactly as `location.origin` and a message's `event.origin` give it:
37
+ * `scheme://host[:port]`, lowercase, any scheme (an Electron or Tauri app's own registered scheme,
38
+ * `ferry://app`, `app://local`), never `"null"` (an opaque origin isn't one origin: every opaque
39
+ * page says "null") and never a path. The shell takes messages only from these, exactly.
40
+ */
41
+ const HOST_ORIGIN =
42
+ /^[a-z][a-z0-9+.-]*:\/\/([a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*|\[[0-9a-f:.]+\])(:\d{1,5})?$/u;
43
+
44
+ export function isHostOrigin(origin: unknown): origin is string {
45
+ return typeof origin === "string" && origin.length <= 300 && HOST_ORIGIN.test(origin);
46
+ }
47
+
48
+ /** Why `origin` can't be a host origin, in words a host's developer can act on; null if it can. */
49
+ export function hostOriginProblem(origin: unknown): string | null {
50
+ if (isHostOrigin(origin)) {
51
+ return null;
52
+ }
53
+ if (origin === "null") {
54
+ return 'the host page\'s origin is opaque ("null"): serve it from a real origin (an Electron custom protocol must be registered as standard, and file: pages have none)';
55
+ }
56
+ return `${JSON.stringify(origin)} is not an exact origin (scheme://host[:port], lowercase, no path)`;
57
+ }
58
+
59
+ /**
60
+ * Whether two hosts may be the same site. Without the public-suffix list this errs one way only:
61
+ * it says "same site" whenever the hosts are equal or share their last two labels, so a pair it
62
+ * calls cross-site always is (a refused pair, like `a.github.io` and `b.github.io`, may not be:
63
+ * a safe failure). An IP address is a site of its own; `localhost`'s subdomains are each one, as
64
+ * browsers treat an unlisted top-level name as a public suffix.
65
+ */
66
+ export function maybeSameSite(hostA: string, hostB: string): boolean {
67
+ const a = hostA.toLowerCase().replace(/\.$/u, "");
68
+ const b = hostB.toLowerCase().replace(/\.$/u, "");
69
+ if (a === b) {
70
+ return true;
71
+ }
72
+ if (IPV4.test(a) || IPV4.test(b) || a.startsWith("[") || b.startsWith("[")) {
73
+ return false;
74
+ }
75
+ const labelsA = a.split(".").toReversed();
76
+ const labelsB = b.split(".").toReversed();
77
+ let shared = 0;
78
+ while (
79
+ shared < labelsA.length &&
80
+ shared < labelsB.length &&
81
+ labelsA[shared] === labelsB[shared]
82
+ ) {
83
+ shared += 1;
84
+ }
85
+ return shared >= 2;
86
+ }
87
+
88
+ /**
89
+ * Whether `origin` matches a frame source as the config writes them (`https://*.example.com`,
90
+ * `http://*.localhost:8790`, or an exact origin): same scheme and port; `*.` matches one or more
91
+ * labels below the name, never the name itself.
92
+ */
93
+ export function originMatchesSource(origin: string, source: string): boolean {
94
+ const parsed = parseOrigin(origin);
95
+ const wildcard = /^(https?):\/\/\*\.([a-z0-9.-]+)(?::(\d{1,5}))?$/u.exec(source);
96
+ if (parsed === null) {
97
+ return false;
98
+ }
99
+ if (wildcard === null) {
100
+ const exact = parseOrigin(source);
101
+ return (
102
+ exact !== null &&
103
+ exact.scheme === parsed.scheme &&
104
+ exact.host === parsed.host &&
105
+ exact.port === parsed.port
106
+ );
107
+ }
108
+ const [, scheme = "", suffix = "", port] = wildcard;
109
+ return (
110
+ scheme === parsed.scheme &&
111
+ (port ?? DEFAULT_PORTS[scheme]) === parsed.port &&
112
+ parsed.host.endsWith(`.${suffix}`) &&
113
+ parsed.host.length > suffix.length + 1
114
+ );
115
+ }
116
+
117
+ /**
118
+ * Throws unless `location` is a frame a host on `hostOrigin` may load for a config whose content
119
+ * is served from `contentOrigins`: its URL is on its stated origin, that origin is one the config
120
+ * names, and it is not of the host's own site (a same-site frame with `allow-same-origin` could
121
+ * reach cookies and storage the host shares with its site).
122
+ */
123
+ export function checkFrameLocation(
124
+ location: FrameLocation,
125
+ contentOrigins: readonly string[],
126
+ hostOrigin: string,
127
+ ): void {
128
+ const origin = parseOrigin(location.origin);
129
+ let url: URL;
130
+ try {
131
+ url = new URL(location.url);
132
+ } catch {
133
+ throw new TypeError(`The located URL ${JSON.stringify(location.url)} is not a URL`);
134
+ }
135
+ if (origin === null || url.origin !== location.origin) {
136
+ throw new TypeError(
137
+ `The located URL ${JSON.stringify(location.url)} is not on its origin ${JSON.stringify(location.origin)}`,
138
+ );
139
+ }
140
+ if (!contentOrigins.some((source) => originMatchesSource(location.origin, source))) {
141
+ throw new TypeError(
142
+ `The located origin ${location.origin} is not one of the config's content origins`,
143
+ );
144
+ }
145
+ const host = parseOrigin(hostOrigin);
146
+ if (host !== null && maybeSameSite(origin.host, host.host)) {
147
+ throw new TypeError(
148
+ `The located origin ${location.origin} may be the same site as the host (${hostOrigin}): content must be served from a registrable domain the host uses for nothing else`,
149
+ );
150
+ }
151
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * The isolation-mode seam. Opaque frames need nothing from outside: the host builds the shell
3
+ * document and the frame is `srcdoc`. Served modes (domain, and url mode's external app) load a
4
+ * URL, so the host asks a locator for one; the locator is the host's own code, typically a call to
5
+ * the host's backend, which asks `/server` for a ticketed URL after deciding the viewer may see it.
6
+ *
7
+ * Storage is keyed by origin. In domain mode each storage scope gets its own subdomain of the
8
+ * content domain (`<label>.example-usercontent.com`, the label derived from the scope by `/server`,
9
+ * not guessable from it), so content with the same scope shares an origin and its storage and
10
+ * nothing else does. The host chooses the scope (PRINCIPLE 8): per display, per conversation, per
11
+ * app. Browsers partition a framed origin's storage by the top-level site, so it is stable within
12
+ * one host app.
13
+ */
14
+ import type { EgressConfig } from "../config/schema.ts";
15
+
16
+ /**
17
+ * Which document a served frame loads. `preset` names the config it is served under (the server's
18
+ * `configs`); the artifact and the proxy default to their presets' names.
19
+ */
20
+ export type ServedDocument =
21
+ /** The shell, served with its CSP in headers; content arrives over the transport as for opaque. */
22
+ | { readonly kind: "shell"; readonly preset: string }
23
+ /** A stored page (artifact mode), by id and version: versions are immutable. */
24
+ | {
25
+ readonly kind: "artifact";
26
+ readonly id: string;
27
+ readonly version: string;
28
+ /** Default `artifact`. */
29
+ readonly preset?: string;
30
+ }
31
+ /**
32
+ * The MCP Apps sandbox proxy: a second origin the host hands the view's HTML to, which becomes
33
+ * the view. `egress` is what the view declares (`_meta.ui.csp`, mapped by `/mcp`'s
34
+ * `viewEgress`), opened on top of the config's: its CSP goes out in the proxy's headers, before
35
+ * the HTML arrives.
36
+ */
37
+ | { readonly kind: "mcp-proxy"; readonly preset?: string; readonly egress?: EgressConfig }
38
+ /**
39
+ * Someone else's running app (url mode), e.g. a dev server behind the host's preview gateway:
40
+ * `url` is where the host's upstream finds it; the frame lands on its path and query.
41
+ */
42
+ | { readonly kind: "external"; readonly url: string };
43
+
44
+ /** One served instance: where its storage lives, and what it shows. */
45
+ export interface InstanceRef {
46
+ /**
47
+ * What the content's origin, and so its storage, is keyed by. Instances with the same scope
48
+ * share an origin. Opaque to Porthole: the host picks it (a display id, an app id).
49
+ */
50
+ readonly scope: string;
51
+ readonly document: ServedDocument;
52
+ }
53
+
54
+ /** Where a served frame loads from. */
55
+ export interface FrameLocation {
56
+ /** The URL the frame loads, carrying a one-time ticket where the content is private. */
57
+ readonly url: string;
58
+ /** Its origin: the host takes messages only from this origin, from this frame. */
59
+ readonly origin: string;
60
+ /**
61
+ * Whether the pages there report how loading went (`porthole/notifications/frame-status`):
62
+ * Porthole's `/server` does, so a page that never ran (refused framing) can be told apart from
63
+ * one that loaded. A host's own gateway may send the same message without this.
64
+ */
65
+ readonly reports?: boolean;
66
+ }
67
+
68
+ /** The message a served page sends its host about loading (own-prefixed, P040). */
69
+ export const FRAME_STATUS_METHOD = "porthole/notifications/frame-status";
70
+
71
+ /**
72
+ * Turns an instance into the URL its frame loads. Implemented by the host, usually by asking its
73
+ * own backend (which authenticates the viewer and calls `PortholeServer.issue`). Called on every
74
+ * mount: a ticket is good once.
75
+ */
76
+ export interface InstanceLocator {
77
+ locate(instance: InstanceRef, signal?: AbortSignal): Promise<FrameLocation>;
78
+ }
79
+
80
+ /**
81
+ * Where a frame's document comes from: built by the host (`srcdoc`, opaque isolation), or loaded
82
+ * from a URL (domain and external isolation).
83
+ */
84
+ export type FrameSource =
85
+ | { readonly kind: "srcdoc"; readonly html: string }
86
+ | {
87
+ readonly kind: "url";
88
+ readonly url: string;
89
+ readonly origin: string;
90
+ readonly reports?: boolean;
91
+ };