@omercnet/paseo-shared-browser 0.3.1-next.72.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.
- package/CHANGELOG.md +71 -0
- package/LICENSE +21 -0
- package/README.md +182 -0
- package/client/browser.tsx +2098 -0
- package/docs/images/shared-browser-compact.png +0 -0
- package/docs/images/shared-browser-wide.png +0 -0
- package/index.client.tsx +35 -0
- package/index.server.ts +118 -0
- package/package.json +78 -0
- package/paseo-plugin.json +10 -0
- package/scripts/prepare-dependencies.mjs +25 -0
- package/scripts/prepare-runtime.mjs +168 -0
- package/server/agent-browser-runtime.ts +970 -0
- package/server/browser-policy.ts +836 -0
- package/server/browser.ts +306 -0
- package/server/cdp.ts +265 -0
- package/server/electron.d.ts +1 -0
- package/server/mcp-entry.ts +194 -0
- package/server/runtime-owner.ts +122 -0
- package/server/runtime-protocol.ts +260 -0
- package/server/supervisor-client.ts +402 -0
- package/server/supervisor-entry.ts +9 -0
- package/server/supervisor.ts +1081 -0
- package/shared/browser.ts +364 -0
- package/tsconfig.json +29 -0
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import { defineRpc } from "@getpaseo/plugin";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
export const DEFAULT_VIEWPORT = { width: 1280, height: 800 } as const;
|
|
5
|
+
export const MIN_VIEWPORT = { width: 320, height: 480 } as const;
|
|
6
|
+
export const MAX_VIEWPORT = { width: 1600, height: 1200 } as const;
|
|
7
|
+
export const FRAME_MAX_BYTES = 800_000;
|
|
8
|
+
export const FRAME_MAX_BASE64_CHARS = Math.ceil(FRAME_MAX_BYTES / 3) * 4;
|
|
9
|
+
|
|
10
|
+
export const DEVICE_PRESETS = [
|
|
11
|
+
{
|
|
12
|
+
id: "desktop-chrome",
|
|
13
|
+
label: "Desktop Chrome",
|
|
14
|
+
shortLabel: "Desktop",
|
|
15
|
+
viewport: { width: 1280, height: 720 },
|
|
16
|
+
deviceScaleFactor: 1,
|
|
17
|
+
isMobile: false,
|
|
18
|
+
hasTouch: false,
|
|
19
|
+
platform: "Win32",
|
|
20
|
+
userAgent:
|
|
21
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Safari/537.36",
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: "iphone-15-pro",
|
|
25
|
+
label: "iPhone 15 Pro",
|
|
26
|
+
shortLabel: "iPhone 15",
|
|
27
|
+
viewport: { width: 393, height: 659 },
|
|
28
|
+
deviceScaleFactor: 3,
|
|
29
|
+
isMobile: true,
|
|
30
|
+
hasTouch: true,
|
|
31
|
+
platform: "iPhone",
|
|
32
|
+
userAgent:
|
|
33
|
+
"Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Mobile/15E148 Safari/604.1",
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: "pixel-7",
|
|
37
|
+
label: "Pixel 7",
|
|
38
|
+
shortLabel: "Pixel 7",
|
|
39
|
+
viewport: { width: 412, height: 839 },
|
|
40
|
+
deviceScaleFactor: 2.625,
|
|
41
|
+
isMobile: true,
|
|
42
|
+
hasTouch: true,
|
|
43
|
+
platform: "Linux armv81",
|
|
44
|
+
userAgent:
|
|
45
|
+
"Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/153.0.8010.12 Mobile Safari/537.36",
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: "ipad-pro-11",
|
|
49
|
+
label: "iPad Pro 11",
|
|
50
|
+
shortLabel: "iPad 11",
|
|
51
|
+
viewport: { width: 834, height: 1194 },
|
|
52
|
+
deviceScaleFactor: 2,
|
|
53
|
+
isMobile: true,
|
|
54
|
+
hasTouch: true,
|
|
55
|
+
platform: "iPad",
|
|
56
|
+
userAgent:
|
|
57
|
+
"Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.6 Mobile/15E148 Safari/604.1",
|
|
58
|
+
},
|
|
59
|
+
] as const;
|
|
60
|
+
|
|
61
|
+
export const DEVICE_PRESET_IDS = [
|
|
62
|
+
"desktop-chrome",
|
|
63
|
+
"iphone-15-pro",
|
|
64
|
+
"pixel-7",
|
|
65
|
+
"ipad-pro-11",
|
|
66
|
+
] as const;
|
|
67
|
+
|
|
68
|
+
export type DevicePresetId = (typeof DEVICE_PRESET_IDS)[number];
|
|
69
|
+
const devicePresetIdSchema = z.enum(DEVICE_PRESET_IDS);
|
|
70
|
+
|
|
71
|
+
const workspaceIdSchema = z
|
|
72
|
+
.string()
|
|
73
|
+
.min(1)
|
|
74
|
+
.max(256)
|
|
75
|
+
.regex(/^[A-Za-z0-9_-]+$/, "Invalid workspace ID");
|
|
76
|
+
const opaqueTokenSchema = z
|
|
77
|
+
.string()
|
|
78
|
+
.min(32)
|
|
79
|
+
.max(128)
|
|
80
|
+
.regex(/^[A-Za-z0-9_-]+$/, "Invalid token");
|
|
81
|
+
const generationSchema = z.number().int().nonnegative();
|
|
82
|
+
const epochSchema = z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER);
|
|
83
|
+
const runtimeIdSchema = opaqueTokenSchema;
|
|
84
|
+
|
|
85
|
+
export const browserRecoveryStateSchema = z.enum([
|
|
86
|
+
"available",
|
|
87
|
+
"runtime-unavailable",
|
|
88
|
+
"browser-restarted",
|
|
89
|
+
]);
|
|
90
|
+
|
|
91
|
+
export const viewportSchema = z.object({
|
|
92
|
+
width: z.number().int().min(MIN_VIEWPORT.width).max(MAX_VIEWPORT.width),
|
|
93
|
+
height: z.number().int().min(MIN_VIEWPORT.height).max(MAX_VIEWPORT.height),
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
export const browserStateSchema = z.object({
|
|
97
|
+
sessionId: opaqueTokenSchema,
|
|
98
|
+
workspaceId: workspaceIdSchema,
|
|
99
|
+
status: z.enum(["starting", "ready", "error"]),
|
|
100
|
+
url: z.string().max(8_192),
|
|
101
|
+
title: z.string().max(1_024),
|
|
102
|
+
canGoBack: z.boolean(),
|
|
103
|
+
canGoForward: z.boolean(),
|
|
104
|
+
viewport: viewportSchema,
|
|
105
|
+
navigationGeneration: generationSchema,
|
|
106
|
+
viewportGeneration: generationSchema,
|
|
107
|
+
devicePresetId: devicePresetIdSchema.nullable(),
|
|
108
|
+
userAgent: z.string().min(1).max(512),
|
|
109
|
+
controller: z.enum(["none", "self", "other"]),
|
|
110
|
+
controllerLabel: z.string().max(64).nullable(),
|
|
111
|
+
controllerExpiresAt: z.string().datetime().nullable(),
|
|
112
|
+
viewerCount: z.number().int().nonnegative(),
|
|
113
|
+
error: z.string().max(2_048).nullable(),
|
|
114
|
+
runtimeId: runtimeIdSchema.optional(),
|
|
115
|
+
runtimeCreatedAt: epochSchema.optional(),
|
|
116
|
+
bridgeEpoch: epochSchema.optional(),
|
|
117
|
+
recoveryState: browserRecoveryStateSchema.optional(),
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
export const browserFrameSchema = z.object({
|
|
121
|
+
sessionId: opaqueTokenSchema,
|
|
122
|
+
frameId: opaqueTokenSchema,
|
|
123
|
+
mimeType: z.literal("image/jpeg"),
|
|
124
|
+
transport: z.enum(["cdp-screencast", "screenshot"]),
|
|
125
|
+
dataBase64: z.string().max(FRAME_MAX_BASE64_CHARS),
|
|
126
|
+
byteLength: z.number().int().positive().max(FRAME_MAX_BYTES),
|
|
127
|
+
width: z.number().int().positive().max(MAX_VIEWPORT.width),
|
|
128
|
+
height: z.number().int().positive().max(MAX_VIEWPORT.height),
|
|
129
|
+
navigationGeneration: generationSchema,
|
|
130
|
+
viewportGeneration: generationSchema,
|
|
131
|
+
runtimeId: runtimeIdSchema.optional(),
|
|
132
|
+
captureEpoch: epochSchema.optional(),
|
|
133
|
+
capturedAt: z.string().datetime(),
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
export const attachBrowserRpc = defineRpc({
|
|
137
|
+
name: "shared-browser.attach",
|
|
138
|
+
input: z.object({
|
|
139
|
+
workspaceId: workspaceIdSchema,
|
|
140
|
+
viewerLabel: z.string().trim().min(1).max(64),
|
|
141
|
+
}),
|
|
142
|
+
output: z.object({
|
|
143
|
+
viewerToken: opaqueTokenSchema,
|
|
144
|
+
state: browserStateSchema,
|
|
145
|
+
}),
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
export const detachBrowserRpc = defineRpc({
|
|
149
|
+
name: "shared-browser.detach",
|
|
150
|
+
input: z.object({ viewerToken: opaqueTokenSchema }),
|
|
151
|
+
output: z.object({ detached: z.boolean() }),
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
export const listOpenBrowserWorkspacesRpc = defineRpc({
|
|
155
|
+
name: "shared-browser.presence",
|
|
156
|
+
input: z.object({}),
|
|
157
|
+
output: z.object({ workspaceIds: z.array(workspaceIdSchema).max(32) }),
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
export const captureBrowserRpc = defineRpc({
|
|
161
|
+
name: "shared-browser.capture",
|
|
162
|
+
input: z.object({
|
|
163
|
+
viewerToken: opaqueTokenSchema,
|
|
164
|
+
quality: z.enum(["low", "medium", "high"]).default("medium"),
|
|
165
|
+
knownFrameId: opaqueTokenSchema.nullable().default(null),
|
|
166
|
+
}),
|
|
167
|
+
output: z.object({
|
|
168
|
+
state: browserStateSchema,
|
|
169
|
+
frame: browserFrameSchema.nullable(),
|
|
170
|
+
}),
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
export const acquireControlRpc = defineRpc({
|
|
174
|
+
name: "shared-browser.control.acquire",
|
|
175
|
+
input: z.object({
|
|
176
|
+
viewerToken: opaqueTokenSchema,
|
|
177
|
+
takeover: z.boolean().default(false),
|
|
178
|
+
}),
|
|
179
|
+
output: z.object({
|
|
180
|
+
controlToken: opaqueTokenSchema,
|
|
181
|
+
state: browserStateSchema,
|
|
182
|
+
}),
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
export const releaseControlRpc = defineRpc({
|
|
186
|
+
name: "shared-browser.control.release",
|
|
187
|
+
input: z.object({
|
|
188
|
+
viewerToken: opaqueTokenSchema,
|
|
189
|
+
controlToken: opaqueTokenSchema,
|
|
190
|
+
}),
|
|
191
|
+
output: z.object({ state: browserStateSchema }),
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const expectedStateSchema = z.object({
|
|
195
|
+
sessionId: opaqueTokenSchema,
|
|
196
|
+
navigationGeneration: generationSchema,
|
|
197
|
+
viewportGeneration: generationSchema,
|
|
198
|
+
runtimeId: runtimeIdSchema.optional(),
|
|
199
|
+
bridgeEpoch: epochSchema.optional(),
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
export const navigateBrowserRpc = defineRpc({
|
|
203
|
+
name: "shared-browser.navigate",
|
|
204
|
+
input: z.object({
|
|
205
|
+
viewerToken: opaqueTokenSchema,
|
|
206
|
+
controlToken: opaqueTokenSchema,
|
|
207
|
+
expected: expectedStateSchema,
|
|
208
|
+
action: z.discriminatedUnion("kind", [
|
|
209
|
+
z.object({ kind: z.literal("goto"), url: z.string().trim().min(1).max(8_192) }),
|
|
210
|
+
z.object({ kind: z.literal("back") }),
|
|
211
|
+
z.object({ kind: z.literal("forward") }),
|
|
212
|
+
z.object({ kind: z.literal("reload") }),
|
|
213
|
+
]),
|
|
214
|
+
}),
|
|
215
|
+
output: z.object({ state: browserStateSchema }),
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
export const resizeBrowserRpc = defineRpc({
|
|
219
|
+
name: "shared-browser.viewport.resize",
|
|
220
|
+
input: z.object({
|
|
221
|
+
viewerToken: opaqueTokenSchema,
|
|
222
|
+
controlToken: opaqueTokenSchema,
|
|
223
|
+
expected: expectedStateSchema,
|
|
224
|
+
viewport: viewportSchema,
|
|
225
|
+
}),
|
|
226
|
+
output: z.object({ state: browserStateSchema }),
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
export const applyDevicePresetRpc = defineRpc({
|
|
230
|
+
name: "shared-browser.device.apply",
|
|
231
|
+
input: z.object({
|
|
232
|
+
viewerToken: opaqueTokenSchema,
|
|
233
|
+
controlToken: opaqueTokenSchema,
|
|
234
|
+
expected: expectedStateSchema,
|
|
235
|
+
presetId: devicePresetIdSchema,
|
|
236
|
+
}),
|
|
237
|
+
output: z.object({ state: browserStateSchema }),
|
|
238
|
+
});
|
|
239
|
+
const displayedPointSchema = z.object({
|
|
240
|
+
x: z.number().finite().nonnegative(),
|
|
241
|
+
y: z.number().finite().nonnegative(),
|
|
242
|
+
width: z.number().finite().positive().max(16_384),
|
|
243
|
+
height: z.number().finite().positive().max(16_384),
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
const targetFrameSchema = z.object({
|
|
247
|
+
frameId: opaqueTokenSchema,
|
|
248
|
+
navigationGeneration: generationSchema,
|
|
249
|
+
viewportGeneration: generationSchema,
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
export const browserInputEventSchema = z.discriminatedUnion("kind", [
|
|
253
|
+
z.object({
|
|
254
|
+
kind: z.literal("click"),
|
|
255
|
+
point: displayedPointSchema,
|
|
256
|
+
button: z.enum(["left", "right", "middle"]).default("left"),
|
|
257
|
+
clickCount: z.union([z.literal(1), z.literal(2)]).default(1),
|
|
258
|
+
}),
|
|
259
|
+
z.object({
|
|
260
|
+
kind: z.literal("move"),
|
|
261
|
+
point: displayedPointSchema,
|
|
262
|
+
}),
|
|
263
|
+
z.object({
|
|
264
|
+
kind: z.literal("drag"),
|
|
265
|
+
start: displayedPointSchema,
|
|
266
|
+
end: displayedPointSchema,
|
|
267
|
+
button: z.enum(["left", "right", "middle"]).default("left"),
|
|
268
|
+
}),
|
|
269
|
+
z.object({
|
|
270
|
+
kind: z.literal("scroll"),
|
|
271
|
+
point: displayedPointSchema,
|
|
272
|
+
deltaX: z.number().finite().min(-4_000).max(4_000),
|
|
273
|
+
deltaY: z.number().finite().min(-4_000).max(4_000),
|
|
274
|
+
}),
|
|
275
|
+
z.object({
|
|
276
|
+
kind: z.literal("type"),
|
|
277
|
+
text: z.string().min(1).max(4_000),
|
|
278
|
+
}),
|
|
279
|
+
z.object({
|
|
280
|
+
kind: z.literal("key"),
|
|
281
|
+
key: z.enum([
|
|
282
|
+
"Enter",
|
|
283
|
+
"Tab",
|
|
284
|
+
"Backspace",
|
|
285
|
+
"Delete",
|
|
286
|
+
"Escape",
|
|
287
|
+
"ArrowUp",
|
|
288
|
+
"ArrowDown",
|
|
289
|
+
"ArrowLeft",
|
|
290
|
+
"ArrowRight",
|
|
291
|
+
"Home",
|
|
292
|
+
"End",
|
|
293
|
+
"PageUp",
|
|
294
|
+
"PageDown",
|
|
295
|
+
"Space",
|
|
296
|
+
]),
|
|
297
|
+
}),
|
|
298
|
+
]);
|
|
299
|
+
|
|
300
|
+
export const sendBrowserInputRpc = defineRpc({
|
|
301
|
+
name: "shared-browser.input",
|
|
302
|
+
input: z.object({
|
|
303
|
+
viewerToken: opaqueTokenSchema,
|
|
304
|
+
controlToken: opaqueTokenSchema,
|
|
305
|
+
expected: expectedStateSchema,
|
|
306
|
+
target: targetFrameSchema,
|
|
307
|
+
event: browserInputEventSchema,
|
|
308
|
+
}),
|
|
309
|
+
output: z.object({ state: browserStateSchema }),
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
export type BrowserState = z.output<typeof browserStateSchema>;
|
|
313
|
+
export type BrowserFrame = z.output<typeof browserFrameSchema>;
|
|
314
|
+
export type BrowserInputEvent = z.output<typeof browserInputEventSchema>;
|
|
315
|
+
export type Viewport = z.output<typeof viewportSchema>;
|
|
316
|
+
export type BrowserRecoveryState = z.output<typeof browserRecoveryStateSchema>;
|
|
317
|
+
|
|
318
|
+
export function isBrowserStateCurrent(previous: BrowserState, next: BrowserState): boolean {
|
|
319
|
+
if (
|
|
320
|
+
previous.runtimeCreatedAt !== undefined &&
|
|
321
|
+
next.runtimeCreatedAt !== undefined &&
|
|
322
|
+
next.runtimeCreatedAt < previous.runtimeCreatedAt
|
|
323
|
+
) {
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
if (previous.sessionId !== next.sessionId) return true;
|
|
327
|
+
if (didBrowserRuntimeRestart(previous, next)) return true;
|
|
328
|
+
if (
|
|
329
|
+
previous.runtimeId &&
|
|
330
|
+
next.runtimeId &&
|
|
331
|
+
previous.runtimeId === next.runtimeId &&
|
|
332
|
+
previous.bridgeEpoch !== undefined &&
|
|
333
|
+
next.bridgeEpoch !== undefined &&
|
|
334
|
+
next.bridgeEpoch < previous.bridgeEpoch
|
|
335
|
+
) {
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
return (
|
|
339
|
+
next.navigationGeneration >= previous.navigationGeneration &&
|
|
340
|
+
next.viewportGeneration >= previous.viewportGeneration
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export function didBrowserRuntimeRestart(previous: BrowserState, next: BrowserState): boolean {
|
|
345
|
+
return Boolean(previous.runtimeId && next.runtimeId && previous.runtimeId !== next.runtimeId);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export interface MappedPoint {
|
|
349
|
+
x: number;
|
|
350
|
+
y: number;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export function mapDisplayedPoint(
|
|
354
|
+
point: z.output<typeof displayedPointSchema>,
|
|
355
|
+
viewport: Viewport,
|
|
356
|
+
): MappedPoint {
|
|
357
|
+
if (point.x > point.width || point.y > point.height) {
|
|
358
|
+
throw new Error("Pointer coordinates are outside the displayed frame");
|
|
359
|
+
}
|
|
360
|
+
return {
|
|
361
|
+
x: Math.min(viewport.width - 1, Math.max(0, (point.x / point.width) * viewport.width)),
|
|
362
|
+
y: Math.min(viewport.height - 1, Math.max(0, (point.y / point.height) * viewport.height)),
|
|
363
|
+
};
|
|
364
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"paths": {
|
|
7
|
+
"electron": ["./server/electron.d.ts"]
|
|
8
|
+
},
|
|
9
|
+
"lib": ["ES2024"],
|
|
10
|
+
"types": ["node", "react"],
|
|
11
|
+
"jsx": "react-jsx",
|
|
12
|
+
"strict": true,
|
|
13
|
+
"noUncheckedIndexedAccess": true,
|
|
14
|
+
"exactOptionalPropertyTypes": true,
|
|
15
|
+
"skipLibCheck": true,
|
|
16
|
+
"noEmit": true,
|
|
17
|
+
"esModuleInterop": true,
|
|
18
|
+
"allowSyntheticDefaultImports": true
|
|
19
|
+
},
|
|
20
|
+
"include": [
|
|
21
|
+
"index.client.tsx",
|
|
22
|
+
"index.server.ts",
|
|
23
|
+
"client/**/*.ts",
|
|
24
|
+
"client/**/*.tsx",
|
|
25
|
+
"server/**/*.ts",
|
|
26
|
+
"shared/**/*.ts",
|
|
27
|
+
"tests/**/*.ts"
|
|
28
|
+
]
|
|
29
|
+
}
|