@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.
Files changed (116) hide show
  1. package/dist/__tests__/bridge-activity-parser.test.d.ts +1 -0
  2. package/dist/__tests__/bridge-activity-parser.test.js +0 -0
  3. package/dist/__tests__/bridge-activity-tap.test.d.ts +1 -0
  4. package/dist/__tests__/bridge-activity-tap.test.js +178 -0
  5. package/dist/__tests__/bridge-api-client.test.d.ts +1 -0
  6. package/dist/__tests__/bridge-api-client.test.js +69 -0
  7. package/dist/__tests__/bridge-browser-driver.test.d.ts +1 -0
  8. package/dist/__tests__/bridge-browser-driver.test.js +379 -0
  9. package/dist/__tests__/bridge-browser-frames.test.d.ts +1 -0
  10. package/dist/__tests__/bridge-browser-frames.test.js +166 -0
  11. package/dist/__tests__/bridge-browser-idle.test.d.ts +1 -0
  12. package/dist/__tests__/bridge-browser-idle.test.js +259 -0
  13. package/dist/__tests__/bridge-browser-relay.test.d.ts +1 -0
  14. package/dist/__tests__/bridge-browser-relay.test.js +670 -0
  15. package/dist/__tests__/bridge-bundle-installer.test.d.ts +1 -0
  16. package/dist/__tests__/bridge-bundle-installer.test.js +126 -0
  17. package/dist/__tests__/bridge-cdp-client.test.d.ts +1 -0
  18. package/dist/__tests__/bridge-cdp-client.test.js +133 -0
  19. package/dist/__tests__/bridge-chromium-launcher.test.d.ts +1 -0
  20. package/dist/__tests__/bridge-chromium-launcher.test.js +202 -0
  21. package/dist/__tests__/bridge-command-browser-options.test.d.ts +1 -0
  22. package/dist/__tests__/bridge-command-browser-options.test.js +37 -0
  23. package/dist/__tests__/bridge-config-mcp.test.js +4 -0
  24. package/dist/__tests__/bridge-context-manifest-frame.test.d.ts +1 -0
  25. package/dist/__tests__/bridge-context-manifest-frame.test.js +125 -0
  26. package/dist/__tests__/bridge-context-writer.test.d.ts +1 -0
  27. package/dist/__tests__/bridge-context-writer.test.js +161 -0
  28. package/dist/__tests__/bridge-device-registration.test.d.ts +1 -0
  29. package/dist/__tests__/bridge-device-registration.test.js +33 -0
  30. package/dist/__tests__/bridge-folder-picker-runner.test.d.ts +1 -0
  31. package/dist/__tests__/bridge-folder-picker-runner.test.js +37 -0
  32. package/dist/__tests__/bridge-folder-picker.test.d.ts +1 -0
  33. package/dist/__tests__/bridge-folder-picker.test.js +66 -0
  34. package/dist/__tests__/bridge-folder-registry.test.d.ts +1 -0
  35. package/dist/__tests__/bridge-folder-registry.test.js +107 -0
  36. package/dist/__tests__/bridge-frame-pacer.test.d.ts +1 -0
  37. package/dist/__tests__/bridge-frame-pacer.test.js +102 -0
  38. package/dist/__tests__/bridge-install-bundle-frame.test.d.ts +1 -0
  39. package/dist/__tests__/bridge-install-bundle-frame.test.js +107 -0
  40. package/dist/__tests__/bridge-local-add-core.test.d.ts +1 -0
  41. package/dist/__tests__/bridge-local-add-core.test.js +48 -0
  42. package/dist/__tests__/bridge-local-path-validation-fs.test.d.ts +1 -0
  43. package/dist/__tests__/bridge-local-path-validation-fs.test.js +88 -0
  44. package/dist/__tests__/bridge-local-path-validation.test.d.ts +1 -0
  45. package/dist/__tests__/bridge-local-path-validation.test.js +45 -0
  46. package/dist/__tests__/bridge-login-status.test.d.ts +1 -0
  47. package/dist/__tests__/bridge-login-status.test.js +60 -0
  48. package/dist/__tests__/bridge-relay-client.test.d.ts +1 -0
  49. package/dist/__tests__/bridge-relay-client.test.js +283 -0
  50. package/dist/__tests__/bridge-relay-mode.test.d.ts +1 -0
  51. package/dist/__tests__/bridge-relay-mode.test.js +33 -0
  52. package/dist/__tests__/bridge-relay-pucc14.test.d.ts +1 -0
  53. package/dist/__tests__/bridge-relay-pucc14.test.js +230 -0
  54. package/dist/__tests__/bridge-server-local-endpoints.test.d.ts +1 -0
  55. package/dist/__tests__/bridge-server-local-endpoints.test.js +197 -0
  56. package/dist/__tests__/bridge-server.test.js +22 -0
  57. package/dist/__tests__/bridge-workspace-multirepo.test.d.ts +1 -0
  58. package/dist/__tests__/bridge-workspace-multirepo.test.js +317 -0
  59. package/dist/__tests__/devices.test.d.ts +1 -0
  60. package/dist/__tests__/devices.test.js +66 -0
  61. package/dist/__tests__/local.test.d.ts +1 -0
  62. package/dist/__tests__/local.test.js +147 -0
  63. package/dist/bridge/activityParser.d.ts +53 -0
  64. package/dist/bridge/activityParser.js +202 -0
  65. package/dist/bridge/apiClient.d.ts +56 -0
  66. package/dist/bridge/apiClient.js +50 -0
  67. package/dist/bridge/browserDriver.d.ts +98 -0
  68. package/dist/bridge/browserDriver.js +265 -0
  69. package/dist/bridge/browserFrames.d.ts +182 -0
  70. package/dist/bridge/browserFrames.js +179 -0
  71. package/dist/bridge/browserIdle.d.ts +75 -0
  72. package/dist/bridge/browserIdle.js +120 -0
  73. package/dist/bridge/bundleInstaller.d.ts +31 -0
  74. package/dist/bridge/bundleInstaller.js +127 -0
  75. package/dist/bridge/cdpClient.d.ts +47 -0
  76. package/dist/bridge/cdpClient.js +102 -0
  77. package/dist/bridge/chromiumLauncher.d.ts +45 -0
  78. package/dist/bridge/chromiumLauncher.js +113 -0
  79. package/dist/bridge/config.d.ts +3 -0
  80. package/dist/bridge/contextWriter.d.ts +31 -0
  81. package/dist/bridge/contextWriter.js +139 -0
  82. package/dist/bridge/deviceRegistration.d.ts +28 -0
  83. package/dist/bridge/deviceRegistration.js +19 -0
  84. package/dist/bridge/folderPicker.d.ts +34 -0
  85. package/dist/bridge/folderPicker.js +36 -0
  86. package/dist/bridge/folderPickerRunner.d.ts +3 -0
  87. package/dist/bridge/folderPickerRunner.js +25 -0
  88. package/dist/bridge/folderRegistry.d.ts +19 -0
  89. package/dist/bridge/folderRegistry.js +100 -0
  90. package/dist/bridge/framePacer.d.ts +61 -0
  91. package/dist/bridge/framePacer.js +79 -0
  92. package/dist/bridge/localAdd.d.ts +28 -0
  93. package/dist/bridge/localAdd.js +67 -0
  94. package/dist/bridge/localPathValidation.d.ts +20 -0
  95. package/dist/bridge/localPathValidation.js +58 -0
  96. package/dist/bridge/localPathValidationFs.d.ts +2 -0
  97. package/dist/bridge/localPathValidationFs.js +98 -0
  98. package/dist/bridge/loginStatus.d.ts +38 -0
  99. package/dist/bridge/loginStatus.js +56 -0
  100. package/dist/bridge/realPty.js +6 -2
  101. package/dist/bridge/relayClient.d.ts +166 -0
  102. package/dist/bridge/relayClient.js +517 -0
  103. package/dist/bridge/repoCheckout.d.ts +31 -0
  104. package/dist/bridge/repoCheckout.js +18 -0
  105. package/dist/bridge/server.d.ts +33 -0
  106. package/dist/bridge/server.js +98 -10
  107. package/dist/bridge/sessionManager.d.ts +18 -0
  108. package/dist/bridge/sessionManager.js +15 -1
  109. package/dist/commands/bridge.d.ts +21 -0
  110. package/dist/commands/bridge.js +194 -1
  111. package/dist/commands/devices.d.ts +9 -0
  112. package/dist/commands/devices.js +53 -0
  113. package/dist/commands/local.d.ts +9 -0
  114. package/dist/commands/local.js +141 -0
  115. package/dist/index.js +7 -1
  116. 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;