@fungi.computer/oyster 0.2.0 → 0.3.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 (291) hide show
  1. package/README.md +396 -72
  2. package/dist/agents.d.ts +3 -2
  3. package/dist/agents.js +4 -2
  4. package/dist/app-builds.d.ts +81 -8
  5. package/dist/app-builds.js +213 -60
  6. package/dist/app-storage.d.ts +3 -3
  7. package/dist/approval-capability.d.ts +5 -0
  8. package/dist/approval-capability.js +46 -0
  9. package/dist/approval-home.d.ts +51 -0
  10. package/dist/approval-home.js +226 -0
  11. package/dist/apps/catalog.d.ts +335 -0
  12. package/dist/apps/catalog.js +46 -0
  13. package/dist/apps/gate.d.ts +20 -6
  14. package/dist/apps/gate.js +42 -16
  15. package/dist/apps/index.d.ts +3 -1
  16. package/dist/apps/index.js +3 -1
  17. package/dist/apps/installation.d.ts +132 -51
  18. package/dist/apps/installation.js +176 -70
  19. package/dist/apps/permissions.d.ts +247 -0
  20. package/dist/apps/permissions.js +255 -0
  21. package/dist/boot-capability-route.d.ts +15 -0
  22. package/dist/boot-capability-route.js +196 -0
  23. package/dist/boot-capability.d.ts +103 -0
  24. package/dist/boot-capability.js +141 -0
  25. package/dist/boot-construction.d.ts +12 -0
  26. package/dist/boot-construction.js +175 -0
  27. package/dist/boot-gate.d.ts +267 -0
  28. package/dist/boot-gate.js +236 -0
  29. package/dist/boot-host-script.d.ts +2 -0
  30. package/dist/boot-host-script.js +1 -0
  31. package/dist/boot-mount.d.ts +21 -0
  32. package/dist/boot-mount.js +65 -0
  33. package/dist/browser/boot.d.ts +73 -0
  34. package/dist/browser/boot.js +502 -0
  35. package/dist/browser/browser.d.ts +104 -0
  36. package/dist/browser/browser.js +374 -0
  37. package/dist/browser/capability-source.d.ts +76 -0
  38. package/dist/browser/capability-source.js +338 -0
  39. package/dist/browser/cdp-client.d.ts +56 -0
  40. package/dist/browser/cdp-client.js +184 -0
  41. package/dist/browser/errors.d.ts +79 -0
  42. package/dist/browser/errors.js +32 -0
  43. package/dist/browser/index.d.ts +16 -0
  44. package/dist/browser/index.js +14 -0
  45. package/dist/browser/lease-registry.d.ts +61 -0
  46. package/dist/browser/lease-registry.js +101 -0
  47. package/dist/browser/operations.d.ts +225 -0
  48. package/dist/browser/operations.js +137 -0
  49. package/dist/browser/page.d.ts +25 -0
  50. package/dist/browser/page.js +172 -0
  51. package/dist/browser/quick-reads.d.ts +29 -0
  52. package/dist/browser/quick-reads.js +160 -0
  53. package/dist/browser/sessions.d.ts +52 -0
  54. package/dist/browser/sessions.js +118 -0
  55. package/dist/browser/url-policy.d.ts +18 -0
  56. package/dist/browser/url-policy.js +58 -0
  57. package/dist/build/app-imports.d.ts +12 -0
  58. package/dist/build/app-imports.js +105 -0
  59. package/dist/build/build-policy.d.ts +5 -3
  60. package/dist/build/build-policy.js +5 -3
  61. package/dist/build/candidate-builder.d.ts +1 -1
  62. package/dist/build/candidate-cli.mjs +8375 -334
  63. package/dist/build/local-build.d.ts +2 -0
  64. package/dist/build/local-build.js +28 -6
  65. package/dist/build/snapshot-package-store.d.ts +1 -1
  66. package/dist/build/snapshot-package-store.js +7 -2
  67. package/dist/build/workspace-build.d.ts +92 -5
  68. package/dist/build/workspace-build.js +93 -19
  69. package/dist/build.d.ts +1 -1
  70. package/dist/build.js +1 -1
  71. package/dist/catalog-frame.js +1 -1
  72. package/dist/cloudflare.d.ts +13 -3
  73. package/dist/cloudflare.js +102 -4
  74. package/dist/frame-binding-wire-script.js +1 -1
  75. package/dist/frame-binding-wire.js +4 -2
  76. package/dist/git-auth.d.ts +0 -2
  77. package/dist/git-auth.js +0 -2
  78. package/dist/git.d.ts +0 -1
  79. package/dist/git.js +0 -2
  80. package/dist/host.d.ts +39 -13
  81. package/dist/host.js +268 -142
  82. package/dist/index.d.ts +25 -15
  83. package/dist/index.js +39 -16
  84. package/dist/install-release-cli.d.ts +2 -0
  85. package/dist/install-release-cli.js +46 -0
  86. package/dist/installation-client.d.ts +43 -0
  87. package/dist/installation-client.js +178 -0
  88. package/dist/installation-execution.d.ts +11 -0
  89. package/dist/installation-execution.js +184 -0
  90. package/dist/installation-operations.d.ts +1437 -0
  91. package/dist/installation-operations.js +664 -0
  92. package/dist/installation-store-generation.d.ts +3 -0
  93. package/dist/installation-store-generation.js +16 -0
  94. package/dist/instance-id.d.ts +10 -0
  95. package/dist/instance-id.js +10 -0
  96. package/dist/instance-refusal.d.ts +11 -0
  97. package/dist/instance-refusal.js +21 -0
  98. package/dist/internal/delivery.js +4 -2
  99. package/dist/internal/host-gate.js +7 -1
  100. package/dist/internal/registry-installations.js +4 -4
  101. package/dist/keys.d.ts +5 -1
  102. package/dist/keys.js +10 -2
  103. package/dist/machine-link/authority.d.ts +89 -0
  104. package/dist/machine-link/authority.js +37 -0
  105. package/dist/machine-link/client.d.ts +66 -0
  106. package/dist/machine-link/client.js +94 -0
  107. package/dist/machine-link/delivery.d.ts +18 -0
  108. package/dist/machine-link/delivery.js +55 -0
  109. package/dist/machine-link/enrollment.d.ts +111 -0
  110. package/dist/machine-link/enrollment.js +286 -0
  111. package/dist/machine-link/fences.d.ts +37 -0
  112. package/dist/machine-link/fences.js +166 -0
  113. package/dist/machine-link/home.d.ts +20 -0
  114. package/dist/machine-link/home.js +24 -0
  115. package/dist/machine-link/host.d.ts +43 -0
  116. package/dist/machine-link/host.js +189 -0
  117. package/dist/machine-link/index.d.ts +15 -0
  118. package/dist/machine-link/index.js +15 -0
  119. package/dist/machine-link/link.d.ts +48 -0
  120. package/dist/machine-link/link.js +223 -0
  121. package/dist/machine-link/presence.d.ts +10 -0
  122. package/dist/machine-link/presence.js +25 -0
  123. package/dist/machine-link/workers.d.ts +6 -0
  124. package/dist/machine-link/workers.js +6 -0
  125. package/dist/mcp-owner.d.ts +2 -2
  126. package/dist/mcp-owner.js +41 -33
  127. package/dist/mcp.d.ts +15 -3
  128. package/dist/mcp.js +146 -25
  129. package/dist/older-oyster-store-refusal.d.ts +4 -0
  130. package/dist/older-oyster-store-refusal.js +18 -0
  131. package/dist/older-oyster-store.d.ts +21 -0
  132. package/dist/older-oyster-store.js +43 -0
  133. package/dist/pack.d.ts +14 -3
  134. package/dist/pack.js +44 -19
  135. package/dist/page/app-access.d.ts +7 -0
  136. package/dist/page/app-access.js +75 -0
  137. package/dist/page/boot-host.js +57 -0
  138. package/dist/page/capabilities.d.ts +3 -3
  139. package/dist/page/capabilities.js +16 -12
  140. package/dist/page/computer-handles.d.ts +3 -3
  141. package/dist/page/computer-target.d.ts +3 -0
  142. package/dist/page/computer-target.js +9 -0
  143. package/dist/page/mount.d.ts +23 -3
  144. package/dist/page/mount.js +152 -16
  145. package/dist/page-assets.d.ts +22 -0
  146. package/dist/page-assets.js +28 -0
  147. package/dist/page-script.d.ts +3 -0
  148. package/dist/page-script.js +5 -4
  149. package/dist/release.d.ts +13 -6
  150. package/dist/release.js +10 -7
  151. package/dist/root-data.d.ts +16 -6
  152. package/dist/root-data.js +2 -2
  153. package/dist/source/index.d.ts +1 -1
  154. package/dist/source/team-source.d.ts +20 -8
  155. package/dist/source/team-source.js +39 -10
  156. package/dist/source/template-seed.d.ts +2 -2
  157. package/dist/standalone/agent-mail.d.ts +24 -0
  158. package/dist/standalone/agent-mail.js +62 -0
  159. package/dist/standalone/agent-store.d.ts +9 -0
  160. package/dist/standalone/agent-store.js +30 -0
  161. package/dist/standalone/agents.d.ts +68 -9
  162. package/dist/standalone/agents.js +428 -56
  163. package/dist/standalone/app-builds-binding.d.ts +20 -2
  164. package/dist/standalone/app-builds-binding.js +147 -23
  165. package/dist/standalone/app-set.d.ts +5 -2
  166. package/dist/standalone/app-set.js +186 -144
  167. package/dist/standalone/browser.d.ts +11 -0
  168. package/dist/standalone/browser.js +72 -0
  169. package/dist/standalone/builds.d.ts +13 -4
  170. package/dist/standalone/builds.js +28 -44
  171. package/dist/standalone/computer-egress.d.ts +2 -0
  172. package/dist/standalone/computer-egress.js +13 -4
  173. package/dist/standalone/computer-fleet.d.ts +43 -0
  174. package/dist/standalone/computer-fleet.js +200 -0
  175. package/dist/standalone/computer.d.ts +44 -9
  176. package/dist/standalone/computer.js +129 -22
  177. package/dist/standalone/install-apps-cli.js +43 -6
  178. package/dist/standalone/instance.d.ts +2 -2
  179. package/dist/standalone/instance.js +8 -11
  180. package/dist/standalone/machine-link.d.ts +354 -0
  181. package/dist/standalone/machine-link.js +368 -0
  182. package/dist/standalone/media.d.ts +2 -2
  183. package/dist/standalone/media.js +19 -16
  184. package/dist/standalone/preview.d.ts +2 -2
  185. package/dist/standalone/preview.js +37 -10
  186. package/dist/standalone/registry.d.ts +151 -9
  187. package/dist/standalone/registry.js +305 -8
  188. package/dist/standalone/worker.d.ts +19 -5
  189. package/dist/standalone/worker.js +90 -54
  190. package/dist/standalone/workflow-binding.d.ts +18 -22
  191. package/dist/standalone/workflow-binding.js +18 -18
  192. package/dist/standalone/workflow-control.d.ts +4 -0
  193. package/dist/standalone/workflow-control.js +40 -0
  194. package/dist/standalone/workflow-events.d.ts +2 -2
  195. package/dist/standalone/workflow-events.js +9 -10
  196. package/dist/standalone/workflow-git-capture.js +4 -3
  197. package/dist/standalone/workflow-host.d.ts +162 -161
  198. package/dist/standalone/workflow-host.js +101 -21
  199. package/dist/standalone/workflow-node-capture.js +11 -4
  200. package/dist/standalone/workflow-schedules.d.ts +5 -2
  201. package/dist/standalone/workflow-schedules.js +9 -10
  202. package/dist/standalone/workflow-trigger-host.d.ts +65 -5
  203. package/dist/standalone/workflow-trigger-host.js +2 -2
  204. package/dist/standalone/workflow-webhooks.d.ts +22 -0
  205. package/dist/standalone/workflow-webhooks.js +39 -0
  206. package/dist/standalone/workflows.d.ts +140 -43
  207. package/dist/standalone/workflows.js +198 -39
  208. package/dist/storage-frame.d.ts +6 -6
  209. package/dist/testing/capabilities.js +14 -3
  210. package/dist/testing/installation.js +26 -2
  211. package/dist/testing/worker.js +1 -1
  212. package/dist/testing/workers-runtime.d.ts +8 -2
  213. package/dist/testing/workers-runtime.js +20 -2
  214. package/dist/webhook-abuse.d.ts +25 -0
  215. package/dist/webhook-abuse.js +54 -0
  216. package/dist/webhook-custody.d.ts +144 -0
  217. package/dist/webhook-custody.js +222 -0
  218. package/dist/webhook-ingress.d.ts +36 -0
  219. package/dist/webhook-ingress.js +146 -0
  220. package/dist/webhooks.d.ts +111 -0
  221. package/dist/webhooks.js +189 -0
  222. package/dist/workers-git.d.ts +12 -17
  223. package/dist/workers-git.js +64 -52
  224. package/dist/workers.d.ts +226 -13
  225. package/dist/workers.js +363 -51
  226. package/dist/workflow-agent-authority.d.ts +8 -0
  227. package/dist/workflow-agent-authority.js +42 -0
  228. package/dist/workflows.d.ts +66 -0
  229. package/dist/workflows.js +97 -0
  230. package/docs/api/README.md +63 -37
  231. package/docs/api/agents.md +17 -0
  232. package/docs/api/app-storage.md +1 -1
  233. package/docs/api/apps.md +1394 -543
  234. package/docs/api/browser/boot.md +132 -0
  235. package/docs/api/browser.md +1440 -0
  236. package/docs/api/build-contract.md +38 -25
  237. package/docs/api/build.md +263 -84
  238. package/docs/api/cloudflare.md +93 -7
  239. package/docs/api/git.md +3 -13
  240. package/docs/api/host.md +944 -118
  241. package/docs/api/index.md +320 -18
  242. package/docs/api/keys.md +24 -1
  243. package/docs/api/mcp.md +59 -12
  244. package/docs/api/modules.md +33 -40
  245. package/docs/api/pack.md +57 -8
  246. package/docs/api/page/mount.md +92 -4
  247. package/docs/api/workers-git.md +103 -24
  248. package/docs/api/workers.md +1594 -244
  249. package/docs/site/agent-quickstart.mdx +28 -14
  250. package/examples/standalone/worker.ts +3 -1
  251. package/examples/standalone/wrangler.jsonc +2 -0
  252. package/package.json +73 -71
  253. package/dist/installation-consent.d.ts +0 -81
  254. package/dist/installation-consent.js +0 -84
  255. package/dist/shell/app-directory.d.ts +0 -542
  256. package/dist/shell/app-directory.js +0 -196
  257. package/dist/shell/browser-actions.d.ts +0 -7
  258. package/dist/shell/browser-actions.js +0 -56
  259. package/dist/shell/command-host.d.ts +0 -112
  260. package/dist/shell/command-host.js +0 -502
  261. package/dist/shell/commands.d.ts +0 -25
  262. package/dist/shell/commands.js +0 -78
  263. package/dist/shell/index.d.ts +0 -8
  264. package/dist/shell/index.js +0 -8
  265. package/dist/shell/keys.d.ts +0 -41
  266. package/dist/shell/keys.js +0 -134
  267. package/dist/shell/shell-module.d.ts +0 -55
  268. package/dist/shell/shell-module.js +0 -86
  269. package/dist/shell/window-manager.d.ts +0 -128
  270. package/dist/shell/window-manager.js +0 -730
  271. package/dist/shell/workbench-contract.d.ts +0 -68
  272. package/dist/shell/workbench-directory.d.ts +0 -332
  273. package/dist/shell/workbench-directory.js +0 -35
  274. package/dist/shell/workbench-host-types.d.ts +0 -96
  275. package/dist/shell/workbench-host-types.js +0 -1
  276. package/dist/shell/workbench-host.d.ts +0 -34
  277. package/dist/shell/workbench-host.js +0 -352
  278. package/dist/shell/workbench-model.d.ts +0 -194
  279. package/dist/shell/workbench-model.js +0 -53
  280. package/dist/shell/workbench.d.ts +0 -4
  281. package/dist/shell/workbench.js +0 -2
  282. package/docs/api/shell/app-directory.md +0 -599
  283. package/docs/api/shell/command-host.md +0 -224
  284. package/docs/api/shell/commands.md +0 -197
  285. package/docs/api/shell/keys.md +0 -183
  286. package/docs/api/shell/shell-module.md +0 -204
  287. package/docs/api/shell/window-manager.md +0 -267
  288. package/docs/api/shell/workbench-directory.md +0 -119
  289. package/docs/api/shell/workbench.md +0 -205
  290. package/docs/api/shell.md +0 -1915
  291. /package/dist/{shell/workbench-contract.js → page/boot-host.d.ts} +0 -0
