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,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizeCodexRoute = normalizeCodexRoute;
|
|
4
|
+
exports.connectCodexViaCli = connectCodexViaCli;
|
|
5
|
+
exports.connectCodexViaHttp = connectCodexViaHttp;
|
|
6
|
+
const reference_adapter_1 = require("./reference-adapter");
|
|
7
|
+
const claude_code_1 = require("./claude-code");
|
|
8
|
+
const cli_reference_adapter_1 = require("./cli-reference-adapter");
|
|
9
|
+
const http_reference_adapter_1 = require("./http-reference-adapter");
|
|
10
|
+
function normalizeCodexRoute(route, sessionId) {
|
|
11
|
+
return (0, claude_code_1.normalizeClaudeCodeRoute)({ route, sessionId });
|
|
12
|
+
}
|
|
13
|
+
async function connectCodexViaCli(options) {
|
|
14
|
+
const adapter = (0, cli_reference_adapter_1.createCliBridgeAdapter)({
|
|
15
|
+
execute: options.execute
|
|
16
|
+
});
|
|
17
|
+
return (0, reference_adapter_1.connectViaBridge)(adapter, normalizeCodexRoute(options.route, options.sessionId));
|
|
18
|
+
}
|
|
19
|
+
async function connectCodexViaHttp(options) {
|
|
20
|
+
const adapter = (0, http_reference_adapter_1.createHttpBridgeAdapter)({
|
|
21
|
+
execute: options.execute,
|
|
22
|
+
paths: options.paths
|
|
23
|
+
});
|
|
24
|
+
return (0, reference_adapter_1.connectViaBridge)(adapter, normalizeCodexRoute(options.route, options.sessionId));
|
|
25
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { type BrowserAttachUxInterpretation } from "./browser-attach-ux-helper";
|
|
2
|
+
import { type ClaudeCodeRouteName } from "./claude-code";
|
|
3
|
+
import type { AttachService } from "./service/attach-service";
|
|
4
|
+
import type { AttachmentSession, BridgeCapabilitiesContract, BrowserAttachMode, BrowserDiagnostics, ChromeRelayFailureOperation, ErrorPayload, SupportedBrowser } from "./types";
|
|
5
|
+
export type ConnectionRouteName = ClaudeCodeRouteName;
|
|
6
|
+
export interface ConnectionRouteInput {
|
|
7
|
+
route: ConnectionRouteName;
|
|
8
|
+
sessionId?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface ConnectionNextStep {
|
|
11
|
+
action: "connect" | "fix-blocker" | "session-ready" | "retry" | "review-error";
|
|
12
|
+
prompt: string;
|
|
13
|
+
command?: string;
|
|
14
|
+
}
|
|
15
|
+
export type ConnectionOutcome = "success" | "blocked" | "unsupported" | "error";
|
|
16
|
+
export type ConnectionStatus = "ready" | "connected" | "blocked" | "unsupported" | "failed";
|
|
17
|
+
export type ConnectionCategory = "route-ready" | "route-blocked" | "session-connected" | "connection-blocked" | "connection-unsupported" | "connection-failed";
|
|
18
|
+
export interface ConnectionReason {
|
|
19
|
+
code: string;
|
|
20
|
+
message: string;
|
|
21
|
+
retryable?: boolean;
|
|
22
|
+
userActionRequired?: boolean;
|
|
23
|
+
}
|
|
24
|
+
interface ConnectionRouteDescriptor {
|
|
25
|
+
name: ConnectionRouteName;
|
|
26
|
+
browser: SupportedBrowser;
|
|
27
|
+
attachMode: BrowserAttachMode;
|
|
28
|
+
sessionId?: string;
|
|
29
|
+
}
|
|
30
|
+
interface ConnectionEnvelopeBase {
|
|
31
|
+
ok: boolean;
|
|
32
|
+
command: "doctor" | "connect";
|
|
33
|
+
route: ConnectionRouteDescriptor;
|
|
34
|
+
operation: ChromeRelayFailureOperation;
|
|
35
|
+
outcome: ConnectionOutcome;
|
|
36
|
+
status: ConnectionStatus;
|
|
37
|
+
category: ConnectionCategory;
|
|
38
|
+
reason?: ConnectionReason;
|
|
39
|
+
summary: string;
|
|
40
|
+
prompt?: string;
|
|
41
|
+
nextStep: ConnectionNextStep;
|
|
42
|
+
capabilities: BridgeCapabilitiesContract;
|
|
43
|
+
diagnostics: BrowserDiagnostics;
|
|
44
|
+
routeUx: BrowserAttachUxInterpretation;
|
|
45
|
+
}
|
|
46
|
+
export interface ConnectionDoctorEnvelope extends ConnectionEnvelopeBase {
|
|
47
|
+
blocked: boolean;
|
|
48
|
+
}
|
|
49
|
+
export interface ConnectionConnectEnvelope extends ConnectionEnvelopeBase {
|
|
50
|
+
blocked: boolean;
|
|
51
|
+
connected: boolean;
|
|
52
|
+
session?: AttachmentSession;
|
|
53
|
+
sessionUx?: BrowserAttachUxInterpretation;
|
|
54
|
+
error?: ErrorPayload["error"];
|
|
55
|
+
errorUx?: BrowserAttachUxInterpretation;
|
|
56
|
+
}
|
|
57
|
+
export declare function normalizeConnectionRouteName(value: string | undefined): ConnectionRouteName;
|
|
58
|
+
export declare function createServiceBridgeAdapter(service: Pick<AttachService, "getCapabilities" | "diagnostics" | "attach" | "resumeSession">): import("./reference-adapter").BridgeAdapter<BridgeCapabilitiesContract, AttachmentSession, import("./types").ResumedSession>;
|
|
59
|
+
export declare function doctorConnectionRoute(service: Pick<AttachService, "getCapabilities" | "diagnostics" | "attach" | "resumeSession">, input: ConnectionRouteInput): Promise<ConnectionDoctorEnvelope>;
|
|
60
|
+
export declare function connectConnectionRoute(service: Pick<AttachService, "getCapabilities" | "diagnostics" | "attach" | "resumeSession">, input: ConnectionRouteInput): Promise<ConnectionConnectEnvelope>;
|
|
61
|
+
export {};
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizeConnectionRouteName = normalizeConnectionRouteName;
|
|
4
|
+
exports.createServiceBridgeAdapter = createServiceBridgeAdapter;
|
|
5
|
+
exports.doctorConnectionRoute = doctorConnectionRoute;
|
|
6
|
+
exports.connectConnectionRoute = connectConnectionRoute;
|
|
7
|
+
const browser_attach_ux_helper_1 = require("./browser-attach-ux-helper");
|
|
8
|
+
const claude_code_1 = require("./claude-code");
|
|
9
|
+
const errors_1 = require("./errors");
|
|
10
|
+
const reference_adapter_1 = require("./reference-adapter");
|
|
11
|
+
function normalizeConnectionRouteName(value) {
|
|
12
|
+
if (value === "safari" || value === "chrome-direct" || value === "chrome-relay") {
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
throw new errors_1.AppError("--route must be safari, chrome-direct, or chrome-relay.", 400, "invalid_route");
|
|
16
|
+
}
|
|
17
|
+
function createServiceBridgeAdapter(service) {
|
|
18
|
+
return (0, reference_adapter_1.createBridgeAdapter)({
|
|
19
|
+
async getCapabilities() {
|
|
20
|
+
return service.getCapabilities();
|
|
21
|
+
},
|
|
22
|
+
async getDiagnostics(browser) {
|
|
23
|
+
return service.diagnostics(browser);
|
|
24
|
+
},
|
|
25
|
+
async attach(route) {
|
|
26
|
+
return service.attach(route.browser, {
|
|
27
|
+
attach: route.attachMode ? { mode: route.attachMode } : undefined
|
|
28
|
+
});
|
|
29
|
+
},
|
|
30
|
+
async resume(sessionId) {
|
|
31
|
+
return service.resumeSession(sessionId);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
function toDescriptor(input) {
|
|
36
|
+
const route = (0, claude_code_1.normalizeClaudeCodeRoute)(input);
|
|
37
|
+
return {
|
|
38
|
+
name: input.route,
|
|
39
|
+
browser: route.browser,
|
|
40
|
+
attachMode: route.attachMode ?? "direct",
|
|
41
|
+
...(input.sessionId ? { sessionId: input.sessionId } : {})
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function connectCommand(input) {
|
|
45
|
+
return input.sessionId
|
|
46
|
+
? `local-browser-bridge connect --route ${input.route} --session-id ${input.sessionId}`
|
|
47
|
+
: `local-browser-bridge connect --route ${input.route}`;
|
|
48
|
+
}
|
|
49
|
+
function routeTruthNote(routeUx) {
|
|
50
|
+
if (routeUx.sharedTabScoped) {
|
|
51
|
+
return " It remains read-only and only covers the currently shared tab.";
|
|
52
|
+
}
|
|
53
|
+
if (routeUx.readOnly) {
|
|
54
|
+
return " It remains read-only.";
|
|
55
|
+
}
|
|
56
|
+
return " It is actionable.";
|
|
57
|
+
}
|
|
58
|
+
function summarizeDoctor(routeUx) {
|
|
59
|
+
if (routeUx.state === "blocked") {
|
|
60
|
+
return `${routeUx.label} is not ready yet.${routeTruthNote(routeUx)}`;
|
|
61
|
+
}
|
|
62
|
+
return `${routeUx.label} is ready for ${routeUx.operation === "resumeSession" ? "resume" : "attach"}.${routeTruthNote(routeUx)}`;
|
|
63
|
+
}
|
|
64
|
+
function summarizeSession(session, sessionUx) {
|
|
65
|
+
const verb = sessionUx.operation === "resumeSession" ? "Resumed" : "Connected";
|
|
66
|
+
return `${verb} ${sessionUx.label} session ${session.id}.${routeTruthNote(sessionUx)}`;
|
|
67
|
+
}
|
|
68
|
+
function summarizeFailure(routeUx, error) {
|
|
69
|
+
if (!routeUx) {
|
|
70
|
+
return `Connection failed: ${error.message}`;
|
|
71
|
+
}
|
|
72
|
+
return `${routeUx.label} could not connect.${routeTruthNote(routeUx)}`;
|
|
73
|
+
}
|
|
74
|
+
function nextStepForDoctor(input, routeUx) {
|
|
75
|
+
if (routeUx.state === "blocked") {
|
|
76
|
+
return {
|
|
77
|
+
action: "fix-blocker",
|
|
78
|
+
prompt: routeUx.prompt ?? "Resolve the reported blocker, then retry doctor or connect."
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
action: "connect",
|
|
83
|
+
prompt: `Run ${connectCommand(input)} to continue.`,
|
|
84
|
+
command: connectCommand(input)
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function nextStepForSession(session, sessionUx) {
|
|
88
|
+
if (session.status.canAct) {
|
|
89
|
+
return {
|
|
90
|
+
action: "session-ready",
|
|
91
|
+
prompt: `Use session ${session.id} for follow-up actions like activate, navigate, or screenshot.`
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
action: "session-ready",
|
|
96
|
+
prompt: `Use session ${session.id} for inspect/resume flows and keep the read-only scope explicit.`
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function nextStepForFailure(routeUx, error) {
|
|
100
|
+
if (routeUx?.userActionRequired || routeUx?.state === "user-action-required") {
|
|
101
|
+
return {
|
|
102
|
+
action: "fix-blocker",
|
|
103
|
+
prompt: routeUx.prompt ?? routeUx.retryGuidance ?? error.message
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
if (routeUx?.retryable) {
|
|
107
|
+
return {
|
|
108
|
+
action: "retry",
|
|
109
|
+
prompt: routeUx.retryGuidance ?? routeUx.prompt ?? error.message
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
action: "review-error",
|
|
114
|
+
prompt: routeUx?.prompt ?? error.message
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
function firstDiagnosticBlocker(diagnostics, route) {
|
|
118
|
+
if (route.browser === "safari") {
|
|
119
|
+
return [
|
|
120
|
+
...(diagnostics.preflight?.inspect.blockers ?? []),
|
|
121
|
+
...(diagnostics.preflight?.automation.blockers ?? []),
|
|
122
|
+
...(diagnostics.preflight?.screenshot.blockers ?? [])
|
|
123
|
+
][0];
|
|
124
|
+
}
|
|
125
|
+
return (route.attachMode === "relay" ? diagnostics.attach?.relay?.blockers : diagnostics.attach?.direct?.blockers)?.[0];
|
|
126
|
+
}
|
|
127
|
+
function isUnsupportedReasonCode(code) {
|
|
128
|
+
return code === "shared_tab_scope_only"
|
|
129
|
+
|| code === "unsupported_action"
|
|
130
|
+
|| code === "unsupported_browser"
|
|
131
|
+
|| code === "unsupported_route";
|
|
132
|
+
}
|
|
133
|
+
function toErrorReason(error, errorUx) {
|
|
134
|
+
return {
|
|
135
|
+
code: error.code,
|
|
136
|
+
message: error.message,
|
|
137
|
+
...(typeof errorUx?.retryable === "boolean" ? { retryable: errorUx.retryable } : {}),
|
|
138
|
+
...(typeof errorUx?.userActionRequired === "boolean" ? { userActionRequired: errorUx.userActionRequired } : {})
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
async function loadRouteState(adapter, input) {
|
|
142
|
+
const descriptor = toDescriptor(input);
|
|
143
|
+
const capabilities = await adapter.getCapabilities();
|
|
144
|
+
const diagnostics = await adapter.getDiagnostics(descriptor.browser);
|
|
145
|
+
const operation = input.sessionId ? "resumeSession" : "attach";
|
|
146
|
+
const routeUx = (0, browser_attach_ux_helper_1.interpretBrowserAttachUxFromDiagnostics)({
|
|
147
|
+
browser: descriptor.browser,
|
|
148
|
+
attachMode: descriptor.attachMode,
|
|
149
|
+
diagnostics,
|
|
150
|
+
operation
|
|
151
|
+
});
|
|
152
|
+
return {
|
|
153
|
+
descriptor,
|
|
154
|
+
capabilities,
|
|
155
|
+
diagnostics,
|
|
156
|
+
operation,
|
|
157
|
+
routeUx
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
async function doctorConnectionRoute(service, input) {
|
|
161
|
+
const adapter = createServiceBridgeAdapter(service);
|
|
162
|
+
const state = await loadRouteState(adapter, input);
|
|
163
|
+
return {
|
|
164
|
+
ok: state.routeUx.state !== "blocked",
|
|
165
|
+
command: "doctor",
|
|
166
|
+
route: state.descriptor,
|
|
167
|
+
operation: state.operation,
|
|
168
|
+
outcome: state.routeUx.state === "blocked" ? "blocked" : "success",
|
|
169
|
+
status: state.routeUx.state === "blocked" ? "blocked" : "ready",
|
|
170
|
+
category: state.routeUx.state === "blocked" ? "route-blocked" : "route-ready",
|
|
171
|
+
...(state.routeUx.state === "blocked" ? { reason: firstDiagnosticBlocker(state.diagnostics, state.descriptor) } : {}),
|
|
172
|
+
summary: summarizeDoctor(state.routeUx),
|
|
173
|
+
prompt: state.routeUx.prompt,
|
|
174
|
+
nextStep: nextStepForDoctor(input, state.routeUx),
|
|
175
|
+
capabilities: state.capabilities,
|
|
176
|
+
diagnostics: state.diagnostics,
|
|
177
|
+
routeUx: state.routeUx,
|
|
178
|
+
blocked: state.routeUx.state === "blocked"
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
async function connectConnectionRoute(service, input) {
|
|
182
|
+
const adapter = createServiceBridgeAdapter(service);
|
|
183
|
+
const state = await loadRouteState(adapter, input);
|
|
184
|
+
try {
|
|
185
|
+
const prepared = await (0, claude_code_1.prepareClaudeCodeRoute)(adapter, input);
|
|
186
|
+
if (prepared.blocked) {
|
|
187
|
+
return {
|
|
188
|
+
ok: false,
|
|
189
|
+
command: "connect",
|
|
190
|
+
route: state.descriptor,
|
|
191
|
+
operation: state.operation,
|
|
192
|
+
outcome: "blocked",
|
|
193
|
+
status: "blocked",
|
|
194
|
+
category: "connection-blocked",
|
|
195
|
+
reason: firstDiagnosticBlocker(prepared.diagnostics, state.descriptor),
|
|
196
|
+
summary: summarizeDoctor(prepared.routeUx),
|
|
197
|
+
prompt: prepared.prompt,
|
|
198
|
+
nextStep: nextStepForDoctor(input, prepared.routeUx),
|
|
199
|
+
capabilities: prepared.capabilities,
|
|
200
|
+
diagnostics: prepared.diagnostics,
|
|
201
|
+
routeUx: prepared.routeUx,
|
|
202
|
+
blocked: true,
|
|
203
|
+
connected: false
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
const connection = prepared.connection;
|
|
207
|
+
return {
|
|
208
|
+
ok: true,
|
|
209
|
+
command: "connect",
|
|
210
|
+
route: state.descriptor,
|
|
211
|
+
operation: state.operation,
|
|
212
|
+
outcome: "success",
|
|
213
|
+
status: "connected",
|
|
214
|
+
category: "session-connected",
|
|
215
|
+
summary: summarizeSession(connection.session, connection.sessionUx),
|
|
216
|
+
prompt: prepared.prompt,
|
|
217
|
+
nextStep: nextStepForSession(connection.session, connection.sessionUx),
|
|
218
|
+
capabilities: connection.capabilities,
|
|
219
|
+
diagnostics: connection.diagnostics,
|
|
220
|
+
routeUx: connection.routeUx,
|
|
221
|
+
blocked: false,
|
|
222
|
+
connected: true,
|
|
223
|
+
session: connection.session,
|
|
224
|
+
sessionUx: connection.sessionUx
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
const { payload } = (0, errors_1.toErrorPayload)(error);
|
|
229
|
+
const errorUx = (0, browser_attach_ux_helper_1.interpretBrowserAttachUxFromError)({
|
|
230
|
+
details: payload.error.details,
|
|
231
|
+
browser: state.descriptor.browser,
|
|
232
|
+
attachMode: state.descriptor.attachMode,
|
|
233
|
+
operation: state.operation
|
|
234
|
+
});
|
|
235
|
+
return {
|
|
236
|
+
ok: false,
|
|
237
|
+
command: "connect",
|
|
238
|
+
route: state.descriptor,
|
|
239
|
+
operation: state.operation,
|
|
240
|
+
outcome: isUnsupportedReasonCode(payload.error.code) ? "unsupported" : "error",
|
|
241
|
+
status: isUnsupportedReasonCode(payload.error.code) ? "unsupported" : "failed",
|
|
242
|
+
category: isUnsupportedReasonCode(payload.error.code) ? "connection-unsupported" : "connection-failed",
|
|
243
|
+
reason: toErrorReason(payload.error, errorUx),
|
|
244
|
+
summary: summarizeFailure(errorUx, payload.error),
|
|
245
|
+
prompt: errorUx?.prompt ?? payload.error.message,
|
|
246
|
+
nextStep: nextStepForFailure(errorUx, payload.error),
|
|
247
|
+
capabilities: state.capabilities,
|
|
248
|
+
diagnostics: state.diagnostics,
|
|
249
|
+
routeUx: state.routeUx,
|
|
250
|
+
blocked: false,
|
|
251
|
+
connected: false,
|
|
252
|
+
error: payload.error,
|
|
253
|
+
errorUx
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { BridgeErrorDetails, ErrorPayload } from "./types";
|
|
2
|
+
export declare class AppError<TDetails = BridgeErrorDetails> extends Error {
|
|
3
|
+
readonly statusCode: number;
|
|
4
|
+
readonly code: string;
|
|
5
|
+
readonly details?: TDetails;
|
|
6
|
+
constructor(message: string, statusCode?: number, code?: string, details?: TDetails);
|
|
7
|
+
}
|
|
8
|
+
export declare function toErrorPayload(error: unknown): {
|
|
9
|
+
statusCode: number;
|
|
10
|
+
payload: ErrorPayload;
|
|
11
|
+
};
|
|
12
|
+
export declare function writeJsonLine(stream: NodeJS.WritableStream, payload: unknown): void;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AppError = void 0;
|
|
4
|
+
exports.toErrorPayload = toErrorPayload;
|
|
5
|
+
exports.writeJsonLine = writeJsonLine;
|
|
6
|
+
class AppError extends Error {
|
|
7
|
+
statusCode;
|
|
8
|
+
code;
|
|
9
|
+
details;
|
|
10
|
+
constructor(message, statusCode = 500, code = "internal_error", details) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = "AppError";
|
|
13
|
+
this.statusCode = statusCode;
|
|
14
|
+
this.code = code;
|
|
15
|
+
this.details = details;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
exports.AppError = AppError;
|
|
19
|
+
function toErrorPayload(error) {
|
|
20
|
+
if (error instanceof AppError) {
|
|
21
|
+
return {
|
|
22
|
+
statusCode: error.statusCode,
|
|
23
|
+
payload: {
|
|
24
|
+
error: {
|
|
25
|
+
code: error.code,
|
|
26
|
+
message: error.message,
|
|
27
|
+
statusCode: error.statusCode,
|
|
28
|
+
...(error.details ? { details: error.details } : {})
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
if (error instanceof SyntaxError) {
|
|
34
|
+
return {
|
|
35
|
+
statusCode: 400,
|
|
36
|
+
payload: {
|
|
37
|
+
error: {
|
|
38
|
+
code: "invalid_json",
|
|
39
|
+
message: "Request body must be valid JSON.",
|
|
40
|
+
statusCode: 400
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
statusCode: 500,
|
|
47
|
+
payload: {
|
|
48
|
+
error: {
|
|
49
|
+
code: "internal_error",
|
|
50
|
+
message: "Internal server error.",
|
|
51
|
+
statusCode: 500
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function writeJsonLine(stream, payload) {
|
|
57
|
+
stream.write(JSON.stringify(payload, null, 2) + "\n");
|
|
58
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type BridgeAdapter, type BridgeSessionResult } from "./reference-adapter";
|
|
2
|
+
import type { BrowserDiagnostics, BridgeCapabilitiesContract, ResumedSession } from "./types";
|
|
3
|
+
export interface HttpBridgeRequest {
|
|
4
|
+
method: "GET" | "POST";
|
|
5
|
+
path: string;
|
|
6
|
+
body?: unknown;
|
|
7
|
+
}
|
|
8
|
+
export interface HttpBridgeResponse<TBody = unknown> {
|
|
9
|
+
status?: number;
|
|
10
|
+
body: TBody;
|
|
11
|
+
}
|
|
12
|
+
export type HttpBridgeExecutor = (request: HttpBridgeRequest) => Promise<HttpBridgeResponse>;
|
|
13
|
+
export interface HttpCapabilitiesEnvelope<TCapabilities = BridgeCapabilitiesContract> {
|
|
14
|
+
capabilities?: TCapabilities;
|
|
15
|
+
}
|
|
16
|
+
export interface HttpDiagnosticsEnvelope {
|
|
17
|
+
diagnostics?: BrowserDiagnostics;
|
|
18
|
+
}
|
|
19
|
+
export interface HttpAttachEnvelope<TAttachResult extends BridgeSessionResult = BridgeSessionResult> {
|
|
20
|
+
session?: TAttachResult;
|
|
21
|
+
}
|
|
22
|
+
export interface HttpResumeEnvelope<TResumeResult extends ResumedSession = ResumedSession> {
|
|
23
|
+
resumedSession?: TResumeResult;
|
|
24
|
+
}
|
|
25
|
+
export interface CreateHttpBridgeAdapterOptions<TCapabilities = BridgeCapabilitiesContract, TAttachResult extends BridgeSessionResult = BridgeSessionResult, TResumeResult extends ResumedSession = ResumedSession> {
|
|
26
|
+
execute: HttpBridgeExecutor;
|
|
27
|
+
paths?: {
|
|
28
|
+
capabilities?: string;
|
|
29
|
+
diagnostics?: string;
|
|
30
|
+
attach?: string;
|
|
31
|
+
resumeSession?: (sessionId: string) => string;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export declare function createHttpBridgeAdapter<TCapabilities = BridgeCapabilitiesContract, TAttachResult extends BridgeSessionResult = BridgeSessionResult, TResumeResult extends ResumedSession = ResumedSession>(options: CreateHttpBridgeAdapterOptions<TCapabilities, TAttachResult, TResumeResult>): BridgeAdapter<TCapabilities, TAttachResult, TResumeResult>;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createHttpBridgeAdapter = createHttpBridgeAdapter;
|
|
4
|
+
const errors_1 = require("./errors");
|
|
5
|
+
const reference_adapter_1 = require("./reference-adapter");
|
|
6
|
+
function requireEnvelopeField(envelope, key, context) {
|
|
7
|
+
const value = envelope[key];
|
|
8
|
+
if (value === undefined || value === null) {
|
|
9
|
+
throw new errors_1.AppError(`Expected ${context} response to include ${String(key)}.`, 500, "invalid_transport_response");
|
|
10
|
+
}
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
function buildQuery(path, values) {
|
|
14
|
+
const url = new URL(path, "http://local-browser-bridge.test");
|
|
15
|
+
for (const [key, value] of Object.entries(values)) {
|
|
16
|
+
if (value !== undefined) {
|
|
17
|
+
url.searchParams.set(key, value);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return `${url.pathname}${url.search}`;
|
|
21
|
+
}
|
|
22
|
+
function createHttpBridgeAdapter(options) {
|
|
23
|
+
const capabilitiesPath = options.paths?.capabilities ?? "/v1/capabilities";
|
|
24
|
+
const diagnosticsPath = options.paths?.diagnostics ?? "/v1/diagnostics";
|
|
25
|
+
const attachPath = options.paths?.attach ?? "/v1/attach";
|
|
26
|
+
const resumeSessionPath = options.paths?.resumeSession ?? ((sessionId) => `/v1/sessions/${encodeURIComponent(sessionId)}/resume`);
|
|
27
|
+
return (0, reference_adapter_1.createBridgeAdapter)({
|
|
28
|
+
async getCapabilities() {
|
|
29
|
+
const response = await options.execute({
|
|
30
|
+
method: "GET",
|
|
31
|
+
path: capabilitiesPath
|
|
32
|
+
});
|
|
33
|
+
return requireEnvelopeField(response.body, "capabilities", "capabilities");
|
|
34
|
+
},
|
|
35
|
+
async getDiagnostics(browser) {
|
|
36
|
+
const response = await options.execute({
|
|
37
|
+
method: "GET",
|
|
38
|
+
path: buildQuery(diagnosticsPath, { browser })
|
|
39
|
+
});
|
|
40
|
+
return requireEnvelopeField(response.body, "diagnostics", "diagnostics");
|
|
41
|
+
},
|
|
42
|
+
async attach(args) {
|
|
43
|
+
const response = await options.execute({
|
|
44
|
+
method: "POST",
|
|
45
|
+
path: attachPath,
|
|
46
|
+
body: {
|
|
47
|
+
browser: args.browser,
|
|
48
|
+
attach: { mode: args.attachMode }
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
return requireEnvelopeField(response.body, "session", "attach");
|
|
52
|
+
},
|
|
53
|
+
async resume(sessionId) {
|
|
54
|
+
const response = await options.execute({
|
|
55
|
+
method: "POST",
|
|
56
|
+
path: resumeSessionPath(sessionId)
|
|
57
|
+
});
|
|
58
|
+
return requireEnvelopeField(response.body, "resumedSession", "resumeSession");
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
package/dist/src/http.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createApiServer = createApiServer;
|
|
4
|
+
const node_http_1 = require("node:http");
|
|
5
|
+
const errors_1 = require("./errors");
|
|
6
|
+
const browser_1 = require("./browser");
|
|
7
|
+
const target_1 = require("./target");
|
|
8
|
+
function writeJson(response, statusCode, payload) {
|
|
9
|
+
response.writeHead(statusCode, { "content-type": "application/json; charset=utf-8" });
|
|
10
|
+
response.end(JSON.stringify(payload, null, 2));
|
|
11
|
+
}
|
|
12
|
+
async function readJsonBody(request) {
|
|
13
|
+
const chunks = [];
|
|
14
|
+
for await (const chunk of request) {
|
|
15
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
16
|
+
}
|
|
17
|
+
if (chunks.length === 0) {
|
|
18
|
+
return {};
|
|
19
|
+
}
|
|
20
|
+
const raw = Buffer.concat(chunks).toString("utf8");
|
|
21
|
+
return JSON.parse(raw);
|
|
22
|
+
}
|
|
23
|
+
function requireString(value, fieldName, code) {
|
|
24
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
25
|
+
throw new errors_1.AppError(`${fieldName} is required.`, 400, code);
|
|
26
|
+
}
|
|
27
|
+
return value.trim();
|
|
28
|
+
}
|
|
29
|
+
function createApiServer(service) {
|
|
30
|
+
return (0, node_http_1.createServer)(async (request, response) => {
|
|
31
|
+
try {
|
|
32
|
+
const url = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
33
|
+
const method = request.method ?? "GET";
|
|
34
|
+
const sessionMatch = url.pathname.match(/^\/v1\/sessions\/([^/]+)$/);
|
|
35
|
+
const resumeMatch = url.pathname.match(/^\/v1\/sessions\/([^/]+)\/resume$/);
|
|
36
|
+
const activateMatch = url.pathname.match(/^\/v1\/sessions\/([^/]+)\/activate$/);
|
|
37
|
+
const navigateMatch = url.pathname.match(/^\/v1\/sessions\/([^/]+)\/navigate$/);
|
|
38
|
+
const sessionScreenshotMatch = url.pathname.match(/^\/v1\/sessions\/([^/]+)\/screenshot$/);
|
|
39
|
+
if (method === "GET" && url.pathname === "/health") {
|
|
40
|
+
writeJson(response, 200, { ok: true });
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (method === "GET" && url.pathname === "/v1/diagnostics") {
|
|
44
|
+
const browser = (0, browser_1.normalizeBrowser)(url.searchParams.get("browser") ?? undefined);
|
|
45
|
+
const diagnostics = await service.diagnostics(browser);
|
|
46
|
+
writeJson(response, 200, { diagnostics });
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (method === "GET" && url.pathname === "/v1/capabilities") {
|
|
50
|
+
const browserFlag = url.searchParams.get("browser") ?? undefined;
|
|
51
|
+
const browser = browserFlag ? (0, browser_1.normalizeBrowser)(browserFlag) : undefined;
|
|
52
|
+
const capabilities = service.getCapabilities(browser);
|
|
53
|
+
writeJson(response, 200, { capabilities });
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (method === "GET" && url.pathname === "/v1/front-tab") {
|
|
57
|
+
const browser = (0, browser_1.normalizeBrowser)(url.searchParams.get("browser") ?? undefined);
|
|
58
|
+
const frontTab = await service.inspectFrontTab(browser);
|
|
59
|
+
writeJson(response, 200, { frontTab });
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (method === "GET" && url.pathname === "/v1/tab") {
|
|
63
|
+
const browser = (0, browser_1.normalizeBrowser)(url.searchParams.get("browser") ?? undefined);
|
|
64
|
+
const target = (0, target_1.buildTabTarget)({
|
|
65
|
+
windowIndex: url.searchParams.get("windowIndex") ?? undefined,
|
|
66
|
+
tabIndex: url.searchParams.get("tabIndex") ?? undefined,
|
|
67
|
+
signature: url.searchParams.get("signature") ?? undefined,
|
|
68
|
+
url: url.searchParams.get("url") ?? undefined,
|
|
69
|
+
title: url.searchParams.get("title") ?? undefined
|
|
70
|
+
});
|
|
71
|
+
const tab = await service.inspectTab(browser, target);
|
|
72
|
+
writeJson(response, 200, { tab });
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (method === "GET" && url.pathname === "/v1/tabs") {
|
|
76
|
+
const browser = (0, browser_1.normalizeBrowser)(url.searchParams.get("browser") ?? undefined);
|
|
77
|
+
const tabs = await service.listTabs(browser);
|
|
78
|
+
writeJson(response, 200, { tabs });
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (method === "POST" && url.pathname === "/v1/attach") {
|
|
82
|
+
const body = (await readJsonBody(request));
|
|
83
|
+
const browser = (0, browser_1.normalizeBrowser)(body.browser);
|
|
84
|
+
const target = (0, target_1.buildTabTarget)(body.target);
|
|
85
|
+
const attachMode = body.attach?.mode;
|
|
86
|
+
if (attachMode !== undefined && attachMode !== "direct" && attachMode !== "relay") {
|
|
87
|
+
throw new errors_1.AppError("attach.mode must be direct or relay.", 400, "invalid_attach_mode");
|
|
88
|
+
}
|
|
89
|
+
const session = await service.attach(browser, {
|
|
90
|
+
target,
|
|
91
|
+
attach: { mode: attachMode }
|
|
92
|
+
});
|
|
93
|
+
writeJson(response, 201, { session });
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (method === "POST" && url.pathname === "/v1/activate") {
|
|
97
|
+
const body = (await readJsonBody(request));
|
|
98
|
+
const browser = (0, browser_1.normalizeBrowser)(body.browser);
|
|
99
|
+
const target = (0, target_1.buildTabTarget)(body.target);
|
|
100
|
+
const activation = await service.activate(browser, target);
|
|
101
|
+
writeJson(response, 201, { activation });
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (method === "POST" && url.pathname === "/v1/navigate") {
|
|
105
|
+
const body = (await readJsonBody(request));
|
|
106
|
+
const browser = (0, browser_1.normalizeBrowser)(body.browser);
|
|
107
|
+
const target = (0, target_1.buildTabTarget)(body.target);
|
|
108
|
+
const navigation = await service.navigate(browser, target, {
|
|
109
|
+
url: requireString(body.url, "url", "missing_url")
|
|
110
|
+
});
|
|
111
|
+
writeJson(response, 201, { navigation });
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (method === "POST" && url.pathname === "/v1/screenshot") {
|
|
115
|
+
const body = (await readJsonBody(request));
|
|
116
|
+
const browser = (0, browser_1.normalizeBrowser)(body.browser);
|
|
117
|
+
const target = (0, target_1.buildTabTarget)(body.target);
|
|
118
|
+
const screenshot = await service.screenshot(browser, target, { outputPath: body.outputPath });
|
|
119
|
+
writeJson(response, 201, { screenshot });
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (method === "GET" && url.pathname === "/v1/sessions") {
|
|
123
|
+
const sessions = await service.listSessions();
|
|
124
|
+
writeJson(response, 200, { sessions });
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (method === "GET" && sessionMatch) {
|
|
128
|
+
const session = await service.getSession(decodeURIComponent(sessionMatch[1]));
|
|
129
|
+
writeJson(response, 200, { session });
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (method === "POST" && resumeMatch) {
|
|
133
|
+
const resumedSession = await service.resumeSession(decodeURIComponent(resumeMatch[1]));
|
|
134
|
+
writeJson(response, 200, { resumedSession });
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (method === "POST" && activateMatch) {
|
|
138
|
+
const sessionActivation = await service.activateSession(decodeURIComponent(activateMatch[1]));
|
|
139
|
+
writeJson(response, 201, { sessionActivation });
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (method === "POST" && navigateMatch) {
|
|
143
|
+
const body = (await readJsonBody(request));
|
|
144
|
+
const sessionNavigation = await service.navigateSession(decodeURIComponent(navigateMatch[1]), { url: requireString(body.url, "url", "missing_url") });
|
|
145
|
+
writeJson(response, 201, { sessionNavigation });
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (method === "POST" && sessionScreenshotMatch) {
|
|
149
|
+
const body = (await readJsonBody(request));
|
|
150
|
+
const sessionScreenshot = await service.screenshotSession(decodeURIComponent(sessionScreenshotMatch[1]), { outputPath: body.outputPath });
|
|
151
|
+
writeJson(response, 201, { sessionScreenshot });
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
writeJson(response, 404, { error: { code: "not_found", message: "Route not found.", statusCode: 404 } });
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
const { statusCode, payload } = (0, errors_1.toErrorPayload)(error);
|
|
158
|
+
writeJson(response, statusCode, payload);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
}
|