local-browser-bridge 0.1.0
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/README.md +724 -0
- package/dist/package.json +61 -0
- package/dist/src/browser/chrome.d.ts +19 -0
- package/dist/src/browser/chrome.js +778 -0
- package/dist/src/browser/index.d.ts +3 -0
- package/dist/src/browser/index.js +25 -0
- package/dist/src/browser/safari.d.ts +41 -0
- package/dist/src/browser/safari.js +827 -0
- package/dist/src/browser-attach-ux-helper.d.ts +39 -0
- package/dist/src/browser-attach-ux-helper.js +157 -0
- package/dist/src/capabilities.d.ts +3 -0
- package/dist/src/capabilities.js +182 -0
- package/dist/src/chrome-relay-error-helper.d.ts +19 -0
- package/dist/src/chrome-relay-error-helper.js +78 -0
- package/dist/src/chrome-relay-helper-cli.d.ts +2 -0
- package/dist/src/chrome-relay-helper-cli.js +97 -0
- package/dist/src/chrome-relay-helper.d.ts +29 -0
- package/dist/src/chrome-relay-helper.js +151 -0
- package/dist/src/chrome-relay-state.d.ts +23 -0
- package/dist/src/chrome-relay-state.js +108 -0
- package/dist/src/claude-code.d.ts +20 -0
- package/dist/src/claude-code.js +66 -0
- package/dist/src/cli-reference-adapter.d.ts +13 -0
- package/dist/src/cli-reference-adapter.js +48 -0
- package/dist/src/cli.d.ts +3 -0
- package/dist/src/cli.js +200 -0
- package/dist/src/codex.d.ts +17 -0
- package/dist/src/codex.js +25 -0
- package/dist/src/connection-ux.d.ts +61 -0
- package/dist/src/connection-ux.js +256 -0
- package/dist/src/errors.d.ts +12 -0
- package/dist/src/errors.js +58 -0
- package/dist/src/http-reference-adapter.d.ts +34 -0
- package/dist/src/http-reference-adapter.js +61 -0
- package/dist/src/http.d.ts +3 -0
- package/dist/src/http.js +161 -0
- package/dist/src/index.d.ts +17 -0
- package/dist/src/index.js +43 -0
- package/dist/src/mcp-stdio.d.ts +2 -0
- package/dist/src/mcp-stdio.js +10 -0
- package/dist/src/mcp.d.ts +25 -0
- package/dist/src/mcp.js +483 -0
- package/dist/src/reference-adapter.d.ts +32 -0
- package/dist/src/reference-adapter.js +42 -0
- package/dist/src/service/attach-service.d.ts +28 -0
- package/dist/src/service/attach-service.js +272 -0
- package/dist/src/session-metadata.d.ts +4 -0
- package/dist/src/session-metadata.js +88 -0
- package/dist/src/store/session-store.d.ts +14 -0
- package/dist/src/store/session-store.js +52 -0
- package/dist/src/target.d.ts +9 -0
- package/dist/src/target.js +61 -0
- package/dist/src/types.d.ts +397 -0
- package/dist/src/types.js +2 -0
- package/dist/tests/attach-service.test.d.ts +1 -0
- package/dist/tests/attach-service.test.js +1367 -0
- package/dist/tests/browser-attach-ux-helper.test.d.ts +1 -0
- package/dist/tests/browser-attach-ux-helper.test.js +139 -0
- package/dist/tests/chrome-relay-error-helper.test.d.ts +1 -0
- package/dist/tests/chrome-relay-error-helper.test.js +67 -0
- package/dist/tests/chrome-relay-helper.test.d.ts +1 -0
- package/dist/tests/chrome-relay-helper.test.js +142 -0
- package/dist/tests/chrome-relay-state-schema.test.d.ts +1 -0
- package/dist/tests/chrome-relay-state-schema.test.js +96 -0
- package/dist/tests/claude-code-wrapper.test.d.ts +1 -0
- package/dist/tests/claude-code-wrapper.test.js +170 -0
- package/dist/tests/codex.test.d.ts +1 -0
- package/dist/tests/codex.test.js +210 -0
- package/dist/tests/demo-client-smoke.test.d.ts +1 -0
- package/dist/tests/demo-client-smoke.test.js +405 -0
- package/dist/tests/docs-fixtures.test.d.ts +1 -0
- package/dist/tests/docs-fixtures.test.js +255 -0
- package/dist/tests/doctor-connect-wrapper.test.d.ts +1 -0
- package/dist/tests/doctor-connect-wrapper.test.js +62 -0
- package/dist/tests/fixtures/doctor-connect-cli-stub.d.ts +1 -0
- package/dist/tests/fixtures/doctor-connect-cli-stub.js +93 -0
- package/dist/tests/fixtures/public-root-cli-stub.d.ts +210 -0
- package/dist/tests/fixtures/public-root-cli-stub.js +143 -0
- package/dist/tests/fixtures/public-root-consumer.js +67 -0
- package/dist/tests/mcp.test.d.ts +1 -0
- package/dist/tests/mcp.test.js +345 -0
- package/dist/tests/public-consumer-helpers.test.d.ts +1 -0
- package/dist/tests/public-consumer-helpers.test.js +33 -0
- package/dist/tests/public-package-git-consumption.test.d.ts +1 -0
- package/dist/tests/public-package-git-consumption.test.js +56 -0
- package/dist/tests/public-root-consumer-smoke.test.d.ts +1 -0
- package/dist/tests/public-root-consumer-smoke.test.js +214 -0
- package/dist/tests/reference-adapter.test.d.ts +1 -0
- package/dist/tests/reference-adapter.test.js +220 -0
- package/dist/tests/transport-reference-adapters.test.d.ts +1 -0
- package/dist/tests/transport-reference-adapters.test.js +214 -0
- package/package.json +61 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const strict_1 = __importDefault(require("node:assert/strict"));
|
|
7
|
+
const node_test_1 = __importDefault(require("node:test"));
|
|
8
|
+
const codex_1 = require("../src/codex");
|
|
9
|
+
const capabilities = {
|
|
10
|
+
schemaVersion: 1,
|
|
11
|
+
kind: "local-browser-bridge",
|
|
12
|
+
product: { name: "local-browser-bridge", version: "0.1.0" }
|
|
13
|
+
};
|
|
14
|
+
const diagnostics = {
|
|
15
|
+
browser: "chrome",
|
|
16
|
+
checkedAt: "2026-03-30T12:00:00.000Z",
|
|
17
|
+
runtime: {
|
|
18
|
+
platform: "darwin",
|
|
19
|
+
arch: "arm64",
|
|
20
|
+
nodeVersion: "v25.8.0",
|
|
21
|
+
safariRunning: true
|
|
22
|
+
},
|
|
23
|
+
host: {
|
|
24
|
+
osascriptAvailable: true,
|
|
25
|
+
screencaptureAvailable: true,
|
|
26
|
+
safariApplicationAvailable: true
|
|
27
|
+
},
|
|
28
|
+
supportedFeatures: {
|
|
29
|
+
inspectTabs: true,
|
|
30
|
+
attach: true,
|
|
31
|
+
activate: true,
|
|
32
|
+
navigate: true,
|
|
33
|
+
screenshot: true,
|
|
34
|
+
savedSessions: true,
|
|
35
|
+
cli: true,
|
|
36
|
+
httpApi: true
|
|
37
|
+
},
|
|
38
|
+
constraints: [],
|
|
39
|
+
attach: {
|
|
40
|
+
direct: {
|
|
41
|
+
mode: "direct",
|
|
42
|
+
source: "user-browser",
|
|
43
|
+
scope: "browser",
|
|
44
|
+
supported: true,
|
|
45
|
+
ready: false,
|
|
46
|
+
state: "attention-required",
|
|
47
|
+
blockers: []
|
|
48
|
+
},
|
|
49
|
+
relay: {
|
|
50
|
+
mode: "relay",
|
|
51
|
+
source: "extension-relay",
|
|
52
|
+
scope: "tab",
|
|
53
|
+
supported: true,
|
|
54
|
+
ready: true,
|
|
55
|
+
state: "ready",
|
|
56
|
+
blockers: []
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
const session = {
|
|
61
|
+
schemaVersion: 1,
|
|
62
|
+
id: "sess-codex-relay",
|
|
63
|
+
kind: "chrome-readonly",
|
|
64
|
+
browser: "chrome",
|
|
65
|
+
target: { type: "front" },
|
|
66
|
+
tab: {
|
|
67
|
+
browser: "chrome",
|
|
68
|
+
windowIndex: 1,
|
|
69
|
+
tabIndex: 1,
|
|
70
|
+
title: "Shared tab",
|
|
71
|
+
url: "https://example.com",
|
|
72
|
+
attachedAt: "2026-03-30T12:00:00.000Z",
|
|
73
|
+
identity: {
|
|
74
|
+
signature: "sig",
|
|
75
|
+
urlKey: "https://example.com",
|
|
76
|
+
titleKey: "Shared tab",
|
|
77
|
+
origin: "https://example.com",
|
|
78
|
+
pathname: "/"
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
frontTab: {
|
|
82
|
+
browser: "chrome",
|
|
83
|
+
windowIndex: 1,
|
|
84
|
+
tabIndex: 1,
|
|
85
|
+
title: "Shared tab",
|
|
86
|
+
url: "https://example.com",
|
|
87
|
+
attachedAt: "2026-03-30T12:00:00.000Z",
|
|
88
|
+
identity: {
|
|
89
|
+
signature: "sig",
|
|
90
|
+
urlKey: "https://example.com",
|
|
91
|
+
titleKey: "Shared tab",
|
|
92
|
+
origin: "https://example.com",
|
|
93
|
+
pathname: "/"
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
attach: {
|
|
97
|
+
mode: "relay",
|
|
98
|
+
source: "extension-relay",
|
|
99
|
+
scope: "tab",
|
|
100
|
+
resumable: true
|
|
101
|
+
},
|
|
102
|
+
semantics: {
|
|
103
|
+
inspect: "shared-tab-only",
|
|
104
|
+
list: "saved-session",
|
|
105
|
+
resume: "current-shared-tab",
|
|
106
|
+
tabReference: {
|
|
107
|
+
windowIndex: "synthetic-shared-tab-position",
|
|
108
|
+
tabIndex: "synthetic-shared-tab-position"
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
capabilities: {
|
|
112
|
+
resume: true,
|
|
113
|
+
activate: false,
|
|
114
|
+
navigate: false,
|
|
115
|
+
screenshot: false
|
|
116
|
+
},
|
|
117
|
+
status: {
|
|
118
|
+
state: "read-only",
|
|
119
|
+
canAct: false
|
|
120
|
+
},
|
|
121
|
+
createdAt: "2026-03-30T12:00:00.000Z"
|
|
122
|
+
};
|
|
123
|
+
const resumedSession = {
|
|
124
|
+
session: {
|
|
125
|
+
...session,
|
|
126
|
+
id: "sess-codex-relay-resumed"
|
|
127
|
+
},
|
|
128
|
+
tab: session.tab,
|
|
129
|
+
resumedAt: "2026-03-30T12:05:00.000Z",
|
|
130
|
+
resolution: {
|
|
131
|
+
strategy: "front",
|
|
132
|
+
matched: true,
|
|
133
|
+
attachMode: "relay",
|
|
134
|
+
semantics: "current-shared-tab"
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
(0, node_test_1.default)("normalizeCodexRoute maps Codex route names into bridge routes", () => {
|
|
138
|
+
strict_1.default.deepEqual((0, codex_1.normalizeCodexRoute)("safari"), {
|
|
139
|
+
browser: "safari",
|
|
140
|
+
attachMode: "direct"
|
|
141
|
+
});
|
|
142
|
+
strict_1.default.deepEqual((0, codex_1.normalizeCodexRoute)("chrome-direct"), {
|
|
143
|
+
browser: "chrome",
|
|
144
|
+
attachMode: "direct"
|
|
145
|
+
});
|
|
146
|
+
strict_1.default.deepEqual((0, codex_1.normalizeCodexRoute)("chrome-relay", "sess-1"), {
|
|
147
|
+
browser: "chrome",
|
|
148
|
+
attachMode: "relay",
|
|
149
|
+
sessionId: "sess-1"
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
(0, node_test_1.default)("connectCodexViaHttp reuses the shared HTTP adapter and route flow", async () => {
|
|
153
|
+
const requests = [];
|
|
154
|
+
const connection = await (0, codex_1.connectCodexViaHttp)({
|
|
155
|
+
route: "chrome-relay",
|
|
156
|
+
async execute(request) {
|
|
157
|
+
requests.push(request);
|
|
158
|
+
if (request.method === "GET" && request.path === "/v1/capabilities") {
|
|
159
|
+
return { body: { capabilities } };
|
|
160
|
+
}
|
|
161
|
+
if (request.method === "GET" && request.path === "/v1/diagnostics?browser=chrome") {
|
|
162
|
+
return { body: { diagnostics } };
|
|
163
|
+
}
|
|
164
|
+
if (request.method === "POST" && request.path === "/v1/attach") {
|
|
165
|
+
return { body: { session } };
|
|
166
|
+
}
|
|
167
|
+
throw new Error(`Unexpected request: ${request.method} ${request.path}`);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
strict_1.default.equal(connection.operation, "attach");
|
|
171
|
+
strict_1.default.equal(connection.routeUx.label, "Chrome (shared tab, read-only)");
|
|
172
|
+
strict_1.default.equal(connection.session.id, "sess-codex-relay");
|
|
173
|
+
strict_1.default.deepEqual(requests, [
|
|
174
|
+
{ method: "GET", path: "/v1/capabilities" },
|
|
175
|
+
{ method: "GET", path: "/v1/diagnostics?browser=chrome" },
|
|
176
|
+
{
|
|
177
|
+
method: "POST",
|
|
178
|
+
path: "/v1/attach",
|
|
179
|
+
body: { browser: "chrome", attach: { mode: "relay" } }
|
|
180
|
+
}
|
|
181
|
+
]);
|
|
182
|
+
});
|
|
183
|
+
(0, node_test_1.default)("connectCodexViaCli reuses the shared CLI adapter and resume flow", async () => {
|
|
184
|
+
const commands = [];
|
|
185
|
+
const connection = await (0, codex_1.connectCodexViaCli)({
|
|
186
|
+
route: "chrome-relay",
|
|
187
|
+
sessionId: "sess-codex-relay-resumed",
|
|
188
|
+
async execute(command) {
|
|
189
|
+
commands.push(command.args);
|
|
190
|
+
if (command.args[0] === "capabilities") {
|
|
191
|
+
return { stdout: JSON.stringify({ capabilities }) };
|
|
192
|
+
}
|
|
193
|
+
if (command.args[0] === "diagnostics") {
|
|
194
|
+
return { stdout: JSON.stringify({ diagnostics }) };
|
|
195
|
+
}
|
|
196
|
+
if (command.args[0] === "resume") {
|
|
197
|
+
return { stdout: JSON.stringify({ resumedSession }) };
|
|
198
|
+
}
|
|
199
|
+
throw new Error(`Unexpected command: ${command.args.join(" ")}`);
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
strict_1.default.equal(connection.operation, "resumeSession");
|
|
203
|
+
strict_1.default.equal(connection.session.id, "sess-codex-relay-resumed");
|
|
204
|
+
strict_1.default.equal(connection.sessionUx.sharedTabScoped, true);
|
|
205
|
+
strict_1.default.deepEqual(commands, [
|
|
206
|
+
["capabilities"],
|
|
207
|
+
["diagnostics", "--browser", "chrome"],
|
|
208
|
+
["resume", "--id", "sess-codex-relay-resumed"]
|
|
209
|
+
]);
|
|
210
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const node_test_1 = __importDefault(require("node:test"));
|
|
7
|
+
const strict_1 = __importDefault(require("node:assert/strict"));
|
|
8
|
+
const node_http_1 = require("node:http");
|
|
9
|
+
const node_child_process_1 = require("node:child_process");
|
|
10
|
+
const node_util_1 = require("node:util");
|
|
11
|
+
const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
12
|
+
function createCapabilitiesPayload() {
|
|
13
|
+
return {
|
|
14
|
+
capabilities: {
|
|
15
|
+
schemaVersion: 1,
|
|
16
|
+
browsers: [
|
|
17
|
+
{
|
|
18
|
+
browser: "safari",
|
|
19
|
+
kind: "safari-actionable",
|
|
20
|
+
operations: {
|
|
21
|
+
attach: true,
|
|
22
|
+
diagnostics: true,
|
|
23
|
+
resumeSession: true,
|
|
24
|
+
activate: true,
|
|
25
|
+
navigate: true,
|
|
26
|
+
screenshot: true
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
browser: "chrome",
|
|
31
|
+
kind: "chrome-readonly",
|
|
32
|
+
operations: {
|
|
33
|
+
attach: true,
|
|
34
|
+
diagnostics: true,
|
|
35
|
+
resumeSession: true,
|
|
36
|
+
activate: false,
|
|
37
|
+
navigate: false,
|
|
38
|
+
screenshot: false
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function createSafariDiagnostics(blockers = []) {
|
|
46
|
+
const ready = blockers.length === 0;
|
|
47
|
+
return {
|
|
48
|
+
diagnostics: {
|
|
49
|
+
browser: "safari",
|
|
50
|
+
checkedAt: "2026-03-28T12:00:00.000Z",
|
|
51
|
+
preflight: {
|
|
52
|
+
inspect: { ready, blockers },
|
|
53
|
+
automation: { ready, blockers },
|
|
54
|
+
screenshot: { ready: true, blockers: [] }
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function createChromeDiagnostics(args) {
|
|
60
|
+
return {
|
|
61
|
+
diagnostics: {
|
|
62
|
+
browser: "chrome",
|
|
63
|
+
checkedAt: "2026-03-28T12:00:00.000Z",
|
|
64
|
+
attach: {
|
|
65
|
+
direct: {
|
|
66
|
+
mode: "direct",
|
|
67
|
+
ready: args.direct.ready,
|
|
68
|
+
state: args.direct.state,
|
|
69
|
+
blockers: args.direct.blockers ?? []
|
|
70
|
+
},
|
|
71
|
+
relay: {
|
|
72
|
+
mode: "relay",
|
|
73
|
+
ready: args.relay.ready,
|
|
74
|
+
state: args.relay.state,
|
|
75
|
+
blockers: args.relay.blockers ?? []
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function createSafariSession() {
|
|
82
|
+
return {
|
|
83
|
+
session: {
|
|
84
|
+
id: "session-safari-demo",
|
|
85
|
+
schemaVersion: 1,
|
|
86
|
+
browser: "safari",
|
|
87
|
+
kind: "safari-actionable",
|
|
88
|
+
attach: { mode: "direct", scope: "browser" },
|
|
89
|
+
semantics: {
|
|
90
|
+
inspect: "browser-tabs",
|
|
91
|
+
resume: "saved-browser-target",
|
|
92
|
+
tabReference: { windowIndex: "browser-position", tabIndex: "browser-position" }
|
|
93
|
+
},
|
|
94
|
+
capabilities: { resume: true, activate: true, navigate: true, screenshot: true },
|
|
95
|
+
status: { state: "actionable", canAct: true }
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function createChromeDirectSession() {
|
|
100
|
+
return {
|
|
101
|
+
session: {
|
|
102
|
+
id: "session-chrome-direct-demo",
|
|
103
|
+
schemaVersion: 1,
|
|
104
|
+
browser: "chrome",
|
|
105
|
+
kind: "chrome-readonly",
|
|
106
|
+
attach: { mode: "direct", scope: "browser" },
|
|
107
|
+
semantics: {
|
|
108
|
+
inspect: "browser-tabs",
|
|
109
|
+
resume: "saved-browser-target",
|
|
110
|
+
tabReference: { windowIndex: "browser-position", tabIndex: "browser-position" }
|
|
111
|
+
},
|
|
112
|
+
capabilities: { resume: true, activate: false, navigate: false, screenshot: false },
|
|
113
|
+
status: { state: "read-only", canAct: false }
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
function createChromeRelaySession() {
|
|
118
|
+
return {
|
|
119
|
+
session: {
|
|
120
|
+
id: "session-chrome-relay-demo",
|
|
121
|
+
schemaVersion: 1,
|
|
122
|
+
browser: "chrome",
|
|
123
|
+
kind: "chrome-readonly",
|
|
124
|
+
attach: {
|
|
125
|
+
mode: "relay",
|
|
126
|
+
scope: "tab",
|
|
127
|
+
resumable: false,
|
|
128
|
+
resumeRequiresUserGesture: true,
|
|
129
|
+
expiresAt: "2099-03-28T12:00:00.000Z"
|
|
130
|
+
},
|
|
131
|
+
semantics: {
|
|
132
|
+
inspect: "shared-tab-only",
|
|
133
|
+
resume: "current-shared-tab",
|
|
134
|
+
tabReference: {
|
|
135
|
+
windowIndex: "synthetic-shared-tab-position",
|
|
136
|
+
tabIndex: "synthetic-shared-tab-position"
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
capabilities: { resume: true, activate: false, navigate: false, screenshot: false },
|
|
140
|
+
status: { state: "read-only", canAct: false }
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
async function readJsonBody(request) {
|
|
145
|
+
const chunks = [];
|
|
146
|
+
for await (const chunk of request) {
|
|
147
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
148
|
+
}
|
|
149
|
+
if (chunks.length === 0) {
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
153
|
+
}
|
|
154
|
+
function writeStubJson(response, payload, successStatusCode) {
|
|
155
|
+
const statusCode = payload && typeof payload === "object" && "error" in payload && payload.error && typeof payload.error === "object" && "statusCode" in payload.error
|
|
156
|
+
? Number(payload.error.statusCode)
|
|
157
|
+
: successStatusCode;
|
|
158
|
+
response.writeHead(statusCode, { "content-type": "application/json" });
|
|
159
|
+
response.end(JSON.stringify(payload));
|
|
160
|
+
}
|
|
161
|
+
async function withStubBridge(state, run) {
|
|
162
|
+
const server = (0, node_http_1.createServer)(async (request, response) => {
|
|
163
|
+
const url = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
164
|
+
const body = request.method === "POST" ? await readJsonBody(request) : undefined;
|
|
165
|
+
state.requests.push({ method: request.method, url: `${url.pathname}${url.search}`, body });
|
|
166
|
+
if (request.method === "GET" && url.pathname === "/v1/capabilities") {
|
|
167
|
+
response.writeHead(200, { "content-type": "application/json" });
|
|
168
|
+
response.end(JSON.stringify(state.capabilities));
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (request.method === "GET" && url.pathname === "/v1/diagnostics") {
|
|
172
|
+
const browser = url.searchParams.get("browser") ?? "";
|
|
173
|
+
response.writeHead(200, { "content-type": "application/json" });
|
|
174
|
+
response.end(JSON.stringify(state.diagnosticsByBrowser[browser]));
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (request.method === "POST" && url.pathname === "/v1/attach") {
|
|
178
|
+
const attachMode = body && typeof body === "object" && "attach" in body && body.attach && typeof body.attach === "object" && "mode" in body.attach
|
|
179
|
+
? String(body.attach.mode)
|
|
180
|
+
: "direct";
|
|
181
|
+
writeStubJson(response, state.attachResponseByMode[attachMode], 201);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (request.method === "POST" && url.pathname.startsWith("/v1/sessions/") && url.pathname.endsWith("/resume")) {
|
|
185
|
+
writeStubJson(response, state.resumeResponse, 200);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
response.writeHead(404, { "content-type": "application/json" });
|
|
189
|
+
response.end(JSON.stringify({ error: { code: "not_found", message: "not found", statusCode: 404 } }));
|
|
190
|
+
});
|
|
191
|
+
await new Promise((resolvePromise) => server.listen(0, "127.0.0.1", resolvePromise));
|
|
192
|
+
const address = server.address();
|
|
193
|
+
strict_1.default.ok(address && typeof address === "object");
|
|
194
|
+
const baseUrl = `http://127.0.0.1:${address.port}`;
|
|
195
|
+
try {
|
|
196
|
+
return await run(baseUrl);
|
|
197
|
+
}
|
|
198
|
+
finally {
|
|
199
|
+
await new Promise((resolvePromise, reject) => server.close((error) => (error ? reject(error) : resolvePromise())));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
async function runDemo(route, args) {
|
|
203
|
+
return await execFileAsync(process.execPath, ["--experimental-strip-types", "examples/clients/http-node.ts", route], {
|
|
204
|
+
cwd: process.cwd(),
|
|
205
|
+
env: {
|
|
206
|
+
...process.env,
|
|
207
|
+
LOCAL_BROWSER_BRIDGE_URL: args?.baseUrl,
|
|
208
|
+
LOCAL_BROWSER_BRIDGE_SESSION_ID: args?.sessionId
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
(0, node_test_1.default)("http demo smoke: safari route uses capabilities + diagnostics and renders actionable labeling", async () => {
|
|
213
|
+
const state = {
|
|
214
|
+
capabilities: createCapabilitiesPayload(),
|
|
215
|
+
diagnosticsByBrowser: {
|
|
216
|
+
safari: createSafariDiagnostics(),
|
|
217
|
+
chrome: createChromeDiagnostics({
|
|
218
|
+
direct: { ready: false, state: "unavailable" },
|
|
219
|
+
relay: { ready: false, state: "unavailable" }
|
|
220
|
+
})
|
|
221
|
+
},
|
|
222
|
+
attachResponseByMode: {
|
|
223
|
+
direct: createSafariSession(),
|
|
224
|
+
relay: createChromeRelaySession()
|
|
225
|
+
},
|
|
226
|
+
requests: []
|
|
227
|
+
};
|
|
228
|
+
await withStubBridge(state, async (baseUrl) => {
|
|
229
|
+
const result = await runDemo("safari", { baseUrl });
|
|
230
|
+
strict_1.default.match(result.stdout, /Requested route: safari/);
|
|
231
|
+
strict_1.default.match(result.stdout, /Capabilities: kind=safari-actionable/);
|
|
232
|
+
strict_1.default.match(result.stdout, /Selected path: Safari \(actionable\)/);
|
|
233
|
+
strict_1.default.match(result.stdout, /Diagnostics readiness: state=ready, ready=true/);
|
|
234
|
+
strict_1.default.match(result.stdout, /Attaching via Safari \(actionable\)/);
|
|
235
|
+
strict_1.default.match(result.stdout, /Session kind: safari-actionable/);
|
|
236
|
+
strict_1.default.match(result.stdout, /User-facing label: Safari \(actionable\)/);
|
|
237
|
+
strict_1.default.match(result.stdout, /Runtime actions: activate=true, navigate=true, screenshot=true/);
|
|
238
|
+
strict_1.default.deepEqual(state.requests.map((entry) => `${entry.method} ${entry.url}`), ["GET /v1/capabilities", "GET /v1/diagnostics?browser=safari", "POST /v1/attach"]);
|
|
239
|
+
strict_1.default.deepEqual(state.requests[2]?.body, {
|
|
240
|
+
browser: "safari",
|
|
241
|
+
attach: { mode: "direct" }
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
(0, node_test_1.default)("http demo smoke: chrome-direct route renders read-only browser path and no silent relay fallback", async () => {
|
|
246
|
+
const state = {
|
|
247
|
+
capabilities: createCapabilitiesPayload(),
|
|
248
|
+
diagnosticsByBrowser: {
|
|
249
|
+
chrome: createChromeDiagnostics({
|
|
250
|
+
direct: { ready: true, state: "ready" },
|
|
251
|
+
relay: {
|
|
252
|
+
ready: false,
|
|
253
|
+
state: "attention-required",
|
|
254
|
+
blockers: [{ code: "relay_share_required", message: "share first" }]
|
|
255
|
+
}
|
|
256
|
+
}),
|
|
257
|
+
safari: createSafariDiagnostics()
|
|
258
|
+
},
|
|
259
|
+
attachResponseByMode: {
|
|
260
|
+
direct: createChromeDirectSession(),
|
|
261
|
+
relay: createChromeRelaySession()
|
|
262
|
+
},
|
|
263
|
+
requests: []
|
|
264
|
+
};
|
|
265
|
+
await withStubBridge(state, async (baseUrl) => {
|
|
266
|
+
const result = await runDemo("chrome-direct", { baseUrl });
|
|
267
|
+
strict_1.default.match(result.stdout, /Requested route: chrome-direct/);
|
|
268
|
+
strict_1.default.match(result.stdout, /Capabilities: kind=chrome-readonly/);
|
|
269
|
+
strict_1.default.match(result.stdout, /Selected path: Chrome \(direct, read-only\)/);
|
|
270
|
+
strict_1.default.match(result.stdout, /Diagnostics readiness: state=ready, ready=true/);
|
|
271
|
+
strict_1.default.match(result.stdout, /Attaching via Chrome \(direct, read-only\)/);
|
|
272
|
+
strict_1.default.match(result.stdout, /Session kind: chrome-readonly/);
|
|
273
|
+
strict_1.default.match(result.stdout, /Session attach mode: direct/);
|
|
274
|
+
strict_1.default.match(result.stdout, /Flow state: attached/);
|
|
275
|
+
strict_1.default.match(result.stdout, /User-facing label: Chrome \(direct, read-only\)/);
|
|
276
|
+
strict_1.default.match(result.stdout, /Show inspect\/resume UI only\. Hide activate\/navigate\/screenshot\./);
|
|
277
|
+
strict_1.default.deepEqual(state.requests.map((entry) => `${entry.method} ${entry.url}`), ["GET /v1/capabilities", "GET /v1/diagnostics?browser=chrome", "POST /v1/attach"]);
|
|
278
|
+
strict_1.default.deepEqual(state.requests[2]?.body, {
|
|
279
|
+
browser: "chrome",
|
|
280
|
+
attach: { mode: "direct" }
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
(0, node_test_1.default)("http demo smoke: chrome-relay route renders shared-tab labeling and relay resume prompt", async () => {
|
|
285
|
+
const state = {
|
|
286
|
+
capabilities: createCapabilitiesPayload(),
|
|
287
|
+
diagnosticsByBrowser: {
|
|
288
|
+
chrome: createChromeDiagnostics({
|
|
289
|
+
direct: { ready: false, state: "unavailable" },
|
|
290
|
+
relay: { ready: true, state: "ready" }
|
|
291
|
+
}),
|
|
292
|
+
safari: createSafariDiagnostics()
|
|
293
|
+
},
|
|
294
|
+
attachResponseByMode: {
|
|
295
|
+
direct: createChromeDirectSession(),
|
|
296
|
+
relay: createChromeRelaySession()
|
|
297
|
+
},
|
|
298
|
+
requests: [],
|
|
299
|
+
resumeResponse: {
|
|
300
|
+
resumedSession: createChromeRelaySession()
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
await withStubBridge(state, async (baseUrl) => {
|
|
304
|
+
const result = await runDemo("chrome-relay", { baseUrl, sessionId: "saved-relay-session" });
|
|
305
|
+
strict_1.default.match(result.stdout, /Requested route: chrome-relay/);
|
|
306
|
+
strict_1.default.match(result.stdout, /Selected path: Chrome \(shared tab, read-only\)/);
|
|
307
|
+
strict_1.default.match(result.stdout, /Diagnostics readiness: state=ready, ready=true/);
|
|
308
|
+
strict_1.default.match(result.stdout, /Resuming saved session saved-relay-session for Chrome \(shared tab, read-only\)/);
|
|
309
|
+
strict_1.default.match(result.stdout, /Session kind: chrome-readonly/);
|
|
310
|
+
strict_1.default.match(result.stdout, /Session attach mode: relay/);
|
|
311
|
+
strict_1.default.match(result.stdout, /Flow state: resumed/);
|
|
312
|
+
strict_1.default.match(result.stdout, /User-facing label: Chrome \(shared tab, read-only\)/);
|
|
313
|
+
strict_1.default.match(result.stdout, /Describe this as a shared-tab session, not a browser-wide Chrome session\./);
|
|
314
|
+
strict_1.default.match(result.stdout, /Derive relay behavior from attach\.mode=relay plus the returned semantics fields\./);
|
|
315
|
+
strict_1.default.match(result.stdout, /Inspect semantics: shared-tab-only/);
|
|
316
|
+
strict_1.default.match(result.stdout, /Resume semantics: current-shared-tab/);
|
|
317
|
+
strict_1.default.match(result.stdout, /Tab reference semantics: windowIndex=synthetic-shared-tab-position, tabIndex=synthetic-shared-tab-position/);
|
|
318
|
+
strict_1.default.match(result.stdout, /Scope note: Chrome relay is still limited to the currently shared tab and remains read-only\./);
|
|
319
|
+
strict_1.default.match(result.stdout, /Resume prompt: That shared-tab grant is no longer active\. Click the relay extension again on the original tab, then retry resume\./);
|
|
320
|
+
strict_1.default.deepEqual(state.requests.map((entry) => `${entry.method} ${entry.url}`), ["GET /v1/capabilities", "GET /v1/diagnostics?browser=chrome", "POST /v1/sessions/saved-relay-session/resume"]);
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
(0, node_test_1.default)("http demo smoke: relay attach failures surface structured relay branch prompts", async () => {
|
|
324
|
+
const state = {
|
|
325
|
+
capabilities: createCapabilitiesPayload(),
|
|
326
|
+
diagnosticsByBrowser: {
|
|
327
|
+
chrome: createChromeDiagnostics({
|
|
328
|
+
direct: { ready: true, state: "ready" },
|
|
329
|
+
relay: { ready: true, state: "ready" }
|
|
330
|
+
}),
|
|
331
|
+
safari: createSafariDiagnostics()
|
|
332
|
+
},
|
|
333
|
+
attachResponseByMode: {
|
|
334
|
+
direct: createChromeDirectSession(),
|
|
335
|
+
relay: {
|
|
336
|
+
error: {
|
|
337
|
+
code: "relay_attach_target_out_of_scope",
|
|
338
|
+
message: "Chrome relay attach is scoped to the currently shared tab only; use the front tab target or omit an explicit target.",
|
|
339
|
+
statusCode: 409,
|
|
340
|
+
details: {
|
|
341
|
+
context: { browser: "chrome", attachMode: "relay", operation: "attach" },
|
|
342
|
+
relay: {
|
|
343
|
+
branch: "use-current-shared-tab",
|
|
344
|
+
retryable: true,
|
|
345
|
+
userActionRequired: true,
|
|
346
|
+
phase: "target-selection",
|
|
347
|
+
sharedTabScope: "current-shared-tab",
|
|
348
|
+
currentSharedTabMatches: false
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
},
|
|
354
|
+
requests: []
|
|
355
|
+
};
|
|
356
|
+
await withStubBridge(state, async (baseUrl) => {
|
|
357
|
+
try {
|
|
358
|
+
await runDemo("chrome-relay", { baseUrl });
|
|
359
|
+
strict_1.default.fail("expected the demo to exit with an error");
|
|
360
|
+
}
|
|
361
|
+
catch (error) {
|
|
362
|
+
const execError = error;
|
|
363
|
+
strict_1.default.equal(execError.code, 1);
|
|
364
|
+
strict_1.default.match(execError.stderr ?? "", /Relay failure branch: use-current-shared-tab/);
|
|
365
|
+
strict_1.default.match(execError.stderr ?? "", /Relay failure phase: target-selection/);
|
|
366
|
+
strict_1.default.match(execError.stderr ?? "", /Relay UX state: user-action-required/);
|
|
367
|
+
strict_1.default.match(execError.stderr ?? "", /Relay failure category: shared-tab-read-only-scope-limitation/);
|
|
368
|
+
strict_1.default.match(execError.stderr ?? "", /Relay failure retryable: true/);
|
|
369
|
+
strict_1.default.match(execError.stderr ?? "", /Scope note: Chrome relay is still limited to the currently shared tab and remains read-only\./);
|
|
370
|
+
strict_1.default.match(execError.stderr ?? "", /Relay user prompt: Chrome relay attach only works for the tab that is currently shared\. Use the shared tab or share a different tab first\./);
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
(0, node_test_1.default)("http demo smoke: readiness blockers surface documented prompts without attempting attach", async () => {
|
|
375
|
+
const state = {
|
|
376
|
+
capabilities: createCapabilitiesPayload(),
|
|
377
|
+
diagnosticsByBrowser: {
|
|
378
|
+
chrome: createChromeDiagnostics({
|
|
379
|
+
direct: {
|
|
380
|
+
ready: false,
|
|
381
|
+
state: "unavailable",
|
|
382
|
+
blockers: [
|
|
383
|
+
{
|
|
384
|
+
code: "direct_unavailable_attach_endpoint_missing",
|
|
385
|
+
message: "missing local DevTools endpoint"
|
|
386
|
+
}
|
|
387
|
+
]
|
|
388
|
+
},
|
|
389
|
+
relay: { ready: false, state: "unavailable" }
|
|
390
|
+
}),
|
|
391
|
+
safari: createSafariDiagnostics()
|
|
392
|
+
},
|
|
393
|
+
attachResponseByMode: {
|
|
394
|
+
direct: createChromeDirectSession(),
|
|
395
|
+
relay: createChromeRelaySession()
|
|
396
|
+
},
|
|
397
|
+
requests: []
|
|
398
|
+
};
|
|
399
|
+
await withStubBridge(state, async (baseUrl) => {
|
|
400
|
+
const result = await runDemo("chrome-direct", { baseUrl });
|
|
401
|
+
strict_1.default.match(result.stdout, /User prompt: Chrome direct attach needs a local DevTools endpoint that is already available on this machine\./);
|
|
402
|
+
strict_1.default.match(result.stdout, /Attach skipped because the selected path is not ready\./);
|
|
403
|
+
strict_1.default.deepEqual(state.requests.map((entry) => `${entry.method} ${entry.url}`), ["GET /v1/capabilities", "GET /v1/diagnostics?browser=chrome"]);
|
|
404
|
+
});
|
|
405
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|