@@ -0,0 +1,172 @@
1
+ import { z } from "zod";
2
+ import { CloudBrowserTimeout, CloudBrowserUpstreamError } from "./errors.js";
3
+ import { cloudBrowserBounds, } from "./operations.js";
4
+ /**
5
+ * Drive one page of a connected session through a closed set of CDP methods
6
+ * (navigate/snapshot/click/type/press/scroll/screenshot/pdf). Internal to the
7
+ * module: callers have already checked lease ownership and URL egress policy.
8
+ * The session-create response shape was confirmed by the live workerd smoke
9
+ * on 2026-10-03; see `test/browser/cdp-client.test.ts`.
10
+ */
11
+ const targetSchema = z.object({ targetId: z.string(), type: z.string() });
12
+ const getTargetsResultSchema = z.object({ targetInfos: z.array(targetSchema) });
13
+ const attachResultSchema = z.object({ sessionId: z.string() });
14
+ const navigateResultSchema = z.object({
15
+ frameId: z.string().optional(),
16
+ errorText: z.string().optional(),
17
+ });
18
+ const navigationHistorySchema = z.object({
19
+ currentIndex: z.number().int(),
20
+ entries: z.array(z.object({ url: z.string(), title: z.string().optional() })),
21
+ });
22
+ const axValueSchema = z.object({ value: z.json().optional() }).optional();
23
+ const axNodeSchema = z.object({
24
+ role: axValueSchema,
25
+ name: axValueSchema,
26
+ value: axValueSchema,
27
+ ignored: z.boolean().optional(),
28
+ });
29
+ const axTreeSchema = z.object({ nodes: z.array(axNodeSchema) });
30
+ const capturedDataSchema = z.object({ data: z.string() });
31
+ /** Attach to the session's first page target (or a new blank one). */
32
+ export async function attachPage(client) {
33
+ const targets = getTargetsResultSchema.parse(await client.call("Target.getTargets", {}));
34
+ const targetId = targets.targetInfos.find((target) => target.type === "page")?.targetId ??
35
+ z
36
+ .object({ targetId: z.string() })
37
+ .parse(await client.call("Target.createTarget", { url: "about:blank" }))
38
+ .targetId;
39
+ const { sessionId: cdpSessionId } = attachResultSchema.parse(await client.call("Target.attachToTarget", { targetId, flatten: true }));
40
+ await client.call("Page.enable", {}, { sessionId: cdpSessionId });
41
+ await client.call("Accessibility.enable", {}, { sessionId: cdpSessionId });
42
+ return { client, cdpSessionId, targetId };
43
+ }
44
+ /** The page's current history entry. */
45
+ export async function pageLocation({ client, cdpSessionId }, fallbackUrl) {
46
+ const history = navigationHistorySchema.parse(await client.call("Page.getNavigationHistory", {}, { sessionId: cdpSessionId }));
47
+ const current = history.entries[history.currentIndex];
48
+ return { url: current?.url ?? fallbackUrl, title: current?.title ?? null };
49
+ }
50
+ /** Navigate to an already policy-checked URL and wait for its load event. */
51
+ export async function navigatePage(page, url, timeoutMs) {
52
+ const { client, cdpSessionId } = page;
53
+ // Listen before navigating so a fast load is not missed; a navigation that
54
+ // fails never leaves a pending wait behind.
55
+ const load = { fired: false, notify: () => undefined };
56
+ const stop = client.on("Page.loadEventFired", (_params, sessionId) => {
57
+ if (sessionId !== cdpSessionId)
58
+ return;
59
+ load.fired = true;
60
+ load.notify();
61
+ });
62
+ let timer;
63
+ try {
64
+ const result = navigateResultSchema.parse(await client.call("Page.navigate", { url: url.href }, { sessionId: cdpSessionId }));
65
+ if (result.errorText !== undefined)
66
+ throw new CloudBrowserUpstreamError({
67
+ op: "navigate",
68
+ status: null,
69
+ message: result.errorText,
70
+ });
71
+ await new Promise((resolve, reject) => {
72
+ if (load.fired) {
73
+ resolve();
74
+ return;
75
+ }
76
+ load.notify = resolve;
77
+ timer = setTimeout(() => {
78
+ reject(new CloudBrowserTimeout({ op: "Page.loadEventFired", timeoutMs }));
79
+ }, timeoutMs);
80
+ });
81
+ return await pageLocation(page, url.href);
82
+ }
83
+ finally {
84
+ stop();
85
+ if (timer !== undefined)
86
+ clearTimeout(timer);
87
+ }
88
+ }
89
+ function axText(value) {
90
+ const parsed = z.string().min(1).safeParse(value);
91
+ return parsed.success ? parsed.data : null;
92
+ }
93
+ export async function snapshotPage({ client, cdpSessionId }, leaseId, signal) {
94
+ const tree = axTreeSchema.parse(await client.call("Accessibility.getFullAXTree", {}, { sessionId: cdpSessionId, signal }));
95
+ const lines = [];
96
+ let joinedLength = 0;
97
+ let truncated = false;
98
+ for (const node of tree.nodes) {
99
+ if (node.ignored === true)
100
+ continue;
101
+ const role = axText(node.role?.value);
102
+ const name = axText(node.name?.value);
103
+ const value = axText(node.value?.value);
104
+ if (role === null && name === null)
105
+ continue;
106
+ const line = [role, name, value]
107
+ .filter((part) => part !== null)
108
+ .join(" · ");
109
+ if (joinedLength + line.length > cloudBrowserBounds.maxSnapshotBytes) {
110
+ truncated = true;
111
+ break;
112
+ }
113
+ joinedLength += (lines.length === 0 ? 0 : 1) + line.length;
114
+ lines.push(line);
115
+ }
116
+ return { leaseId, tree: lines.join("\n"), truncated };
117
+ }
118
+ export async function clickPage({ client, cdpSessionId }, x, y, signal) {
119
+ const options = { sessionId: cdpSessionId, signal };
120
+ for (const type of ["mousePressed", "mouseReleased"])
121
+ await client.call("Input.dispatchMouseEvent", { type, x, y, button: "left", clickCount: 1 }, options);
122
+ }
123
+ export async function typePage({ client, cdpSessionId }, text, signal) {
124
+ await client.call("Input.insertText", { text }, { sessionId: cdpSessionId, signal });
125
+ }
126
+ export async function pressPage({ client, cdpSessionId }, key, signal) {
127
+ const options = { sessionId: cdpSessionId, signal };
128
+ for (const type of ["keyDown", "keyUp"])
129
+ await client.call("Input.dispatchKeyEvent", { type, key }, options);
130
+ }
131
+ export async function scrollPage({ client, cdpSessionId }, deltaX, deltaY, signal) {
132
+ await client.call("Input.dispatchMouseEvent", { type: "mouseWheel", x: 1, y: 1, deltaX, deltaY }, { sessionId: cdpSessionId, signal });
133
+ }
134
+ function decodeBase64(data) {
135
+ const binary = atob(data);
136
+ const bytes = new Uint8Array(binary.length);
137
+ for (let index = 0; index < binary.length; index += 1)
138
+ bytes[index] = binary.charCodeAt(index);
139
+ return bytes;
140
+ }
141
+ async function capture({ client, cdpSessionId }, input, leaseId, signal) {
142
+ const result = capturedDataSchema.parse(await client.call(input.method, input.params, {
143
+ sessionId: cdpSessionId,
144
+ signal,
145
+ }));
146
+ const bytes = decodeBase64(result.data);
147
+ if (bytes.byteLength > input.maxBytes)
148
+ throw new CloudBrowserUpstreamError({
149
+ op: input.op,
150
+ status: null,
151
+ message: `The ${input.op} exceeds the 8 MiB bound`,
152
+ });
153
+ return { leaseId, bytes, contentType: input.contentType };
154
+ }
155
+ export function screenshotPage(page, leaseId, signal) {
156
+ return capture(page, {
157
+ op: "screenshot",
158
+ method: "Page.captureScreenshot",
159
+ params: { format: "png" },
160
+ maxBytes: cloudBrowserBounds.maxScreenshotBytes,
161
+ contentType: "image/png",
162
+ }, leaseId, signal);
163
+ }
164
+ export function pdfPage(page, leaseId, signal) {
165
+ return capture(page, {
166
+ op: "pdf",
167
+ method: "Page.printToPDF",
168
+ params: {},
169
+ maxBytes: cloudBrowserBounds.maxPdfBytes,
170
+ contentType: "application/pdf",
171
+ }, leaseId, signal);
172
+ }
@@ -0,0 +1,29 @@
1
+ import type { HttpFetch } from "./cdp-client.js";
2
+ import { type ExtractResult, type LinksResult, type MarkdownResult, type ScrapeResult } from "./operations.js";
3
+ /** The Browser Run account and API token a host's Quick Reads use. */
4
+ export type BrowserRunQuickReadsConfig = Readonly<{
5
+ accountId: string;
6
+ apiToken: string;
7
+ fetch?: HttpFetch;
8
+ }>;
9
+ /** A read's result and the browser time the provider reports for it, if any. */
10
+ export type QuickRead<Result> = Readonly<{
11
+ result: Result;
12
+ browserMs: number | null;
13
+ }>;
14
+ /**
15
+ * The stateless half of the host port; only `browserRunQuickReads` builds
16
+ * one. The caller (`browser.ts`) owns the deadline through `signal`.
17
+ */
18
+ export type QuickReads = Readonly<{
19
+ /** Render a page as markdown, truncated to `maxChars`. */
20
+ markdown: (url: URL, maxChars: number, signal: AbortSignal) => Promise<QuickRead<MarkdownResult>>;
21
+ /** Render a page and return its links. */
22
+ links: (url: URL, signal: AbortSignal) => Promise<QuickRead<LinksResult>>;
23
+ /** Render a page and return elements matching CSS selectors. */
24
+ scrape: (url: URL, selectors: readonly string[], signal: AbortSignal) => Promise<QuickRead<ScrapeResult>>;
25
+ /** Render a page and extract structured data matching a prompt. */
26
+ extract: (url: URL, prompt: string, signal: AbortSignal) => Promise<QuickRead<ExtractResult>>;
27
+ }>;
28
+ /** Browser Run's REST Quick Actions as a host's `QuickReads`. */
29
+ export declare function browserRunQuickReads(config: BrowserRunQuickReadsConfig): QuickReads;
@@ -0,0 +1,160 @@
1
+ import { z } from "zod";
2
+ import { CloudBrowserUpstreamError } from "./errors.js";
3
+ import { cloudBrowserBounds, extractResultSchema, } from "./operations.js";
4
+ /**
5
+ * Cloudflare Browser Run's stateless "Quick Actions" REST endpoints. A Workers
6
+ * BROWSER binding has no equivalent, so a binding host offers no Quick Reads.
7
+ * Each call is one POST with an account-scoped API token holding only the
8
+ * "Browser Rendering Edit" permission. Every URL arrives already checked by
9
+ * `browser.ts` against `url-policy.ts`; this file never sees a raw one.
10
+ *
11
+ * https://developers.cloudflare.com/browser-run/rest-api/
12
+ * https://developers.cloudflare.com/api/resources/browser_rendering/subresources/markdown/methods/create/
13
+ * https://developers.cloudflare.com/api/resources/browser_rendering/subresources/links/methods/create/
14
+ * https://developers.cloudflare.com/api/resources/browser_rendering/subresources/scrape/methods/create/
15
+ * https://developers.cloudflare.com/api/resources/browser_rendering/subresources/json/methods/create/
16
+ */
17
+ const quickActionResponseSchema = z.object({
18
+ success: z.boolean(),
19
+ result: z.json().optional(),
20
+ errors: z
21
+ .array(z.object({ code: z.number(), message: z.string() }))
22
+ .optional(),
23
+ });
24
+ /**
25
+ * Browser Run reports each REST call's browser time in this header
26
+ * (https://developers.cloudflare.com/browser-run/rest-api/).
27
+ */
28
+ const browserMsHeaderSchema = z
29
+ .string()
30
+ .regex(/^\d{1,15}$/u)
31
+ .transform(Number)
32
+ .pipe(z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER));
33
+ function reportedBrowserMs(response) {
34
+ const parsed = browserMsHeaderSchema.safeParse(response.headers.get("x-browser-ms-used") ?? undefined);
35
+ return parsed.success ? parsed.data : null;
36
+ }
37
+ const scrapeElementSchema = z.object({
38
+ attributes: z
39
+ .array(z.object({ name: z.string(), value: z.string() }))
40
+ .optional(),
41
+ text: z.string().optional(),
42
+ html: z.string().optional(),
43
+ });
44
+ const scrapeEntrySchema = z.object({
45
+ selector: z.string(),
46
+ // Cloudflare's own examples are inconsistent about one match vs many; accept both.
47
+ results: z.union([scrapeElementSchema, z.array(scrapeElementSchema)]),
48
+ });
49
+ function truncate(text, maxChars) {
50
+ if (text.length <= maxChars)
51
+ return { text, truncated: false };
52
+ return { text: `${text.slice(0, maxChars)}\n[truncated]`, truncated: true };
53
+ }
54
+ function boundScrape(entries) {
55
+ let remaining = cloudBrowserBounds.maxScrapeBytes;
56
+ let truncated = false;
57
+ const mapped = [];
58
+ outer: for (const entry of entries) {
59
+ const source = Array.isArray(entry.results)
60
+ ? entry.results
61
+ : [entry.results];
62
+ const elements = [];
63
+ for (const element of source) {
64
+ const text = element.text ?? "";
65
+ const html = element.html ?? "";
66
+ const size = text.length + html.length;
67
+ if (size > remaining) {
68
+ const share = Math.max(remaining, 0);
69
+ elements.push({
70
+ text: text.slice(0, share),
71
+ html: html.slice(0, Math.max(share - text.length, 0)),
72
+ });
73
+ truncated = true;
74
+ mapped.push({ selector: entry.selector, elements });
75
+ break outer;
76
+ }
77
+ remaining -= size;
78
+ elements.push({ text, html });
79
+ }
80
+ mapped.push({ selector: entry.selector, elements });
81
+ }
82
+ return { result: mapped, truncated };
83
+ }
84
+ /** Browser Run's REST Quick Actions as a host's `QuickReads`. */
85
+ export function browserRunQuickReads(config) {
86
+ const fetcher = config.fetch ?? ((url, init) => fetch(url, init));
87
+ async function call(action, body, signal) {
88
+ const response = await fetcher(`https://api.cloudflare.com/client/v4/accounts/${encodeURIComponent(config.accountId)}/browser-rendering/${action}`, {
89
+ method: "POST",
90
+ headers: {
91
+ authorization: `Bearer ${config.apiToken}`,
92
+ "content-type": "application/json",
93
+ },
94
+ body: JSON.stringify(body),
95
+ signal,
96
+ });
97
+ const status = response.status;
98
+ let parsed;
99
+ try {
100
+ parsed = quickActionResponseSchema.parse(await response.json());
101
+ }
102
+ catch {
103
+ throw new CloudBrowserUpstreamError({
104
+ op: action,
105
+ status,
106
+ message: "Browser Run returned an unreadable response",
107
+ });
108
+ }
109
+ if (!response.ok || !parsed.success)
110
+ throw new CloudBrowserUpstreamError({
111
+ op: action,
112
+ status,
113
+ message: parsed.errors?.[0]?.message ??
114
+ `Browser Run ${action} failed (HTTP ${String(status)})`,
115
+ });
116
+ return { result: parsed.result, browserMs: reportedBrowserMs(response) };
117
+ }
118
+ return {
119
+ markdown: async (url, maxChars, signal) => {
120
+ const { result, browserMs } = await call("markdown", { url: url.href }, signal);
121
+ const bound = truncate(z.string().parse(result), maxChars);
122
+ return {
123
+ result: { url: url.href, text: bound.text, truncated: bound.truncated },
124
+ browserMs,
125
+ };
126
+ },
127
+ links: async (url, signal) => {
128
+ const { result, browserMs } = await call("links", { url: url.href }, signal);
129
+ const all = z.array(z.string()).parse(result);
130
+ return {
131
+ result: {
132
+ url: url.href,
133
+ links: all.slice(0, cloudBrowserBounds.maxLinks),
134
+ truncated: all.length > cloudBrowserBounds.maxLinks,
135
+ },
136
+ browserMs,
137
+ };
138
+ },
139
+ scrape: async (url, selectors, signal) => {
140
+ const { result, browserMs } = await call("scrape", {
141
+ url: url.href,
142
+ elements: selectors.map((selector) => ({ selector })),
143
+ }, signal);
144
+ return {
145
+ result: {
146
+ url: url.href,
147
+ ...boundScrape(z.array(scrapeEntrySchema).parse(result)),
148
+ },
149
+ browserMs,
150
+ };
151
+ },
152
+ extract: async (url, prompt, signal) => {
153
+ const { result, browserMs } = await call("json", { url: url.href, prompt }, signal);
154
+ return {
155
+ result: extractResultSchema.parse({ url: url.href, result }),
156
+ browserMs,
157
+ };
158
+ },
159
+ };
160
+ }
@@ -0,0 +1,52 @@
1
+ import { type CdpClient, type CdpSocket, type HttpFetch } from "./cdp-client.js";
2
+ /**
3
+ * One browser session's provider lifecycle: create, connect CDP, delete and
4
+ * confirm closure. Cloudflare Browser Run's REST API and a Workers BROWSER
5
+ * binding speak the same devtools protocol under different roots, so both
6
+ * hosts share this one implementation:
7
+ *
8
+ * - REST: `https://api.cloudflare.com/client/v4/accounts/{account}/browser-run/devtools`
9
+ * with `Authorization: Bearer` (https://developers.cloudflare.com/browser-run/cdp/,
10
+ * .../cdp/session-management/).
11
+ * - Binding: `binding.fetch("http://fake.host/v1/devtools/…")`, the endpoints
12
+ * a Workers Browser Run binding answers.
13
+ *
14
+ * Provider session ids stay inside the module; agent leases use their own ids.
15
+ */
16
+ export type AcquireOptions = Readonly<{
17
+ /** Provider idle keep-alive, clamped to Browser Run's 10s–20min range. */
18
+ keepAliveMs: number;
19
+ /** Provider guardrail: the only hosts this session may load. */
20
+ allowedDomains?: readonly string[];
21
+ }>;
22
+ /**
23
+ * The provider half of the host port. Built only by the two functions below.
24
+ * Each step is separate so `createBrowser` owns a provider session id from the
25
+ * moment it exists, including when its CDP connect then fails.
26
+ */
27
+ export type BrowserSessions = Readonly<{
28
+ /** Create one provider session; resolves its provider id. @internal */
29
+ create: (options: AcquireOptions, signal: AbortSignal) => Promise<string>;
30
+ /** Connect the CDP socket of a created session, bounded by `signal`. @internal */
31
+ connect: (sessionId: string, signal: AbortSignal) => Promise<CdpClient>;
32
+ /** Ask the provider to delete a session; 404 is already gone. @internal */
33
+ release: (sessionId: string, signal: AbortSignal) => Promise<"closed" | "closing">;
34
+ /** Reads the provider's session record until it reports an end time. @internal */
35
+ confirmClosed: (sessionId: string, deadlineAt: number) => Promise<void>;
36
+ }>;
37
+ /** A Workers BROWSER binding (`Fetcher`), reduced to the call this module makes. */
38
+ export type BrowserBinding = Readonly<{
39
+ fetch: HttpFetch;
40
+ }>;
41
+ /** Opens the CDP WebSocket for one provider session URL. */
42
+ export type CdpConnect = (url: string, headers: Readonly<Record<string, string>>, signal: AbortSignal) => Promise<CdpSocket>;
43
+ /** Cloudflare Browser Run over REST with an account token (Hub, any celld host). */
44
+ export declare function browserRunSessions(config: Readonly<{
45
+ accountId: string;
46
+ apiToken: string;
47
+ fetch?: HttpFetch;
48
+ /** The CDP WebSocket transport; defaults to the outbound upgrade over `fetch`. */
49
+ connect?: CdpConnect;
50
+ }>): BrowserSessions;
51
+ /** A standalone Oyster's own Workers BROWSER binding. */
52
+ export declare function bindingSessions(binding: BrowserBinding): BrowserSessions;
@@ -0,0 +1,118 @@
1
+ import { z } from "zod";
2
+ import { createCdpClient, openCdpSocket, } from "./cdp-client.js";
3
+ import { CloudBrowserUpstreamError } from "./errors.js";
4
+ const createdSchema = z.object({ sessionId: z.string().min(1).max(256) });
5
+ const releasedSchema = z.object({ status: z.enum(["closing", "closed"]) });
6
+ const sessionDetailsSchema = z
7
+ .object({ sessionId: z.string(), endTime: z.number().optional() })
8
+ .nullable();
9
+ const minKeepAliveMs = 10_000;
10
+ const maxKeepAliveMs = 1_200_000;
11
+ function devtoolsSessions(input) {
12
+ const sessionUrl = (sessionId) => `${input.root}/browser/${encodeURIComponent(sessionId)}`;
13
+ async function create(options, signal) {
14
+ const keepAlive = Math.min(Math.max(options.keepAliveMs, minKeepAliveMs), maxKeepAliveMs);
15
+ const init = {
16
+ method: "POST",
17
+ headers: input.headers,
18
+ signal,
19
+ };
20
+ if (options.allowedDomains !== undefined) {
21
+ // Guardrails travel in the create body (the WebSocket upgrade for an
22
+ // existing session carries none).
23
+ init.headers = { ...input.headers, "content-type": "application/json" };
24
+ init.body = JSON.stringify({
25
+ guardrails: { allowedDomains: options.allowedDomains },
26
+ });
27
+ }
28
+ const response = await input.fetch(`${input.root}/browser?keep_alive=${String(keepAlive)}`, init);
29
+ if (!response.ok)
30
+ throw new CloudBrowserUpstreamError({
31
+ op: "open",
32
+ status: response.status,
33
+ message: "The browser provider refused to create a session",
34
+ });
35
+ const created = createdSchema.safeParse(await response.json());
36
+ if (!created.success)
37
+ throw new CloudBrowserUpstreamError({
38
+ op: "open",
39
+ status: null,
40
+ message: "The browser provider returned an unreadable session",
41
+ });
42
+ return created.data.sessionId;
43
+ }
44
+ async function deleteSession(sessionId, signal) {
45
+ const response = await input.fetch(sessionUrl(sessionId), {
46
+ method: "DELETE",
47
+ headers: input.headers,
48
+ signal,
49
+ });
50
+ if (response.status === 404)
51
+ return "closed";
52
+ if (!response.ok)
53
+ throw new CloudBrowserUpstreamError({
54
+ op: "close",
55
+ status: response.status,
56
+ message: "The browser provider refused to close the session",
57
+ });
58
+ let body;
59
+ try {
60
+ body = await response.json();
61
+ }
62
+ catch {
63
+ // An unreadable acknowledgement is not proof of closure.
64
+ return "closing";
65
+ }
66
+ const released = releasedSchema.safeParse(body);
67
+ return released.success ? released.data.status : "closing";
68
+ }
69
+ return {
70
+ create,
71
+ connect: async (sessionId, signal) => createCdpClient(await input.connect(sessionUrl(sessionId), input.headers, signal)),
72
+ release: deleteSession,
73
+ confirmClosed: async (sessionId, deadlineAt) => {
74
+ const url = `${input.root}/session/${encodeURIComponent(sessionId)}`;
75
+ while (Date.now() < deadlineAt) {
76
+ const response = await input.fetch(url, {
77
+ method: "GET",
78
+ headers: input.headers,
79
+ signal: AbortSignal.timeout(Math.max(1, deadlineAt - Date.now())),
80
+ });
81
+ if (response.status !== 200)
82
+ break;
83
+ const details = sessionDetailsSchema.safeParse(await response.json());
84
+ if (!details.success || details.data?.sessionId !== sessionId)
85
+ break;
86
+ if ((details.data.endTime ?? 0) > 0)
87
+ return;
88
+ await new Promise((resolve) => setTimeout(resolve, Math.min(50, Math.max(0, deadlineAt - Date.now()))));
89
+ }
90
+ throw new CloudBrowserUpstreamError({
91
+ op: "close.confirm",
92
+ status: null,
93
+ message: "The browser provider did not confirm the session closed",
94
+ });
95
+ },
96
+ };
97
+ }
98
+ /** Cloudflare Browser Run over REST with an account token (Hub, any celld host). */
99
+ export function browserRunSessions(config) {
100
+ const fetcher = config.fetch ?? ((url, init) => fetch(url, init));
101
+ return devtoolsSessions({
102
+ root: `https://api.cloudflare.com/client/v4/accounts/${encodeURIComponent(config.accountId)}/browser-run/devtools`,
103
+ fetch: fetcher,
104
+ headers: { authorization: `Bearer ${config.apiToken}` },
105
+ connect: config.connect ??
106
+ ((url, headers, signal) => openCdpSocket(fetcher, url, headers, signal)),
107
+ });
108
+ }
109
+ /** A standalone Oyster's own Workers BROWSER binding. */
110
+ export function bindingSessions(binding) {
111
+ const fetcher = (url, init) => binding.fetch(url, init);
112
+ return devtoolsSessions({
113
+ root: "http://fake.host/v1/devtools",
114
+ fetch: fetcher,
115
+ headers: {},
116
+ connect: (url, headers, signal) => openCdpSocket(fetcher, url, headers, signal),
117
+ });
118
+ }
@@ -0,0 +1,18 @@
1
+ import { type HostResolver } from "@fungi.computer/veil/model-key-network";
2
+ export type UrlCheckOptions = Readonly<{
3
+ resolve?: HostResolver;
4
+ signal: AbortSignal;
5
+ }>;
6
+ /**
7
+ * The egress and URL policy every cloud-browser call checks before it reaches
8
+ * Cloudflare Browser Run: http(s) only, no embedded credentials, and the
9
+ * resolved name must be public unicast. This reuses the exact SSRF defence
10
+ * Veil's public-fetch door and the Team's BYO model-key intake already use
11
+ * (`@fungi.computer/veil/model-key-network`): a literal like `127.0.0.1` or
12
+ * `169.254.169.254`, a private range like `10/8`, the box's own address, and
13
+ * a rebinding hostname (one whose DNS answer is private) are all refused
14
+ * here, before Cloudflare ever sees the URL. The name is resolved once; the
15
+ * caller never connects to it directly — Cloudflare does, so this is a policy
16
+ * gate, not a proxy.
17
+ */
18
+ export declare function assertCloudBrowserUrlAllowed(raw: string, options: UrlCheckOptions): Promise<URL>;
@@ -0,0 +1,58 @@
1
+ import { VeilError } from "@fungi.computer/veil";
2
+ import { createDnsOverHttpsResolver, resolvePublicAddresses, } from "@fungi.computer/veil/model-key-network";
3
+ import { CloudBrowserUrlDenied } from "./errors.js";
4
+ /**
5
+ * The egress and URL policy every cloud-browser call checks before it reaches
6
+ * Cloudflare Browser Run: http(s) only, no embedded credentials, and the
7
+ * resolved name must be public unicast. This reuses the exact SSRF defence
8
+ * Veil's public-fetch door and the Team's BYO model-key intake already use
9
+ * (`@fungi.computer/veil/model-key-network`): a literal like `127.0.0.1` or
10
+ * `169.254.169.254`, a private range like `10/8`, the box's own address, and
11
+ * a rebinding hostname (one whose DNS answer is private) are all refused
12
+ * here, before Cloudflare ever sees the URL. The name is resolved once; the
13
+ * caller never connects to it directly — Cloudflare does, so this is a policy
14
+ * gate, not a proxy.
15
+ */
16
+ export async function assertCloudBrowserUrlAllowed(raw, options) {
17
+ let url;
18
+ try {
19
+ url = new URL(raw);
20
+ }
21
+ catch {
22
+ throw new CloudBrowserUrlDenied({
23
+ reason: "invalid_url",
24
+ message: `${raw} is not a URL`,
25
+ });
26
+ }
27
+ if (url.protocol !== "https:" && url.protocol !== "http:")
28
+ throw new CloudBrowserUrlDenied({
29
+ reason: "unsupported_scheme",
30
+ message: "only http(s) URLs are fetched",
31
+ });
32
+ if (url.username !== "" || url.password !== "")
33
+ throw new CloudBrowserUrlDenied({
34
+ reason: "credentials_in_url",
35
+ message: "URLs with credentials are refused",
36
+ });
37
+ const resolve = options.resolve ?? createDnsOverHttpsResolver();
38
+ try {
39
+ await resolvePublicAddresses(url.hostname, resolve, options.signal);
40
+ }
41
+ catch (error) {
42
+ if (options.signal.aborted)
43
+ throw error;
44
+ if (error instanceof VeilError && error.code === "denied")
45
+ throw new CloudBrowserUrlDenied({
46
+ reason: "not_public",
47
+ message: `${url.hostname} is not a public address`,
48
+ });
49
+ if (error instanceof VeilError)
50
+ throw new CloudBrowserUrlDenied({
51
+ reason: "dns_unavailable",
52
+ message: `${url.hostname} did not resolve`,
53
+ });
54
+ throw error;
55
+ }
56
+ url.hash = "";
57
+ return url;
58
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Find runtime package imports in the App, not in its authoring tools or in
3
+ * package export siblings. esbuild follows local modules and erases type-only
4
+ * imports; packages stay external until their declared products are compiled.
5
+ * parse5 owns HTML syntax, including inline module scripts.
6
+ */
7
+ export declare function appRuntimeImports(options: {
8
+ readonly root: string;
9
+ readonly entry: string;
10
+ readonly backendEntry?: string | undefined;
11
+ readonly shellEntry?: string | undefined;
12
+ }): Promise<ReadonlySet<string>>;