@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
@@ -0,0 +1,261 @@
1
+ import { CAPABILITY_NAMES, isCapabilityName } from "./capabilities.js";
2
+ import { URL_SANDBOX_EXTRAS } from "./schema.js";
3
+ /**
4
+ * Checks a config before anything is derived from it. Every string that ends up in a CSP, an
5
+ * attribute or the shell is checked here, so a malformed origin can't inject a directive
6
+ * (`https://a.com; script-src *`) and a typo can't widen a policy. A config that fails is refused
7
+ * whole, never repaired.
8
+ */
9
+ export class ConfigError extends Error {
10
+ path;
11
+ constructor(path, message) {
12
+ super(`${path}: ${message}`);
13
+ this.name = "ConfigError";
14
+ this.path = path;
15
+ }
16
+ }
17
+ /** The built-in manual topics; a host's own topics may not reuse these names. */
18
+ export const BUILT_IN_TOPICS = ["widget", "chart", "interactive"];
19
+ const LOOPBACK = /^(localhost|127\.0\.0\.1|\[::1\])$|\.localhost$/u;
20
+ const HOST = /^(\*\.)?([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$/u;
21
+ /**
22
+ * Whether `value` is a CSP host source Porthole accepts: a scheme, a host (optionally `*.` for
23
+ * every subdomain) and an optional port, nothing else. Insecure schemes only for loopback names.
24
+ */
25
+ export function isOriginSource(value) {
26
+ return originSourceProblem(value) === null;
27
+ }
28
+ /** Why `value` isn't an origin source Porthole accepts, in words to act on; null if it is. */
29
+ export function originSourceProblem(value) {
30
+ const shape = "is not an origin (scheme://host[:port], lowercase, no path)";
31
+ if (typeof value !== "string" || value.length > 253 + 16) {
32
+ return `${JSON.stringify(value)} ${shape}`;
33
+ }
34
+ const match = /^(https|http|wss|ws):\/\/([^/:]+|\[::1\])(:(\d{1,5}))?$/u.exec(value);
35
+ if (match === null) {
36
+ return `${JSON.stringify(value)} ${shape}`;
37
+ }
38
+ const [, scheme, host = "", , port] = match;
39
+ if (port !== undefined && (Number(port) < 1 || Number(port) > 65_535)) {
40
+ return `${JSON.stringify(value)} has no valid port`;
41
+ }
42
+ if (host !== "[::1]" && !HOST.test(host)) {
43
+ return `${JSON.stringify(value)} ${shape}`;
44
+ }
45
+ if ((scheme === "http" || scheme === "ws") && !LOOPBACK.test(host.replace(/^\*\./u, "x."))) {
46
+ return `${JSON.stringify(value)} uses ${scheme} off loopback: ${scheme} is allowed only for localhost, *.localhost, 127.0.0.1 and [::1] (loopback development); use ${scheme === "http" ? "https" : "wss"}`;
47
+ }
48
+ return null;
49
+ }
50
+ const fail = (path, message) => {
51
+ throw new ConfigError(path, message);
52
+ };
53
+ const isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
54
+ function object(value, path) {
55
+ return isObject(value) ? value : fail(path, "must be an object");
56
+ }
57
+ function only(value, keys, path) {
58
+ for (const key of Object.keys(value)) {
59
+ if (!keys.includes(key)) {
60
+ fail(`${path}.${key}`, "is not a known field");
61
+ }
62
+ }
63
+ }
64
+ function sources(value, path) {
65
+ if (value === undefined) {
66
+ return;
67
+ }
68
+ if (!Array.isArray(value)) {
69
+ fail(path, "must be an array of origins");
70
+ return;
71
+ }
72
+ value.forEach((source, index) => {
73
+ const problem = originSourceProblem(source);
74
+ if (problem !== null) {
75
+ fail(`${path}[${index}]`, problem);
76
+ }
77
+ });
78
+ }
79
+ function positive(value, path, max = Number.MAX_SAFE_INTEGER) {
80
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 1 || value > max) {
81
+ fail(path, `must be an integer from 1 to ${max}`);
82
+ }
83
+ }
84
+ function gate(value, path) {
85
+ if (value === "user-activation" || value === "confirm") {
86
+ return;
87
+ }
88
+ const limit = isObject(value) && isObject(value.rateLimit) ? value.rateLimit : null;
89
+ if (limit === null || Object.keys(value).length !== 1) {
90
+ fail(path, 'must be "user-activation", "confirm" or { rateLimit: { perMinute } }');
91
+ return;
92
+ }
93
+ only(limit, ["perMinute"], `${path}.rateLimit`);
94
+ positive(limit.perMinute, `${path}.rateLimit.perMinute`, 10_000);
95
+ }
96
+ const MODES = ["inline", "artifact", "mcp-app", "url"];
97
+ const ISOLATIONS = ["opaque", "domain", "external"];
98
+ /** Returns `input` as a config once every field checks out; throws a `ConfigError` otherwise. */
99
+ export function validateConfig(input) {
100
+ const config = object(input, "config");
101
+ only(config, [
102
+ "mode",
103
+ "isolation",
104
+ "capabilities",
105
+ "egress",
106
+ "theme",
107
+ "limits",
108
+ "layout",
109
+ "guide",
110
+ "contentOrigins",
111
+ "embedder",
112
+ "sandbox",
113
+ ], "config");
114
+ if (!MODES.includes(config.mode)) {
115
+ fail("config.mode", `must be one of ${MODES.join(", ")}`);
116
+ }
117
+ if (!ISOLATIONS.includes(config.isolation)) {
118
+ fail("config.isolation", `must be one of ${ISOLATIONS.join(", ")}`);
119
+ }
120
+ if ((config.mode === "url") !== (config.isolation === "external")) {
121
+ fail("config.isolation", 'url mode, and only url mode, is "external"');
122
+ }
123
+ const capabilities = object(config.capabilities, "config.capabilities");
124
+ for (const [name, value] of Object.entries(capabilities)) {
125
+ const path = `config.capabilities.${name}`;
126
+ if (!isCapabilityName(name)) {
127
+ fail(path, `is not a capability (${CAPABILITY_NAMES.join(", ")})`);
128
+ }
129
+ const capability = object(value, path);
130
+ only(capability, ["gates", "connect", "resource", "guide"], path);
131
+ if (capability.gates !== undefined) {
132
+ if (!Array.isArray(capability.gates)) {
133
+ fail(`${path}.gates`, "must be an array");
134
+ }
135
+ capability.gates.forEach((each, index) => gate(each, `${path}.gates[${index}]`));
136
+ }
137
+ sources(capability.connect, `${path}.connect`);
138
+ sources(capability.resource, `${path}.resource`);
139
+ if (capability.guide !== undefined && typeof capability.guide !== "string") {
140
+ fail(`${path}.guide`, "must be a string");
141
+ }
142
+ }
143
+ if (config.mode === "url" && Object.keys(capabilities).length > 0) {
144
+ fail("config.capabilities", "url mode has no shell, so it can declare no capabilities");
145
+ }
146
+ const egress = object(config.egress, "config.egress");
147
+ const kinds = ["connect", "script", "style", "img", "font", "media"];
148
+ only(egress, kinds, "config.egress");
149
+ for (const kind of kinds) {
150
+ sources(egress[kind], `config.egress.${kind}`);
151
+ }
152
+ const theme = object(config.theme, "config.theme");
153
+ only(theme, ["variables", "event", "documents"], "config.theme");
154
+ if (theme.documents !== undefined &&
155
+ theme.documents !== "declared" &&
156
+ theme.documents !== "always") {
157
+ fail("config.theme.documents", 'must be "declared" or "always"');
158
+ }
159
+ const variables = object(theme.variables, "config.theme.variables");
160
+ for (const [name, value] of Object.entries(variables)) {
161
+ const path = `config.theme.variables[${JSON.stringify(name)}]`;
162
+ if (!/^--[a-z0-9]+(-[a-z0-9]+)*$/u.test(name)) {
163
+ fail(path, "must be a CSS custom property name (--lowercase-words)");
164
+ }
165
+ const values = isObject(value) ? [value.light, value.dark] : [value];
166
+ if (isObject(value)) {
167
+ only(value, ["light", "dark"], path);
168
+ }
169
+ for (const each of values) {
170
+ if (typeof each !== "string" ||
171
+ each.length === 0 ||
172
+ each.length > 500 ||
173
+ /[;{}<>]/u.test(each)) {
174
+ fail(path, "must be a CSS value (1–500 characters, no ; { } < >), or { light, dark } of them");
175
+ }
176
+ }
177
+ }
178
+ if (typeof theme.event !== "string" || !/^[a-z][a-z0-9]*([:.-][a-z0-9]+)*$/u.test(theme.event)) {
179
+ fail("config.theme.event", 'must be an event name such as "porthole:theme"');
180
+ }
181
+ const limits = object(config.limits, "config.limits");
182
+ const limitNames = [
183
+ "maxHeightPx",
184
+ "minHeightPx",
185
+ "maxBytes",
186
+ "maxLiveFrames",
187
+ "declinesBeforeMute",
188
+ "maxPromptChars",
189
+ "maxErrorReports",
190
+ ];
191
+ only(limits, [...limitNames, "loadTimeoutMs"], "config.limits");
192
+ for (const name of limitNames) {
193
+ positive(limits[name], `config.limits.${name}`);
194
+ }
195
+ if (limits.loadTimeoutMs !== undefined) {
196
+ positive(limits.loadTimeoutMs, "config.limits.loadTimeoutMs", 600_000);
197
+ }
198
+ if (limits.minHeightPx > limits.maxHeightPx) {
199
+ fail("config.limits.minHeightPx", "must not exceed maxHeightPx");
200
+ }
201
+ const layout = object(config.layout, "config.layout");
202
+ only(layout, ["minWidthPx", "narrowWidthPx", "wideWidthPx"], "config.layout");
203
+ for (const name of ["minWidthPx", "narrowWidthPx", "wideWidthPx"]) {
204
+ positive(layout[name], `config.layout.${name}`, 10_000);
205
+ }
206
+ if (layout.minWidthPx > layout.narrowWidthPx ||
207
+ layout.narrowWidthPx > layout.wideWidthPx) {
208
+ fail("config.layout", "must hold minWidthPx ≤ narrowWidthPx ≤ wideWidthPx");
209
+ }
210
+ const guide = object(config.guide, "config.guide");
211
+ only(guide, ["toolName", "guideToolName", "topics"], "config.guide");
212
+ for (const name of ["toolName", "guideToolName"]) {
213
+ const value = guide[name];
214
+ if (value !== undefined &&
215
+ (typeof value !== "string" || !/^[A-Za-z0-9_.-]{1,64}$/u.test(value))) {
216
+ fail(`config.guide.${name}`, "must be a tool name (letters, digits, _ . -)");
217
+ }
218
+ }
219
+ if (guide.topics !== undefined) {
220
+ for (const [name, text] of Object.entries(object(guide.topics, "config.guide.topics"))) {
221
+ const path = `config.guide.topics.${name}`;
222
+ if (!/^[a-z][a-z0-9-]{0,31}$/u.test(name) ||
223
+ BUILT_IN_TOPICS.includes(name)) {
224
+ fail(path, "must be a new topic name (lowercase), not a built-in one");
225
+ }
226
+ if (typeof text !== "string" || text.trim() === "") {
227
+ fail(path, "must be the topic's text");
228
+ }
229
+ }
230
+ }
231
+ sources(config.contentOrigins, "config.contentOrigins");
232
+ const served = config.isolation === "domain" || config.isolation === "external";
233
+ const contentOrigins = config.contentOrigins;
234
+ if (served !== contentOrigins.length > 0) {
235
+ fail("config.contentOrigins", served
236
+ ? `${String(config.isolation)} isolation needs the origins its content is served from`
237
+ : "opaque isolation serves nothing: leave it empty");
238
+ }
239
+ const embedder = object(config.embedder, "config.embedder");
240
+ only(embedder, ["frameSrc"], "config.embedder");
241
+ // The host's own frames may include its own origin, by name.
242
+ sources(Array.isArray(embedder.frameSrc)
243
+ ? embedder.frameSrc.filter((source) => source !== "'self'")
244
+ : embedder.frameSrc, "config.embedder.frameSrc");
245
+ if (config.sandbox !== undefined) {
246
+ if (config.mode !== "url") {
247
+ fail("config.sandbox", "only url mode may add sandbox tokens");
248
+ }
249
+ if (!Array.isArray(config.sandbox)) {
250
+ fail("config.sandbox", "must be an array of sandbox tokens");
251
+ }
252
+ config.sandbox.forEach((token, index) => {
253
+ if (!URL_SANDBOX_EXTRAS.includes(token)) {
254
+ fail(`config.sandbox[${index}]`, /top-navigation|same-origin/u.test(String(token))
255
+ ? `${JSON.stringify(token)} is never granted (P037)`
256
+ : `must be one of ${URL_SANDBOX_EXTRAS.join(", ")}`);
257
+ }
258
+ });
259
+ }
260
+ return input;
261
+ }
@@ -0,0 +1,2 @@
1
+ import { type Probe } from "./kit.ts";
2
+ export declare const BEHAVIOR_PROBES: readonly Probe[];