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.
Files changed (92) hide show
  1. package/README.md +724 -0
  2. package/dist/package.json +61 -0
  3. package/dist/src/browser/chrome.d.ts +19 -0
  4. package/dist/src/browser/chrome.js +778 -0
  5. package/dist/src/browser/index.d.ts +3 -0
  6. package/dist/src/browser/index.js +25 -0
  7. package/dist/src/browser/safari.d.ts +41 -0
  8. package/dist/src/browser/safari.js +827 -0
  9. package/dist/src/browser-attach-ux-helper.d.ts +39 -0
  10. package/dist/src/browser-attach-ux-helper.js +157 -0
  11. package/dist/src/capabilities.d.ts +3 -0
  12. package/dist/src/capabilities.js +182 -0
  13. package/dist/src/chrome-relay-error-helper.d.ts +19 -0
  14. package/dist/src/chrome-relay-error-helper.js +78 -0
  15. package/dist/src/chrome-relay-helper-cli.d.ts +2 -0
  16. package/dist/src/chrome-relay-helper-cli.js +97 -0
  17. package/dist/src/chrome-relay-helper.d.ts +29 -0
  18. package/dist/src/chrome-relay-helper.js +151 -0
  19. package/dist/src/chrome-relay-state.d.ts +23 -0
  20. package/dist/src/chrome-relay-state.js +108 -0
  21. package/dist/src/claude-code.d.ts +20 -0
  22. package/dist/src/claude-code.js +66 -0
  23. package/dist/src/cli-reference-adapter.d.ts +13 -0
  24. package/dist/src/cli-reference-adapter.js +48 -0
  25. package/dist/src/cli.d.ts +3 -0
  26. package/dist/src/cli.js +200 -0
  27. package/dist/src/codex.d.ts +17 -0
  28. package/dist/src/codex.js +25 -0
  29. package/dist/src/connection-ux.d.ts +61 -0
  30. package/dist/src/connection-ux.js +256 -0
  31. package/dist/src/errors.d.ts +12 -0
  32. package/dist/src/errors.js +58 -0
  33. package/dist/src/http-reference-adapter.d.ts +34 -0
  34. package/dist/src/http-reference-adapter.js +61 -0
  35. package/dist/src/http.d.ts +3 -0
  36. package/dist/src/http.js +161 -0
  37. package/dist/src/index.d.ts +17 -0
  38. package/dist/src/index.js +43 -0
  39. package/dist/src/mcp-stdio.d.ts +2 -0
  40. package/dist/src/mcp-stdio.js +10 -0
  41. package/dist/src/mcp.d.ts +25 -0
  42. package/dist/src/mcp.js +483 -0
  43. package/dist/src/reference-adapter.d.ts +32 -0
  44. package/dist/src/reference-adapter.js +42 -0
  45. package/dist/src/service/attach-service.d.ts +28 -0
  46. package/dist/src/service/attach-service.js +272 -0
  47. package/dist/src/session-metadata.d.ts +4 -0
  48. package/dist/src/session-metadata.js +88 -0
  49. package/dist/src/store/session-store.d.ts +14 -0
  50. package/dist/src/store/session-store.js +52 -0
  51. package/dist/src/target.d.ts +9 -0
  52. package/dist/src/target.js +61 -0
  53. package/dist/src/types.d.ts +397 -0
  54. package/dist/src/types.js +2 -0
  55. package/dist/tests/attach-service.test.d.ts +1 -0
  56. package/dist/tests/attach-service.test.js +1367 -0
  57. package/dist/tests/browser-attach-ux-helper.test.d.ts +1 -0
  58. package/dist/tests/browser-attach-ux-helper.test.js +139 -0
  59. package/dist/tests/chrome-relay-error-helper.test.d.ts +1 -0
  60. package/dist/tests/chrome-relay-error-helper.test.js +67 -0
  61. package/dist/tests/chrome-relay-helper.test.d.ts +1 -0
  62. package/dist/tests/chrome-relay-helper.test.js +142 -0
  63. package/dist/tests/chrome-relay-state-schema.test.d.ts +1 -0
  64. package/dist/tests/chrome-relay-state-schema.test.js +96 -0
  65. package/dist/tests/claude-code-wrapper.test.d.ts +1 -0
  66. package/dist/tests/claude-code-wrapper.test.js +170 -0
  67. package/dist/tests/codex.test.d.ts +1 -0
  68. package/dist/tests/codex.test.js +210 -0
  69. package/dist/tests/demo-client-smoke.test.d.ts +1 -0
  70. package/dist/tests/demo-client-smoke.test.js +405 -0
  71. package/dist/tests/docs-fixtures.test.d.ts +1 -0
  72. package/dist/tests/docs-fixtures.test.js +255 -0
  73. package/dist/tests/doctor-connect-wrapper.test.d.ts +1 -0
  74. package/dist/tests/doctor-connect-wrapper.test.js +62 -0
  75. package/dist/tests/fixtures/doctor-connect-cli-stub.d.ts +1 -0
  76. package/dist/tests/fixtures/doctor-connect-cli-stub.js +93 -0
  77. package/dist/tests/fixtures/public-root-cli-stub.d.ts +210 -0
  78. package/dist/tests/fixtures/public-root-cli-stub.js +143 -0
  79. package/dist/tests/fixtures/public-root-consumer.js +67 -0
  80. package/dist/tests/mcp.test.d.ts +1 -0
  81. package/dist/tests/mcp.test.js +345 -0
  82. package/dist/tests/public-consumer-helpers.test.d.ts +1 -0
  83. package/dist/tests/public-consumer-helpers.test.js +33 -0
  84. package/dist/tests/public-package-git-consumption.test.d.ts +1 -0
  85. package/dist/tests/public-package-git-consumption.test.js +56 -0
  86. package/dist/tests/public-root-consumer-smoke.test.d.ts +1 -0
  87. package/dist/tests/public-root-consumer-smoke.test.js +214 -0
  88. package/dist/tests/reference-adapter.test.d.ts +1 -0
  89. package/dist/tests/reference-adapter.test.js +220 -0
  90. package/dist/tests/transport-reference-adapters.test.d.ts +1 -0
  91. package/dist/tests/transport-reference-adapters.test.js +214 -0
  92. 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
+ }
@@ -0,0 +1,3 @@
1
+ import { type IncomingMessage, type ServerResponse } from "node:http";
2
+ import { AttachService } from "./service/attach-service";
3
+ export declare function createApiServer(service: AttachService): import("http").Server<typeof IncomingMessage, typeof ServerResponse>;
@@ -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
+ }