@wenrwa/marketplace-cli 0.4.4 → 0.4.6
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/dist/__tests__/bridge-activity-parser.test.d.ts +1 -0
- package/dist/__tests__/bridge-activity-parser.test.js +0 -0
- package/dist/__tests__/bridge-activity-tap.test.d.ts +1 -0
- package/dist/__tests__/bridge-activity-tap.test.js +178 -0
- package/dist/__tests__/bridge-api-client.test.d.ts +1 -0
- package/dist/__tests__/bridge-api-client.test.js +69 -0
- package/dist/__tests__/bridge-browser-driver.test.d.ts +1 -0
- package/dist/__tests__/bridge-browser-driver.test.js +379 -0
- package/dist/__tests__/bridge-browser-frames.test.d.ts +1 -0
- package/dist/__tests__/bridge-browser-frames.test.js +166 -0
- package/dist/__tests__/bridge-browser-idle.test.d.ts +1 -0
- package/dist/__tests__/bridge-browser-idle.test.js +259 -0
- package/dist/__tests__/bridge-browser-relay.test.d.ts +1 -0
- package/dist/__tests__/bridge-browser-relay.test.js +670 -0
- package/dist/__tests__/bridge-bundle-installer.test.d.ts +1 -0
- package/dist/__tests__/bridge-bundle-installer.test.js +126 -0
- package/dist/__tests__/bridge-cdp-client.test.d.ts +1 -0
- package/dist/__tests__/bridge-cdp-client.test.js +133 -0
- package/dist/__tests__/bridge-chromium-launcher.test.d.ts +1 -0
- package/dist/__tests__/bridge-chromium-launcher.test.js +202 -0
- package/dist/__tests__/bridge-command-browser-options.test.d.ts +1 -0
- package/dist/__tests__/bridge-command-browser-options.test.js +37 -0
- package/dist/__tests__/bridge-config-mcp.test.js +4 -0
- package/dist/__tests__/bridge-context-manifest-frame.test.d.ts +1 -0
- package/dist/__tests__/bridge-context-manifest-frame.test.js +125 -0
- package/dist/__tests__/bridge-context-writer.test.d.ts +1 -0
- package/dist/__tests__/bridge-context-writer.test.js +161 -0
- package/dist/__tests__/bridge-device-registration.test.d.ts +1 -0
- package/dist/__tests__/bridge-device-registration.test.js +33 -0
- package/dist/__tests__/bridge-folder-picker-runner.test.d.ts +1 -0
- package/dist/__tests__/bridge-folder-picker-runner.test.js +37 -0
- package/dist/__tests__/bridge-folder-picker.test.d.ts +1 -0
- package/dist/__tests__/bridge-folder-picker.test.js +66 -0
- package/dist/__tests__/bridge-folder-registry.test.d.ts +1 -0
- package/dist/__tests__/bridge-folder-registry.test.js +107 -0
- package/dist/__tests__/bridge-frame-pacer.test.d.ts +1 -0
- package/dist/__tests__/bridge-frame-pacer.test.js +102 -0
- package/dist/__tests__/bridge-install-bundle-frame.test.d.ts +1 -0
- package/dist/__tests__/bridge-install-bundle-frame.test.js +107 -0
- package/dist/__tests__/bridge-local-add-core.test.d.ts +1 -0
- package/dist/__tests__/bridge-local-add-core.test.js +48 -0
- package/dist/__tests__/bridge-local-path-validation-fs.test.d.ts +1 -0
- package/dist/__tests__/bridge-local-path-validation-fs.test.js +88 -0
- package/dist/__tests__/bridge-local-path-validation.test.d.ts +1 -0
- package/dist/__tests__/bridge-local-path-validation.test.js +45 -0
- package/dist/__tests__/bridge-login-status.test.d.ts +1 -0
- package/dist/__tests__/bridge-login-status.test.js +60 -0
- package/dist/__tests__/bridge-relay-client.test.d.ts +1 -0
- package/dist/__tests__/bridge-relay-client.test.js +283 -0
- package/dist/__tests__/bridge-relay-mode.test.d.ts +1 -0
- package/dist/__tests__/bridge-relay-mode.test.js +33 -0
- package/dist/__tests__/bridge-relay-pucc14.test.d.ts +1 -0
- package/dist/__tests__/bridge-relay-pucc14.test.js +230 -0
- package/dist/__tests__/bridge-server-local-endpoints.test.d.ts +1 -0
- package/dist/__tests__/bridge-server-local-endpoints.test.js +197 -0
- package/dist/__tests__/bridge-server.test.js +22 -0
- package/dist/__tests__/bridge-workspace-multirepo.test.d.ts +1 -0
- package/dist/__tests__/bridge-workspace-multirepo.test.js +317 -0
- package/dist/__tests__/devices.test.d.ts +1 -0
- package/dist/__tests__/devices.test.js +66 -0
- package/dist/__tests__/local.test.d.ts +1 -0
- package/dist/__tests__/local.test.js +147 -0
- package/dist/bridge/activityParser.d.ts +53 -0
- package/dist/bridge/activityParser.js +202 -0
- package/dist/bridge/apiClient.d.ts +56 -0
- package/dist/bridge/apiClient.js +50 -0
- package/dist/bridge/browserDriver.d.ts +98 -0
- package/dist/bridge/browserDriver.js +265 -0
- package/dist/bridge/browserFrames.d.ts +182 -0
- package/dist/bridge/browserFrames.js +179 -0
- package/dist/bridge/browserIdle.d.ts +75 -0
- package/dist/bridge/browserIdle.js +120 -0
- package/dist/bridge/bundleInstaller.d.ts +31 -0
- package/dist/bridge/bundleInstaller.js +127 -0
- package/dist/bridge/cdpClient.d.ts +47 -0
- package/dist/bridge/cdpClient.js +102 -0
- package/dist/bridge/chromiumLauncher.d.ts +45 -0
- package/dist/bridge/chromiumLauncher.js +113 -0
- package/dist/bridge/config.d.ts +3 -0
- package/dist/bridge/contextWriter.d.ts +31 -0
- package/dist/bridge/contextWriter.js +139 -0
- package/dist/bridge/deviceRegistration.d.ts +28 -0
- package/dist/bridge/deviceRegistration.js +19 -0
- package/dist/bridge/folderPicker.d.ts +34 -0
- package/dist/bridge/folderPicker.js +36 -0
- package/dist/bridge/folderPickerRunner.d.ts +3 -0
- package/dist/bridge/folderPickerRunner.js +25 -0
- package/dist/bridge/folderRegistry.d.ts +19 -0
- package/dist/bridge/folderRegistry.js +100 -0
- package/dist/bridge/framePacer.d.ts +61 -0
- package/dist/bridge/framePacer.js +79 -0
- package/dist/bridge/localAdd.d.ts +28 -0
- package/dist/bridge/localAdd.js +67 -0
- package/dist/bridge/localPathValidation.d.ts +20 -0
- package/dist/bridge/localPathValidation.js +58 -0
- package/dist/bridge/localPathValidationFs.d.ts +2 -0
- package/dist/bridge/localPathValidationFs.js +98 -0
- package/dist/bridge/loginStatus.d.ts +38 -0
- package/dist/bridge/loginStatus.js +56 -0
- package/dist/bridge/realPty.js +6 -2
- package/dist/bridge/relayClient.d.ts +166 -0
- package/dist/bridge/relayClient.js +517 -0
- package/dist/bridge/repoCheckout.d.ts +31 -0
- package/dist/bridge/repoCheckout.js +18 -0
- package/dist/bridge/server.d.ts +33 -0
- package/dist/bridge/server.js +98 -10
- package/dist/bridge/sessionManager.d.ts +18 -0
- package/dist/bridge/sessionManager.js +15 -1
- package/dist/commands/bridge.d.ts +21 -0
- package/dist/commands/bridge.js +194 -1
- package/dist/commands/devices.d.ts +9 -0
- package/dist/commands/devices.js +53 -0
- package/dist/commands/local.d.ts +9 -0
- package/dist/commands/local.js +141 -0
- package/dist/index.js +7 -1
- package/package.json +1 -1
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cbs-01 — Cloud Browser Session frame protocol, v1.
|
|
3
|
+
*
|
|
4
|
+
* (docs/prd/cloud-browser-session.md, ADR TBD) A headless Chromium runs
|
|
5
|
+
* inside the user's per-user container, driven over CDP by the bridge, and
|
|
6
|
+
* streamed to the arena Browser pane over the SAME terminal relay socket as
|
|
7
|
+
* PTY frames (no parallel transport) — new non-PTY frame types, modeled on
|
|
8
|
+
* the existing `install-bundle` / `context-manifest` precedent in
|
|
9
|
+
* relayClient.ts (see PROTOCOL.md). This module defines ONLY the wire
|
|
10
|
+
* shapes + runtime type guards; it is deliberately NOT wired into
|
|
11
|
+
* relayClient.ts in this slice (no relay/backend code is touched by cbs-01).
|
|
12
|
+
*
|
|
13
|
+
* ── Envelope discipline (matches ClientFrame in relayClient.ts) ──────────
|
|
14
|
+
* - Protocol version: v1 (BROWSER_FRAME_PROTOCOL_VERSION below). Frames
|
|
15
|
+
* do not carry a per-frame `v` — like the existing terminal frames, the
|
|
16
|
+
* version is asserted once at connection handshake time, not per frame.
|
|
17
|
+
* - Text-only JSON envelope. Every frame is `{ t, sessionId, ...fields }`.
|
|
18
|
+
* Binary payloads (the JPEG screencast frame) ride as base64 `data`
|
|
19
|
+
* inside the JSON, exactly like PTY `data` frames already do.
|
|
20
|
+
* - Session identity: `browser:<containerId>` (mirrors the `login:` prefix
|
|
21
|
+
* precedent) — out of scope for this slice, but every frame here already
|
|
22
|
+
* carries `sessionId` so routing needs no protocol change to adopt it.
|
|
23
|
+
*
|
|
24
|
+
* ── Frame directions ──────────────────────────────────────────────────────
|
|
25
|
+
* pane -> container:
|
|
26
|
+
* `browser-open` { t, sessionId, url }
|
|
27
|
+
* `browser-input` { t, sessionId, event } — event is a discriminated
|
|
28
|
+
* union (mouse | key | insertText | wheel)
|
|
29
|
+
* container -> pane:
|
|
30
|
+
* `browser-frame` { t, sessionId, seq, data, w, h } — data is base64 JPEG
|
|
31
|
+
* `browser-state` { t, sessionId, state, url?, title?, reason? }
|
|
32
|
+
*
|
|
33
|
+
* ── Drop rule (the whole point of framePacer.ts) ──────────────────────────
|
|
34
|
+
* FRAMES are droppable — `browser-frame` is a keep-newest video stream; a
|
|
35
|
+
* slow consumer sees the newest frame, never an unbounded backlog.
|
|
36
|
+
* INPUTS are never dropped — `browser-input` is small and ORDER-SENSITIVE
|
|
37
|
+
* (a mouse-down must reach the page before its mouse-up); it bypasses the
|
|
38
|
+
* frame pacer entirely and is expected to ride the relay's existing
|
|
39
|
+
* ordered-delivery guarantee, same as PTY `input` frames today.
|
|
40
|
+
*
|
|
41
|
+
* ── cbs-06 additions (ack + sleep) ─────────────────────────────────────────
|
|
42
|
+
* Arena's Browser pane (cbs-05, landed on arena master) needed two shapes
|
|
43
|
+
* this file left undefined. This CLI file is the hand-maintained mirror
|
|
44
|
+
* (see the MIRROR NOTICE at the top of this block) — cbs-06 lands the
|
|
45
|
+
* matching addition here so the two repos' shape-pinning tests stay in
|
|
46
|
+
* sync:
|
|
47
|
+
* `browser-frame-ack` { t, sessionId, seq } — pane -> container. The
|
|
48
|
+
* other half of the keep-newest pacing contract: cbs-06 wires this
|
|
49
|
+
* into the bridge's CDP screencast ack gate (framePacer.ts via
|
|
50
|
+
* browserDriver.ackFrame()), replacing the interim cbs-02 policy of
|
|
51
|
+
* self-acking on send. The pane must ack every `browser-frame` it
|
|
52
|
+
* actually PAINTED (never batched, never sent for a frame that was
|
|
53
|
+
* superseded before it rendered).
|
|
54
|
+
* `browser-sleep` { t, sessionId } — pane -> container. The pane's
|
|
55
|
+
* explicit 💤 control (mirrors the terminal pane's manual-sleep
|
|
56
|
+
* affordance) — minimal, no payload beyond identity, same effect as
|
|
57
|
+
* the bridge's 15-minute idle auto-close (browserIdle.ts) but
|
|
58
|
+
* user-triggered.
|
|
59
|
+
*/
|
|
60
|
+
/** Protocol version for the cloud browser session frame set. */
|
|
61
|
+
export declare const BROWSER_FRAME_PROTOCOL_VERSION = 1;
|
|
62
|
+
/** Keyboard/mouse modifier flags — abstracted from CDP's bitmask so callers
|
|
63
|
+
* don't need to know CDP's encoding; the bridge's CDP driver (a later
|
|
64
|
+
* slice) maps these to `Input.dispatchMouseEvent`/`dispatchKeyEvent`'s
|
|
65
|
+
* `modifiers` bitfield. */
|
|
66
|
+
export interface BrowserInputModifiers {
|
|
67
|
+
shift?: boolean;
|
|
68
|
+
ctrl?: boolean;
|
|
69
|
+
alt?: boolean;
|
|
70
|
+
meta?: boolean;
|
|
71
|
+
}
|
|
72
|
+
export type BrowserMouseButton = 'left' | 'middle' | 'right';
|
|
73
|
+
export interface BrowserMouseInputEvent {
|
|
74
|
+
kind: 'mouse';
|
|
75
|
+
type: 'down' | 'up' | 'move';
|
|
76
|
+
x: number;
|
|
77
|
+
y: number;
|
|
78
|
+
/** Omitted on `move` (no button implicated). */
|
|
79
|
+
button?: BrowserMouseButton;
|
|
80
|
+
modifiers?: BrowserInputModifiers;
|
|
81
|
+
}
|
|
82
|
+
export interface BrowserKeyInputEvent {
|
|
83
|
+
kind: 'key';
|
|
84
|
+
type: 'down' | 'up';
|
|
85
|
+
/** DOM KeyboardEvent.key, e.g. 'a', 'Enter', 'Backspace'. */
|
|
86
|
+
key: string;
|
|
87
|
+
/** DOM KeyboardEvent.code, e.g. 'KeyA', 'Enter'. */
|
|
88
|
+
code: string;
|
|
89
|
+
modifiers?: BrowserInputModifiers;
|
|
90
|
+
}
|
|
91
|
+
/** IME composition / paste text — maps 1:1 to CDP's `Input.insertText`,
|
|
92
|
+
* which is how passwords from a password manager and non-Latin IME input
|
|
93
|
+
* reach the page without synthesizing a keystroke per character. */
|
|
94
|
+
export interface BrowserInsertTextInputEvent {
|
|
95
|
+
kind: 'insertText';
|
|
96
|
+
text: string;
|
|
97
|
+
}
|
|
98
|
+
export interface BrowserWheelInputEvent {
|
|
99
|
+
kind: 'wheel';
|
|
100
|
+
x: number;
|
|
101
|
+
y: number;
|
|
102
|
+
deltaX: number;
|
|
103
|
+
deltaY: number;
|
|
104
|
+
}
|
|
105
|
+
export type BrowserInputEvent = BrowserMouseInputEvent | BrowserKeyInputEvent | BrowserInsertTextInputEvent | BrowserWheelInputEvent;
|
|
106
|
+
/** pane -> container: open (or re-navigate) the user's private headless
|
|
107
|
+
* Chromium tab to `url`. */
|
|
108
|
+
export interface BrowserOpenFrame {
|
|
109
|
+
t: 'browser-open';
|
|
110
|
+
sessionId: string;
|
|
111
|
+
url: string;
|
|
112
|
+
}
|
|
113
|
+
/** pane -> container: one input event. NEVER paced/dropped (see the module
|
|
114
|
+
* doc block's drop rule) — every `browser-input` frame must reach the
|
|
115
|
+
* container in the order it was sent. */
|
|
116
|
+
export interface BrowserInputFrame {
|
|
117
|
+
t: 'browser-input';
|
|
118
|
+
sessionId: string;
|
|
119
|
+
event: BrowserInputEvent;
|
|
120
|
+
}
|
|
121
|
+
/** container -> pane: one JPEG screencast frame from CDP's
|
|
122
|
+
* `Page.startScreencast`. `data` is base64-encoded JPEG bytes; `seq` is a
|
|
123
|
+
* monotonically increasing per-session counter the pane acks by number
|
|
124
|
+
* (ack semantics live in the pane's consumer, not this type). This is the
|
|
125
|
+
* frame kind the keep-newest pacer (framePacer.ts) governs. */
|
|
126
|
+
export interface BrowserScreenFrame {
|
|
127
|
+
t: 'browser-frame';
|
|
128
|
+
sessionId: string;
|
|
129
|
+
seq: number;
|
|
130
|
+
/** base64 JPEG. */
|
|
131
|
+
data: string;
|
|
132
|
+
w: number;
|
|
133
|
+
h: number;
|
|
134
|
+
}
|
|
135
|
+
/** pane -> container: acknowledges a displayed `browser-frame` by `seq` —
|
|
136
|
+
* cbs-06 mirror of arena's cbs-05 addition, see the module doc block's
|
|
137
|
+
* "cbs-06 additions" section. NEVER sent for a frame that was superseded
|
|
138
|
+
* before it painted (keep-newest: the pane only ever holds the LATEST
|
|
139
|
+
* frame, so only the latest gets acked). */
|
|
140
|
+
export interface BrowserFrameAckFrame {
|
|
141
|
+
t: 'browser-frame-ack';
|
|
142
|
+
sessionId: string;
|
|
143
|
+
seq: number;
|
|
144
|
+
}
|
|
145
|
+
/** pane -> container: explicit manual sleep (the pane's 💤 control) —
|
|
146
|
+
* cbs-06 mirror of arena's cbs-05 addition, see the module doc block's
|
|
147
|
+
* "cbs-06 additions" section. Minimal: no payload beyond session identity. */
|
|
148
|
+
export interface BrowserSleepFrame {
|
|
149
|
+
t: 'browser-sleep';
|
|
150
|
+
sessionId: string;
|
|
151
|
+
}
|
|
152
|
+
export type BrowserSessionState = 'opening' | 'open' | 'asleep' | 'closed';
|
|
153
|
+
/** container -> pane: session lifecycle / navigation state. `reason` is a
|
|
154
|
+
* short machine string (e.g. 'idle-timeout', 'awake-cap') for `asleep`/
|
|
155
|
+
* `closed`, surfaced by the pane as an explanatory message. */
|
|
156
|
+
export interface BrowserStateFrame {
|
|
157
|
+
t: 'browser-state';
|
|
158
|
+
sessionId: string;
|
|
159
|
+
state: BrowserSessionState;
|
|
160
|
+
url?: string;
|
|
161
|
+
title?: string;
|
|
162
|
+
reason?: string;
|
|
163
|
+
}
|
|
164
|
+
/** pane -> container frames. */
|
|
165
|
+
export type BrowserPaneToContainerFrame = BrowserOpenFrame | BrowserInputFrame | BrowserFrameAckFrame | BrowserSleepFrame;
|
|
166
|
+
/** container -> pane frames. */
|
|
167
|
+
export type BrowserContainerToPaneFrame = BrowserScreenFrame | BrowserStateFrame;
|
|
168
|
+
/** The full v1 cloud browser session frame set, either direction. */
|
|
169
|
+
export type BrowserFrame = BrowserPaneToContainerFrame | BrowserContainerToPaneFrame;
|
|
170
|
+
export declare function isBrowserOpenFrame(v: unknown): v is BrowserOpenFrame;
|
|
171
|
+
export declare function isBrowserInputEvent(v: unknown): v is BrowserInputEvent;
|
|
172
|
+
export declare function isBrowserInputFrame(v: unknown): v is BrowserInputFrame;
|
|
173
|
+
/** cbs-06 mirror of arena's cbs-05 addition — see the module doc block's
|
|
174
|
+
* "cbs-06 additions" section. */
|
|
175
|
+
export declare function isBrowserFrameAckFrame(v: unknown): v is BrowserFrameAckFrame;
|
|
176
|
+
/** cbs-06 mirror of arena's cbs-05 addition — see the module doc block's
|
|
177
|
+
* "cbs-06 additions" section. */
|
|
178
|
+
export declare function isBrowserSleepFrame(v: unknown): v is BrowserSleepFrame;
|
|
179
|
+
export declare function isBrowserScreenFrame(v: unknown): v is BrowserScreenFrame;
|
|
180
|
+
export declare function isBrowserStateFrame(v: unknown): v is BrowserStateFrame;
|
|
181
|
+
/** True for any v1 cloud browser session frame, either direction. */
|
|
182
|
+
export declare function isBrowserFrame(v: unknown): v is BrowserFrame;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* cbs-01 — Cloud Browser Session frame protocol, v1.
|
|
4
|
+
*
|
|
5
|
+
* (docs/prd/cloud-browser-session.md, ADR TBD) A headless Chromium runs
|
|
6
|
+
* inside the user's per-user container, driven over CDP by the bridge, and
|
|
7
|
+
* streamed to the arena Browser pane over the SAME terminal relay socket as
|
|
8
|
+
* PTY frames (no parallel transport) — new non-PTY frame types, modeled on
|
|
9
|
+
* the existing `install-bundle` / `context-manifest` precedent in
|
|
10
|
+
* relayClient.ts (see PROTOCOL.md). This module defines ONLY the wire
|
|
11
|
+
* shapes + runtime type guards; it is deliberately NOT wired into
|
|
12
|
+
* relayClient.ts in this slice (no relay/backend code is touched by cbs-01).
|
|
13
|
+
*
|
|
14
|
+
* ── Envelope discipline (matches ClientFrame in relayClient.ts) ──────────
|
|
15
|
+
* - Protocol version: v1 (BROWSER_FRAME_PROTOCOL_VERSION below). Frames
|
|
16
|
+
* do not carry a per-frame `v` — like the existing terminal frames, the
|
|
17
|
+
* version is asserted once at connection handshake time, not per frame.
|
|
18
|
+
* - Text-only JSON envelope. Every frame is `{ t, sessionId, ...fields }`.
|
|
19
|
+
* Binary payloads (the JPEG screencast frame) ride as base64 `data`
|
|
20
|
+
* inside the JSON, exactly like PTY `data` frames already do.
|
|
21
|
+
* - Session identity: `browser:<containerId>` (mirrors the `login:` prefix
|
|
22
|
+
* precedent) — out of scope for this slice, but every frame here already
|
|
23
|
+
* carries `sessionId` so routing needs no protocol change to adopt it.
|
|
24
|
+
*
|
|
25
|
+
* ── Frame directions ──────────────────────────────────────────────────────
|
|
26
|
+
* pane -> container:
|
|
27
|
+
* `browser-open` { t, sessionId, url }
|
|
28
|
+
* `browser-input` { t, sessionId, event } — event is a discriminated
|
|
29
|
+
* union (mouse | key | insertText | wheel)
|
|
30
|
+
* container -> pane:
|
|
31
|
+
* `browser-frame` { t, sessionId, seq, data, w, h } — data is base64 JPEG
|
|
32
|
+
* `browser-state` { t, sessionId, state, url?, title?, reason? }
|
|
33
|
+
*
|
|
34
|
+
* ── Drop rule (the whole point of framePacer.ts) ──────────────────────────
|
|
35
|
+
* FRAMES are droppable — `browser-frame` is a keep-newest video stream; a
|
|
36
|
+
* slow consumer sees the newest frame, never an unbounded backlog.
|
|
37
|
+
* INPUTS are never dropped — `browser-input` is small and ORDER-SENSITIVE
|
|
38
|
+
* (a mouse-down must reach the page before its mouse-up); it bypasses the
|
|
39
|
+
* frame pacer entirely and is expected to ride the relay's existing
|
|
40
|
+
* ordered-delivery guarantee, same as PTY `input` frames today.
|
|
41
|
+
*
|
|
42
|
+
* ── cbs-06 additions (ack + sleep) ─────────────────────────────────────────
|
|
43
|
+
* Arena's Browser pane (cbs-05, landed on arena master) needed two shapes
|
|
44
|
+
* this file left undefined. This CLI file is the hand-maintained mirror
|
|
45
|
+
* (see the MIRROR NOTICE at the top of this block) — cbs-06 lands the
|
|
46
|
+
* matching addition here so the two repos' shape-pinning tests stay in
|
|
47
|
+
* sync:
|
|
48
|
+
* `browser-frame-ack` { t, sessionId, seq } — pane -> container. The
|
|
49
|
+
* other half of the keep-newest pacing contract: cbs-06 wires this
|
|
50
|
+
* into the bridge's CDP screencast ack gate (framePacer.ts via
|
|
51
|
+
* browserDriver.ackFrame()), replacing the interim cbs-02 policy of
|
|
52
|
+
* self-acking on send. The pane must ack every `browser-frame` it
|
|
53
|
+
* actually PAINTED (never batched, never sent for a frame that was
|
|
54
|
+
* superseded before it rendered).
|
|
55
|
+
* `browser-sleep` { t, sessionId } — pane -> container. The pane's
|
|
56
|
+
* explicit 💤 control (mirrors the terminal pane's manual-sleep
|
|
57
|
+
* affordance) — minimal, no payload beyond identity, same effect as
|
|
58
|
+
* the bridge's 15-minute idle auto-close (browserIdle.ts) but
|
|
59
|
+
* user-triggered.
|
|
60
|
+
*/
|
|
61
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
62
|
+
exports.BROWSER_FRAME_PROTOCOL_VERSION = void 0;
|
|
63
|
+
exports.isBrowserOpenFrame = isBrowserOpenFrame;
|
|
64
|
+
exports.isBrowserInputEvent = isBrowserInputEvent;
|
|
65
|
+
exports.isBrowserInputFrame = isBrowserInputFrame;
|
|
66
|
+
exports.isBrowserFrameAckFrame = isBrowserFrameAckFrame;
|
|
67
|
+
exports.isBrowserSleepFrame = isBrowserSleepFrame;
|
|
68
|
+
exports.isBrowserScreenFrame = isBrowserScreenFrame;
|
|
69
|
+
exports.isBrowserStateFrame = isBrowserStateFrame;
|
|
70
|
+
exports.isBrowserFrame = isBrowserFrame;
|
|
71
|
+
/** Protocol version for the cloud browser session frame set. */
|
|
72
|
+
exports.BROWSER_FRAME_PROTOCOL_VERSION = 1;
|
|
73
|
+
// ── Type guards ─────────────────────────────────────────────────────────
|
|
74
|
+
function isRecord(v) {
|
|
75
|
+
return typeof v === 'object' && v !== null;
|
|
76
|
+
}
|
|
77
|
+
function isNonEmptyString(v) {
|
|
78
|
+
return typeof v === 'string' && v.length > 0;
|
|
79
|
+
}
|
|
80
|
+
function isModifiers(v) {
|
|
81
|
+
if (v === undefined)
|
|
82
|
+
return true;
|
|
83
|
+
if (!isRecord(v))
|
|
84
|
+
return false;
|
|
85
|
+
for (const key of ['shift', 'ctrl', 'alt', 'meta']) {
|
|
86
|
+
const val = v[key];
|
|
87
|
+
if (val !== undefined && typeof val !== 'boolean')
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
function isBrowserOpenFrame(v) {
|
|
93
|
+
if (!isRecord(v))
|
|
94
|
+
return false;
|
|
95
|
+
return v.t === 'browser-open' && isNonEmptyString(v.sessionId) && typeof v.url === 'string';
|
|
96
|
+
}
|
|
97
|
+
function isBrowserInputEvent(v) {
|
|
98
|
+
if (!isRecord(v))
|
|
99
|
+
return false;
|
|
100
|
+
switch (v.kind) {
|
|
101
|
+
case 'mouse':
|
|
102
|
+
return ((v.type === 'down' || v.type === 'up' || v.type === 'move') &&
|
|
103
|
+
typeof v.x === 'number' &&
|
|
104
|
+
typeof v.y === 'number' &&
|
|
105
|
+
(v.button === undefined || v.button === 'left' || v.button === 'middle' || v.button === 'right') &&
|
|
106
|
+
isModifiers(v.modifiers));
|
|
107
|
+
case 'key':
|
|
108
|
+
return ((v.type === 'down' || v.type === 'up') &&
|
|
109
|
+
typeof v.key === 'string' &&
|
|
110
|
+
typeof v.code === 'string' &&
|
|
111
|
+
isModifiers(v.modifiers));
|
|
112
|
+
case 'insertText':
|
|
113
|
+
return typeof v.text === 'string';
|
|
114
|
+
case 'wheel':
|
|
115
|
+
return (typeof v.x === 'number' &&
|
|
116
|
+
typeof v.y === 'number' &&
|
|
117
|
+
typeof v.deltaX === 'number' &&
|
|
118
|
+
typeof v.deltaY === 'number');
|
|
119
|
+
default:
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function isBrowserInputFrame(v) {
|
|
124
|
+
if (!isRecord(v))
|
|
125
|
+
return false;
|
|
126
|
+
return v.t === 'browser-input' && isNonEmptyString(v.sessionId) && isBrowserInputEvent(v.event);
|
|
127
|
+
}
|
|
128
|
+
/** cbs-06 mirror of arena's cbs-05 addition — see the module doc block's
|
|
129
|
+
* "cbs-06 additions" section. */
|
|
130
|
+
function isBrowserFrameAckFrame(v) {
|
|
131
|
+
if (!isRecord(v))
|
|
132
|
+
return false;
|
|
133
|
+
return v.t === 'browser-frame-ack' && isNonEmptyString(v.sessionId) && typeof v.seq === 'number' && v.seq >= 0;
|
|
134
|
+
}
|
|
135
|
+
/** cbs-06 mirror of arena's cbs-05 addition — see the module doc block's
|
|
136
|
+
* "cbs-06 additions" section. */
|
|
137
|
+
function isBrowserSleepFrame(v) {
|
|
138
|
+
if (!isRecord(v))
|
|
139
|
+
return false;
|
|
140
|
+
return v.t === 'browser-sleep' && isNonEmptyString(v.sessionId);
|
|
141
|
+
}
|
|
142
|
+
function isBrowserScreenFrame(v) {
|
|
143
|
+
if (!isRecord(v))
|
|
144
|
+
return false;
|
|
145
|
+
return (v.t === 'browser-frame' &&
|
|
146
|
+
isNonEmptyString(v.sessionId) &&
|
|
147
|
+
typeof v.seq === 'number' &&
|
|
148
|
+
v.seq >= 0 &&
|
|
149
|
+
typeof v.data === 'string' &&
|
|
150
|
+
typeof v.w === 'number' &&
|
|
151
|
+
v.w > 0 &&
|
|
152
|
+
typeof v.h === 'number' &&
|
|
153
|
+
v.h > 0);
|
|
154
|
+
}
|
|
155
|
+
const BROWSER_SESSION_STATES = ['opening', 'open', 'asleep', 'closed'];
|
|
156
|
+
function isBrowserStateFrame(v) {
|
|
157
|
+
if (!isRecord(v))
|
|
158
|
+
return false;
|
|
159
|
+
if (v.t !== 'browser-state' || !isNonEmptyString(v.sessionId))
|
|
160
|
+
return false;
|
|
161
|
+
if (typeof v.state !== 'string' || !BROWSER_SESSION_STATES.includes(v.state))
|
|
162
|
+
return false;
|
|
163
|
+
if (v.url !== undefined && typeof v.url !== 'string')
|
|
164
|
+
return false;
|
|
165
|
+
if (v.title !== undefined && typeof v.title !== 'string')
|
|
166
|
+
return false;
|
|
167
|
+
if (v.reason !== undefined && typeof v.reason !== 'string')
|
|
168
|
+
return false;
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
/** True for any v1 cloud browser session frame, either direction. */
|
|
172
|
+
function isBrowserFrame(v) {
|
|
173
|
+
return (isBrowserOpenFrame(v) ||
|
|
174
|
+
isBrowserInputFrame(v) ||
|
|
175
|
+
isBrowserFrameAckFrame(v) ||
|
|
176
|
+
isBrowserSleepFrame(v) ||
|
|
177
|
+
isBrowserScreenFrame(v) ||
|
|
178
|
+
isBrowserStateFrame(v));
|
|
179
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cbs-06 — cloud browser session idle timer.
|
|
3
|
+
*
|
|
4
|
+
* (docs/prd/cloud-browser-session.md, issues/cbs-06-idle-lifecycle.md)
|
|
5
|
+
* Lifecycle parity with agents, bridge-side: a browser session with no
|
|
6
|
+
* input AND no viewer for `thresholdMs` (default 15 minutes) auto-closes.
|
|
7
|
+
* Two liveness signals, tracked independently — INPUT (`browser-input`
|
|
8
|
+
* frames) and ACK (`browser-frame-ack` frames, a proxy for "someone is
|
|
9
|
+
* actually looking at the stream", since a displayed frame implies a live
|
|
10
|
+
* viewer) — mirroring the container-level idle convention exactly
|
|
11
|
+
* (container-lifecycle.service.ts: "Idle is (a) AND (b) both true; either
|
|
12
|
+
* signal alone is enough to stay awake"). This module owns no Chromium/CDP
|
|
13
|
+
* knowledge; it only decides WHEN to call `onIdle()` — the caller
|
|
14
|
+
* (relayClient.ts) owns what that means (driver.stop() + browser-state
|
|
15
|
+
* asleep).
|
|
16
|
+
*
|
|
17
|
+
* ── Suspend-safety (the crux) ─────────────────────────────────────────────
|
|
18
|
+
* The container is RAM-suspended when idle (twr-13/ADR-0034); suspend and
|
|
19
|
+
* resume must be transparent, but a naive wall-clock idle check is NOT
|
|
20
|
+
* transparent — `Date.now() - lastInputAt` after a multi-hour suspend
|
|
21
|
+
* reflects the FULL suspend duration (wall clock keeps ticking / gets
|
|
22
|
+
* corrected on resume) even though nothing "idle" actually happened; the
|
|
23
|
+
* browser itself was frozen, not neglected. A false fire here would close a
|
|
24
|
+
* session the instant it woke up.
|
|
25
|
+
*
|
|
26
|
+
* The fix: every periodic check compares the WALL-clock delta since the
|
|
27
|
+
* last check (`nowFn`) against the MONOTONIC delta since the last check
|
|
28
|
+
* (`monotonicNowFn`, defaults to `process.hrtime`). A RAM-snapshot suspend
|
|
29
|
+
* freezes the guest's monotonic clock (no CPU cycles execute) but the wall
|
|
30
|
+
* clock jumps forward on resume — so `wallDelta` far exceeding `monoDelta`
|
|
31
|
+
* (beyond `suspendGapToleranceMs`, a generous allowance for ordinary
|
|
32
|
+
* event-loop/GC scheduling jitter) is the suspend's signature. When
|
|
33
|
+
* detected, the tick is NOT evaluated for idleness at all — both baselines
|
|
34
|
+
* reset to "now", exactly as if the user had just interacted, giving the
|
|
35
|
+
* resumed session a full fresh window rather than firing immediately.
|
|
36
|
+
*
|
|
37
|
+
* Pure-ish: no CDP/process access, only injectable clocks + an injectable
|
|
38
|
+
* interval scheduler (same injection pattern as relayClient's login poll /
|
|
39
|
+
* activity throttle) so tests never wait on a real clock.
|
|
40
|
+
*/
|
|
41
|
+
/** PRD: "15-minute no-input/no-viewer timer". */
|
|
42
|
+
export declare const DEFAULT_BROWSER_IDLE_THRESHOLD_MS: number;
|
|
43
|
+
export interface BrowserIdleOpts {
|
|
44
|
+
/** Called once both lastInputAt and lastAckAt are stale past the
|
|
45
|
+
* threshold. Fires at most once per tracker (latched). */
|
|
46
|
+
onIdle: () => void;
|
|
47
|
+
/** Idle threshold in ms, applied to BOTH signals. Default 15 minutes
|
|
48
|
+
* (DEFAULT_BROWSER_IDLE_THRESHOLD_MS). */
|
|
49
|
+
thresholdMs?: number;
|
|
50
|
+
/** How often to re-evaluate. Default 30_000ms. */
|
|
51
|
+
checkIntervalMs?: number;
|
|
52
|
+
/** Suspend-gap detection tolerance — see the module doc block. Default
|
|
53
|
+
* 5_000ms. */
|
|
54
|
+
suspendGapToleranceMs?: number;
|
|
55
|
+
/** Injectable wall clock. Default Date.now. */
|
|
56
|
+
nowFn?: () => number;
|
|
57
|
+
/** Injectable monotonic clock (ms). Default process.hrtime-based. */
|
|
58
|
+
monotonicNowFn?: () => number;
|
|
59
|
+
/** Injectable interval timer so tests are deterministic. Defaults to
|
|
60
|
+
* setInterval. */
|
|
61
|
+
setIntervalFn?: (cb: () => void, ms: number) => unknown;
|
|
62
|
+
clearIntervalFn?: (handle: unknown) => void;
|
|
63
|
+
}
|
|
64
|
+
export interface BrowserIdleTracker {
|
|
65
|
+
/** Bump the input liveness signal to "now". */
|
|
66
|
+
recordInput(): void;
|
|
67
|
+
/** Bump the ack (viewer proxy) liveness signal to "now". */
|
|
68
|
+
recordAck(): void;
|
|
69
|
+
/** Begin periodic evaluation. Idempotent — a second call is a no-op. */
|
|
70
|
+
start(): void;
|
|
71
|
+
/** Stop periodic evaluation (cancels the interval). Safe to call more
|
|
72
|
+
* than once and safe to call before start(). */
|
|
73
|
+
stop(): void;
|
|
74
|
+
}
|
|
75
|
+
export declare function createBrowserIdleTracker(opts: BrowserIdleOpts): BrowserIdleTracker;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* cbs-06 — cloud browser session idle timer.
|
|
4
|
+
*
|
|
5
|
+
* (docs/prd/cloud-browser-session.md, issues/cbs-06-idle-lifecycle.md)
|
|
6
|
+
* Lifecycle parity with agents, bridge-side: a browser session with no
|
|
7
|
+
* input AND no viewer for `thresholdMs` (default 15 minutes) auto-closes.
|
|
8
|
+
* Two liveness signals, tracked independently — INPUT (`browser-input`
|
|
9
|
+
* frames) and ACK (`browser-frame-ack` frames, a proxy for "someone is
|
|
10
|
+
* actually looking at the stream", since a displayed frame implies a live
|
|
11
|
+
* viewer) — mirroring the container-level idle convention exactly
|
|
12
|
+
* (container-lifecycle.service.ts: "Idle is (a) AND (b) both true; either
|
|
13
|
+
* signal alone is enough to stay awake"). This module owns no Chromium/CDP
|
|
14
|
+
* knowledge; it only decides WHEN to call `onIdle()` — the caller
|
|
15
|
+
* (relayClient.ts) owns what that means (driver.stop() + browser-state
|
|
16
|
+
* asleep).
|
|
17
|
+
*
|
|
18
|
+
* ── Suspend-safety (the crux) ─────────────────────────────────────────────
|
|
19
|
+
* The container is RAM-suspended when idle (twr-13/ADR-0034); suspend and
|
|
20
|
+
* resume must be transparent, but a naive wall-clock idle check is NOT
|
|
21
|
+
* transparent — `Date.now() - lastInputAt` after a multi-hour suspend
|
|
22
|
+
* reflects the FULL suspend duration (wall clock keeps ticking / gets
|
|
23
|
+
* corrected on resume) even though nothing "idle" actually happened; the
|
|
24
|
+
* browser itself was frozen, not neglected. A false fire here would close a
|
|
25
|
+
* session the instant it woke up.
|
|
26
|
+
*
|
|
27
|
+
* The fix: every periodic check compares the WALL-clock delta since the
|
|
28
|
+
* last check (`nowFn`) against the MONOTONIC delta since the last check
|
|
29
|
+
* (`monotonicNowFn`, defaults to `process.hrtime`). A RAM-snapshot suspend
|
|
30
|
+
* freezes the guest's monotonic clock (no CPU cycles execute) but the wall
|
|
31
|
+
* clock jumps forward on resume — so `wallDelta` far exceeding `monoDelta`
|
|
32
|
+
* (beyond `suspendGapToleranceMs`, a generous allowance for ordinary
|
|
33
|
+
* event-loop/GC scheduling jitter) is the suspend's signature. When
|
|
34
|
+
* detected, the tick is NOT evaluated for idleness at all — both baselines
|
|
35
|
+
* reset to "now", exactly as if the user had just interacted, giving the
|
|
36
|
+
* resumed session a full fresh window rather than firing immediately.
|
|
37
|
+
*
|
|
38
|
+
* Pure-ish: no CDP/process access, only injectable clocks + an injectable
|
|
39
|
+
* interval scheduler (same injection pattern as relayClient's login poll /
|
|
40
|
+
* activity throttle) so tests never wait on a real clock.
|
|
41
|
+
*/
|
|
42
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
|
+
exports.DEFAULT_BROWSER_IDLE_THRESHOLD_MS = void 0;
|
|
44
|
+
exports.createBrowserIdleTracker = createBrowserIdleTracker;
|
|
45
|
+
/** PRD: "15-minute no-input/no-viewer timer". */
|
|
46
|
+
exports.DEFAULT_BROWSER_IDLE_THRESHOLD_MS = 15 * 60_000;
|
|
47
|
+
/** How often the idle condition is (re-)evaluated. Cheap pure comparisons —
|
|
48
|
+
* 30s gives quick-enough response without meaningful overhead. */
|
|
49
|
+
const DEFAULT_CHECK_INTERVAL_MS = 30_000;
|
|
50
|
+
/** Generous allowance for ordinary scheduling jitter (GC pauses, event-loop
|
|
51
|
+
* backlog) between the wall and monotonic deltas of a single check
|
|
52
|
+
* interval — far below any real suspend gap (minutes to hours), so it
|
|
53
|
+
* never masks a genuine one while never false-triggering on jitter. */
|
|
54
|
+
const DEFAULT_SUSPEND_GAP_TOLERANCE_MS = 5_000;
|
|
55
|
+
function defaultMonotonicNowMs() {
|
|
56
|
+
return Number(process.hrtime.bigint() / 1000000n);
|
|
57
|
+
}
|
|
58
|
+
function createBrowserIdleTracker(opts) {
|
|
59
|
+
const thresholdMs = opts.thresholdMs ?? exports.DEFAULT_BROWSER_IDLE_THRESHOLD_MS;
|
|
60
|
+
const checkIntervalMs = opts.checkIntervalMs ?? DEFAULT_CHECK_INTERVAL_MS;
|
|
61
|
+
const suspendGapToleranceMs = opts.suspendGapToleranceMs ?? DEFAULT_SUSPEND_GAP_TOLERANCE_MS;
|
|
62
|
+
const now = opts.nowFn ?? Date.now;
|
|
63
|
+
const monotonicNow = opts.monotonicNowFn ?? defaultMonotonicNowMs;
|
|
64
|
+
const scheduleInterval = opts.setIntervalFn ?? ((cb, ms) => setInterval(cb, ms));
|
|
65
|
+
const cancelInterval = opts.clearIntervalFn ?? ((h) => clearInterval(h));
|
|
66
|
+
let lastInputAt = now();
|
|
67
|
+
let lastAckAt = now();
|
|
68
|
+
let lastCheckWall = now();
|
|
69
|
+
let lastCheckMono = monotonicNow();
|
|
70
|
+
let handle = null;
|
|
71
|
+
let fired = false;
|
|
72
|
+
function resetBaseline(atWall) {
|
|
73
|
+
lastInputAt = atWall;
|
|
74
|
+
lastAckAt = atWall;
|
|
75
|
+
}
|
|
76
|
+
function check() {
|
|
77
|
+
if (fired)
|
|
78
|
+
return;
|
|
79
|
+
const wallNow = now();
|
|
80
|
+
const monoNow = monotonicNow();
|
|
81
|
+
const wallDelta = wallNow - lastCheckWall;
|
|
82
|
+
const monoDelta = monoNow - lastCheckMono;
|
|
83
|
+
lastCheckWall = wallNow;
|
|
84
|
+
lastCheckMono = monoNow;
|
|
85
|
+
if (wallDelta - monoDelta > suspendGapToleranceMs) {
|
|
86
|
+
// Suspend gap: the wall clock advanced far more than real (monotonic)
|
|
87
|
+
// time did. Not idle time — reset the baseline instead of evaluating.
|
|
88
|
+
resetBaseline(wallNow);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const inputIdleMs = wallNow - lastInputAt;
|
|
92
|
+
const ackIdleMs = wallNow - lastAckAt;
|
|
93
|
+
if (inputIdleMs >= thresholdMs && ackIdleMs >= thresholdMs) {
|
|
94
|
+
fired = true;
|
|
95
|
+
opts.onIdle();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
recordInput() {
|
|
100
|
+
lastInputAt = now();
|
|
101
|
+
},
|
|
102
|
+
recordAck() {
|
|
103
|
+
lastAckAt = now();
|
|
104
|
+
},
|
|
105
|
+
start() {
|
|
106
|
+
if (handle !== null)
|
|
107
|
+
return;
|
|
108
|
+
lastCheckWall = now();
|
|
109
|
+
lastCheckMono = monotonicNow();
|
|
110
|
+
handle = scheduleInterval(check, checkIntervalMs);
|
|
111
|
+
handle?.unref?.();
|
|
112
|
+
},
|
|
113
|
+
stop() {
|
|
114
|
+
if (handle === null)
|
|
115
|
+
return;
|
|
116
|
+
cancelInterval(handle);
|
|
117
|
+
handle = null;
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type FileMap = Record<string, string>;
|
|
2
|
+
export interface BundleInstallerDeps {
|
|
3
|
+
/** Root holding every workspace's checkout, e.g. `/data/repos`. */
|
|
4
|
+
reposBaseDir: string;
|
|
5
|
+
/** Create a directory (recursively). */
|
|
6
|
+
mkdirp: (dir: string) => void;
|
|
7
|
+
/** Write (overwrite) a file. */
|
|
8
|
+
writeFile: (filePath: string, content: string) => void;
|
|
9
|
+
}
|
|
10
|
+
export type SkipReason = 'unsafe' | 'unmanaged';
|
|
11
|
+
export interface InstallBundleResult {
|
|
12
|
+
/** The absolute workspace checkout root, or null if the workspaceId itself was unsafe. */
|
|
13
|
+
workspaceRoot: string | null;
|
|
14
|
+
/** Managed paths (relative) that were written. */
|
|
15
|
+
written: string[];
|
|
16
|
+
/** Entries refused, with why. */
|
|
17
|
+
skipped: Array<{
|
|
18
|
+
path: string;
|
|
19
|
+
reason: SkipReason;
|
|
20
|
+
}>;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* A workspaceId is the sole dir segment under reposBaseDir — it must be a plain
|
|
24
|
+
* id, never a path fragment. Reject anything that could climb out of / redirect
|
|
25
|
+
* the base dir before we ever join it.
|
|
26
|
+
*/
|
|
27
|
+
export declare function isSafeWorkspaceId(workspaceId: string): boolean;
|
|
28
|
+
export declare function installBundle(bundle: {
|
|
29
|
+
workspaceId: string;
|
|
30
|
+
files: FileMap;
|
|
31
|
+
}, deps: BundleInstallerDeps): InstallBundleResult;
|