localpreview 0.2.3 → 0.2.5

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.
@@ -2,6 +2,8 @@ import { decodeServerRelayMessage, encodeRelayMessage, filterEndToEndHeaderPairs
2
2
  import { Console, Context, Effect, Fiber, Layer, Ref } from "effect";
3
3
  import { buildCaptureCookiePathPrefix, resolveRoute, } from "./capture-route.js";
4
4
  import { buildCaptureShimScript, buildFrontendOrigin, injectCaptureShim, isHtmlResponse, stripContentSecurityPolicy, } from "./capture-shim.js";
5
+ import { formatCapturedRequestInbound, formatCapturedRequestOutbound, formatInvalidCaptureRouteError, formatMissingCaptureWarning, formatRequestError, formatRequestInbound, formatRequestOutbound, } from "./cli-ui.js";
6
+ import { buildSuggestedCaptureCommand, formatMissingCaptureOrigin, isLocalPreviewEventsPath, missingCaptureDedupeKey, parseMissingCaptureEvent, shouldReportMissingCapture, } from "./missing-capture.js";
5
7
  import { CliConfig } from "./config.js";
6
8
  import { LocalRequestError, RelayProtocolError, RequestLimitError, errorMessage, } from "./errors.js";
7
9
  export class LocalProxy extends Context.Service()("LocalProxy") {
@@ -23,7 +25,7 @@ const handleMessage = (config, requests, socket, session, input) => Effect.gen(f
23
25
  const message = decoded.message;
24
26
  switch (message.type) {
25
27
  case "request-start":
26
- return yield* handleRequestStart(config, requests, socket, message);
28
+ return yield* handleRequestStart(config, requests, socket, session, message);
27
29
  case "request-chunk":
28
30
  return yield* handleRequestChunk(config, requests, socket, message);
29
31
  case "request-end":
@@ -32,7 +34,7 @@ const handleMessage = (config, requests, socket, session, input) => Effect.gen(f
32
34
  return yield* handleCancel(requests, message.requestId);
33
35
  }
34
36
  });
35
- const handleRequestStart = (config, requests, socket, message) => Effect.gen(function* () {
37
+ const handleRequestStart = (config, requests, socket, session, message) => Effect.gen(function* () {
36
38
  const current = yield* Ref.get(requests);
37
39
  if (current.size >= config.maxInFlightRequests) {
38
40
  yield* send(socket, {
@@ -51,7 +53,16 @@ const handleRequestStart = (config, requests, socket, message) => Effect.gen(fun
51
53
  totalBytes: 0,
52
54
  });
53
55
  yield* Ref.set(requests, next);
54
- yield* Console.log(`-> ${message.method} ${logPath(message.path)}`);
56
+ if (shouldLogOutboundRequest(message.method, message.path)) {
57
+ const route = resolveRoute(message.path, session.target, session.captures);
58
+ const displayPath = route.kind === "capture"
59
+ ? formatCaptureTargetUrl(route.capturePath)
60
+ : logPath(message.path);
61
+ const line = route.kind === "capture"
62
+ ? formatCapturedRequestOutbound(message.method, displayPath)
63
+ : formatRequestOutbound(message.method, displayPath);
64
+ yield* Console.log(line);
65
+ }
55
66
  });
56
67
  const handleRequestChunk = (config, requests, socket, message) => Effect.gen(function* () {
57
68
  const current = yield* Ref.get(requests);
@@ -94,7 +105,7 @@ const handleRequestEnd = (config, requests, socket, session, message) => Effect.
94
105
  message: error.message,
95
106
  requestId: message.requestId,
96
107
  type: "response-error",
97
- }).pipe(Effect.andThen(Console.log(`<- error ${logPath(request.path)} ${error.message}`)))), Effect.ensuring(Ref.update(requests, (map) => {
108
+ }).pipe(Effect.andThen(Console.log(formatProxyRequestError(request, error))))), Effect.ensuring(Ref.update(requests, (map) => {
98
109
  const next = new Map(map);
99
110
  next.delete(message.requestId);
100
111
  return next;
@@ -114,9 +125,18 @@ const handleCancel = (requests, requestId) => Effect.gen(function* () {
114
125
  yield* Ref.set(requests, next);
115
126
  });
116
127
  const proxyRequest = (config, socket, session, requestId, request) => Effect.gen(function* () {
128
+ if (request.method === "POST" && isLocalPreviewEventsPath(request.path)) {
129
+ yield* handleMissingCaptureEvent(session, request);
130
+ yield* sendEventsResponse(socket, requestId);
131
+ return;
132
+ }
117
133
  const route = resolveRoute(request.path, session.target, session.captures);
118
134
  if (route.kind === "invalid-capture") {
119
135
  return yield* Effect.fail(new LocalRequestError({
136
+ invalidCapture: {
137
+ hostname: route.capturePath.hostname,
138
+ port: route.capturePath.port,
139
+ },
120
140
  message: route.message,
121
141
  requestId,
122
142
  }));
@@ -169,12 +189,10 @@ const proxyRequest = (config, socket, session, requestId, request) => Effect.gen
169
189
  }));
170
190
  }
171
191
  const contentType = response.headers.get("content-type");
172
- const shouldInjectShim = route.kind === "primary" &&
173
- session.captures.length > 0 &&
174
- isHtmlResponse(contentType);
192
+ const shouldInjectShim = route.kind === "primary" && isHtmlResponse(contentType);
175
193
  if (shouldInjectShim) {
176
194
  const html = responseBody.toString("utf8");
177
- const shimScript = buildCaptureShimScript(session.captures);
195
+ const shimScript = buildCaptureShimScript(session.target, session.captures);
178
196
  responseBody = Buffer.from(injectCaptureShim(html, shimScript), "utf8");
179
197
  }
180
198
  let responseHeaders = filterLocalResponseHeaders(response.headers);
@@ -202,7 +220,54 @@ const proxyRequest = (config, socket, session, requestId, request) => Effect.gen
202
220
  requestId,
203
221
  type: "response-end",
204
222
  });
205
- yield* Console.log(`<- ${response.status} ${logPath(request.path)} ${Date.now() - startedAt}ms`);
223
+ const displayPath = route.kind === "capture" ? formatCaptureTargetUrl(route.capturePath) : logPath(request.path);
224
+ const line = route.kind === "capture"
225
+ ? formatCapturedRequestInbound(response.status, displayPath, Date.now() - startedAt)
226
+ : formatRequestInbound(response.status, displayPath, Date.now() - startedAt);
227
+ yield* Console.log(line);
228
+ });
229
+ const handleMissingCaptureEvent = (session, request) => Effect.gen(function* () {
230
+ const bodyText = Buffer.concat(request.chunks).toString("utf8");
231
+ if (bodyText.length === 0) {
232
+ return;
233
+ }
234
+ let parsedBody;
235
+ try {
236
+ parsedBody = JSON.parse(bodyText);
237
+ }
238
+ catch {
239
+ return;
240
+ }
241
+ const event = parseMissingCaptureEvent(parsedBody);
242
+ if (event === undefined ||
243
+ !shouldReportMissingCapture(event, session.target, session.captures)) {
244
+ return;
245
+ }
246
+ const dedupeKey = missingCaptureDedupeKey(event.payload.protocol, event.payload.hostname, event.payload.port);
247
+ if (session.reportedMissingCaptures.has(dedupeKey)) {
248
+ return;
249
+ }
250
+ session.reportedMissingCaptures.add(dedupeKey);
251
+ const origin = formatMissingCaptureOrigin(event.payload.protocol, event.payload.hostname, event.payload.port);
252
+ const suggestedCommand = buildSuggestedCaptureCommand(session.originalArgv, session.legacy, session.captures, event.payload.hostname, event.payload.port);
253
+ yield* Console.error(formatMissingCaptureWarning({
254
+ ...(event.payload.method === undefined ? {} : { method: event.payload.method }),
255
+ origin,
256
+ suggestedCommand,
257
+ transport: event.payload.transport,
258
+ }));
259
+ });
260
+ const sendEventsResponse = (socket, requestId) => Effect.gen(function* () {
261
+ yield* send(socket, {
262
+ headers: [],
263
+ requestId,
264
+ status: 204,
265
+ type: "response-start",
266
+ });
267
+ yield* send(socket, {
268
+ requestId,
269
+ type: "response-end",
270
+ });
206
271
  });
207
272
  const rewriteCaptureResponseHeaders = (headers, capturePath) => {
208
273
  const pathPrefix = buildCaptureCookiePathPrefix(capturePath);
@@ -249,6 +314,17 @@ const sendRequestError = (socket, requestId, error) => send(socket, {
249
314
  requestId,
250
315
  type: "response-error",
251
316
  });
317
+ const formatProxyRequestError = (request, error) => {
318
+ if (error instanceof LocalRequestError && error.invalidCapture !== undefined) {
319
+ return formatInvalidCaptureRouteError({
320
+ hostname: error.invalidCapture.hostname,
321
+ method: request.method,
322
+ path: request.path,
323
+ port: error.invalidCapture.port,
324
+ });
325
+ }
326
+ return formatRequestError(logPath(request.path), error.message);
327
+ };
252
328
  const send = (socket, message) => Effect.sync(() => {
253
329
  if (socket.readyState === socket.OPEN) {
254
330
  socket.send(encodeRelayMessage(message));
@@ -282,4 +358,6 @@ const logPath = (path) => {
282
358
  return path.split("?")[0] ?? path;
283
359
  }
284
360
  };
361
+ const formatCaptureTargetUrl = (capturePath) => `${capturePath.protocol}://${capturePath.hostname}:${capturePath.port}${capturePath.path}`;
362
+ const shouldLogOutboundRequest = (method, path) => !(method === "POST" && isLocalPreviewEventsPath(path));
285
363
  const timeoutFail = (millis, error) => (effect) => Effect.raceFirst(effect, Effect.sleep(`${millis} millis`).pipe(Effect.andThen(Effect.fail(error))));
@@ -0,0 +1,44 @@
1
+ import { type CaptureTarget, type TunnelTarget } from "@localpreview/protocol";
2
+ export declare const LOCALPREVIEW_EVENTS_PATH = "/__localpreview/events";
3
+ export type MissingCaptureTransport = "fetch" | "xhr" | "websocket" | "eventsource";
4
+ export type MissingCaptureProtocol = "http" | "https" | "ws" | "wss";
5
+ export type MissingCaptureEvent = {
6
+ readonly type: "missing-capture";
7
+ readonly payload: {
8
+ readonly protocol: "http" | "https" | "ws" | "wss";
9
+ readonly hostname: string;
10
+ readonly method?: string;
11
+ readonly port: number;
12
+ readonly transport: MissingCaptureTransport;
13
+ };
14
+ };
15
+ export type MissingCaptureEventInput = {
16
+ readonly hostname: string;
17
+ readonly method?: string;
18
+ readonly port: number;
19
+ readonly protocol: string;
20
+ readonly transport: string;
21
+ };
22
+ /** Canonical loopback group for localhost / 127.0.0.1 / [::1] equivalence. */
23
+ export declare const loopbackGroup: (hostname: string) => string;
24
+ /** Returns true when two loopback endpoints share host equivalence and port. */
25
+ export declare const sameLoopbackEndpoint: (leftHost: string, leftPort: number, rightHost: string, rightPort: number) => boolean;
26
+ /** Returns true when the endpoint matches the primary tunnel target port. */
27
+ export declare const isPrimaryTargetPort: (primaryTarget: Pick<TunnelTarget, "hostname" | "port">, hostname: string, port: number) => boolean;
28
+ /** Returns true when the endpoint is already allowlisted via --capture. */
29
+ export declare const isCapturedEndpoint: (captures: ReadonlyArray<CaptureTarget>, hostname: string, port: number) => boolean;
30
+ /** Formats a missing-capture origin as protocol://hostname:port. */
31
+ export declare const formatMissingCaptureOrigin: (protocol: MissingCaptureEvent["payload"]["protocol"], hostname: string, port: number) => string;
32
+ /** Dedupe key for one missing-capture report per protocol://hostname:port. */
33
+ export declare const missingCaptureDedupeKey: (protocol: MissingCaptureEvent["payload"]["protocol"], hostname: string, port: number) => string;
34
+ /** Returns true when the request targets the internal events route. */
35
+ export declare const isLocalPreviewEventsPath: (requestPath: string) => boolean;
36
+ /** Strictly validates a parsed missing-capture event body. */
37
+ export declare const parseMissingCaptureEvent: (body: unknown) => MissingCaptureEvent | undefined;
38
+ /** Builds a restart command preserving the original argv and appending --capture. */
39
+ export declare const buildSuggestedCaptureCommand: (originalArgv: ReadonlyArray<string>, legacy: boolean, captures: ReadonlyArray<CaptureTarget>, hostname: string, port: number) => string;
40
+ /** Returns true when a missing-capture event should produce a CLI warning. */
41
+ export declare const shouldReportMissingCapture: (event: MissingCaptureEvent, primaryTarget: Pick<TunnelTarget, "hostname" | "port">, captures: ReadonlyArray<CaptureTarget>) => boolean;
42
+ /** Formats a capture target for inclusion in warning text. */
43
+ export declare const formatMissingCaptureTarget: (capture: CaptureTarget) => string;
44
+ //# sourceMappingURL=missing-capture.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"missing-capture.d.ts","sourceRoot":"","sources":["../src/missing-capture.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,aAAa,EAClB,KAAK,YAAY,EAClB,MAAM,wBAAwB,CAAC;AAEhC,eAAO,MAAM,wBAAwB,2BAA2B,CAAC;AAKjE,MAAM,MAAM,uBAAuB,GAAG,OAAO,GAAG,KAAK,GAAG,WAAW,GAAG,aAAa,CAAC;AACpF,MAAM,MAAM,sBAAsB,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,KAAK,CAAC;AAErE,MAAM,MAAM,mBAAmB,GAAG;IAChC,QAAQ,CAAC,IAAI,EAAE,iBAAiB,CAAC;IACjC,QAAQ,CAAC,OAAO,EAAE;QAChB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,KAAK,CAAC;QACnD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;QAC1B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QACtB,QAAQ,CAAC,SAAS,EAAE,uBAAuB,CAAC;KAC7C,CAAC;CACH,CAAC;AAEF,MAAM,MAAM,wBAAwB,GAAG;IACrC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B,CAAC;AAQF,8EAA8E;AAC9E,eAAO,MAAM,aAAa,GAAI,UAAU,MAAM,KAAG,MAYhD,CAAC;AAEF,gFAAgF;AAChF,eAAO,MAAM,oBAAoB,GAC/B,UAAU,MAAM,EAChB,UAAU,MAAM,EAChB,WAAW,MAAM,EACjB,WAAW,MAAM,KAChB,OAAyF,CAAC;AAE7F,6EAA6E;AAC7E,eAAO,MAAM,mBAAmB,GAC9B,eAAe,IAAI,CAAC,YAAY,EAAE,UAAU,GAAG,MAAM,CAAC,EACtD,UAAU,MAAM,EAChB,MAAM,MAAM,KACX,OAA2F,CAAC;AAE/F,2EAA2E;AAC3E,eAAO,MAAM,kBAAkB,GAC7B,UAAU,aAAa,CAAC,aAAa,CAAC,EACtC,UAAU,MAAM,EAChB,MAAM,MAAM,KACX,OAC+F,CAAC;AAEnG,oEAAoE;AACpE,eAAO,MAAM,0BAA0B,GACrC,UAAU,mBAAmB,CAAC,SAAS,CAAC,CAAC,UAAU,CAAC,EACpD,UAAU,MAAM,EAChB,MAAM,MAAM,KACX,MAA6C,CAAC;AAEjD,8EAA8E;AAC9E,eAAO,MAAM,uBAAuB,GAClC,UAAU,mBAAmB,CAAC,SAAS,CAAC,CAAC,UAAU,CAAC,EACpD,UAAU,MAAM,EAChB,MAAM,MAAM,KACX,MAIF,CAAC;AAEF,uEAAuE;AACvE,eAAO,MAAM,wBAAwB,GAAI,aAAa,MAAM,KAAG,OAG9D,CAAC;AAcF,8DAA8D;AAC9D,eAAO,MAAM,wBAAwB,GAAI,MAAM,OAAO,KAAG,mBAAmB,GAAG,SA2C9E,CAAC;AA+CF,qFAAqF;AACrF,eAAO,MAAM,4BAA4B,GACvC,cAAc,aAAa,CAAC,MAAM,CAAC,EACnC,QAAQ,OAAO,EACf,UAAU,aAAa,CAAC,aAAa,CAAC,EACtC,UAAU,MAAM,EAChB,MAAM,MAAM,KACX,MASF,CAAC;AAEF,8EAA8E;AAC9E,eAAO,MAAM,0BAA0B,GACrC,OAAO,mBAAmB,EAC1B,eAAe,IAAI,CAAC,YAAY,EAAE,UAAU,GAAG,MAAM,CAAC,EACtD,UAAU,aAAa,CAAC,aAAa,CAAC,KACrC,OAYF,CAAC;AAEF,8DAA8D;AAC9D,eAAO,MAAM,0BAA0B,GAAI,SAAS,aAAa,KAAG,MACtC,CAAC"}
@@ -0,0 +1,128 @@
1
+ import { formatCaptureOrigin, isLoopbackHost, normalizeLoopbackHostname, } from "@localpreview/protocol";
2
+ export const LOCALPREVIEW_EVENTS_PATH = "/__localpreview/events";
3
+ const MISSING_CAPTURE_PROTOCOLS = new Set(["http", "https", "ws", "wss"]);
4
+ const MISSING_CAPTURE_TRANSPORTS = new Set(["fetch", "xhr", "websocket", "eventsource"]);
5
+ const isMissingCaptureProtocol = (value) => MISSING_CAPTURE_PROTOCOLS.has(value);
6
+ const isMissingCaptureTransport = (value) => MISSING_CAPTURE_TRANSPORTS.has(value);
7
+ /** Canonical loopback group for localhost / 127.0.0.1 / [::1] equivalence. */
8
+ export const loopbackGroup = (hostname) => {
9
+ const normalized = normalizeLoopbackHostname(hostname);
10
+ if (normalized === "localhost" ||
11
+ normalized === "127.0.0.1" ||
12
+ normalized === "[::1]") {
13
+ return "__loopback__";
14
+ }
15
+ return normalized;
16
+ };
17
+ /** Returns true when two loopback endpoints share host equivalence and port. */
18
+ export const sameLoopbackEndpoint = (leftHost, leftPort, rightHost, rightPort) => leftPort === rightPort && loopbackGroup(leftHost) === loopbackGroup(rightHost);
19
+ /** Returns true when the endpoint matches the primary tunnel target port. */
20
+ export const isPrimaryTargetPort = (primaryTarget, hostname, port) => sameLoopbackEndpoint(primaryTarget.hostname, primaryTarget.port, hostname, port);
21
+ /** Returns true when the endpoint is already allowlisted via --capture. */
22
+ export const isCapturedEndpoint = (captures, hostname, port) => captures.some((capture) => sameLoopbackEndpoint(capture.hostname, capture.port, hostname, port));
23
+ /** Formats a missing-capture origin as protocol://hostname:port. */
24
+ export const formatMissingCaptureOrigin = (protocol, hostname, port) => `${protocol}://${hostname}:${port}`;
25
+ /** Dedupe key for one missing-capture report per protocol://hostname:port. */
26
+ export const missingCaptureDedupeKey = (protocol, hostname, port) => {
27
+ const group = loopbackGroup(hostname);
28
+ const hostKey = group === "__loopback__" ? group : normalizeLoopbackHostname(hostname);
29
+ return `${protocol}://${hostKey}:${port}`;
30
+ };
31
+ /** Returns true when the request targets the internal events route. */
32
+ export const isLocalPreviewEventsPath = (requestPath) => {
33
+ const pathname = requestPath.split("?")[0] ?? requestPath;
34
+ return pathname === LOCALPREVIEW_EVENTS_PATH;
35
+ };
36
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
37
+ const normalizeHttpMethod = (value) => {
38
+ if (typeof value !== "string") {
39
+ return undefined;
40
+ }
41
+ const method = value.trim().toUpperCase();
42
+ return /^[!#$%&'*+\-.^_`|~0-9A-Z]+$/.test(method) ? method : undefined;
43
+ };
44
+ /** Strictly validates a parsed missing-capture event body. */
45
+ export const parseMissingCaptureEvent = (body) => {
46
+ if (!isRecord(body) || body.type !== "missing-capture" || !isRecord(body.payload)) {
47
+ return undefined;
48
+ }
49
+ const { payload } = body;
50
+ if (typeof payload.protocol !== "string" ||
51
+ typeof payload.hostname !== "string" ||
52
+ typeof payload.port !== "number" ||
53
+ typeof payload.transport !== "string") {
54
+ return undefined;
55
+ }
56
+ if (!isMissingCaptureProtocol(payload.protocol) ||
57
+ !isMissingCaptureTransport(payload.transport) ||
58
+ !Number.isInteger(payload.port) ||
59
+ payload.port < 1 ||
60
+ payload.port > 65_535 ||
61
+ !isLoopbackHost(payload.hostname)) {
62
+ return undefined;
63
+ }
64
+ const method = payload.method === undefined ? undefined : normalizeHttpMethod(payload.method);
65
+ if (payload.method !== undefined && method === undefined) {
66
+ return undefined;
67
+ }
68
+ return {
69
+ type: "missing-capture",
70
+ payload: {
71
+ hostname: normalizeLoopbackHostname(payload.hostname),
72
+ ...(method === undefined ? {} : { method }),
73
+ port: payload.port,
74
+ protocol: payload.protocol,
75
+ transport: payload.transport,
76
+ },
77
+ };
78
+ };
79
+ const formatCaptureFlagValue = (hostname, port) => hostname.includes(":") ? `[${hostname}]:${port}` : `${hostname}:${port}`;
80
+ const captureFlagExists = (argv, captures, hostname, port) => {
81
+ if (isCapturedEndpoint(captures, hostname, port)) {
82
+ return true;
83
+ }
84
+ for (let index = 0; index < argv.length; index += 1) {
85
+ const arg = argv[index];
86
+ if (arg !== "--capture") {
87
+ continue;
88
+ }
89
+ const value = argv[index + 1];
90
+ if (value === undefined) {
91
+ continue;
92
+ }
93
+ const separator = value.lastIndexOf(":");
94
+ if (separator <= 0) {
95
+ continue;
96
+ }
97
+ const captureHost = value.startsWith("[")
98
+ ? value.slice(0, value.indexOf("]") + 1)
99
+ : value.slice(0, separator);
100
+ const capturePort = Number(value.slice(separator + 1));
101
+ if (sameLoopbackEndpoint(captureHost, capturePort, hostname, port)) {
102
+ return true;
103
+ }
104
+ }
105
+ return false;
106
+ };
107
+ /** Builds a restart command preserving the original argv and appending --capture. */
108
+ export const buildSuggestedCaptureCommand = (originalArgv, legacy, captures, hostname, port) => {
109
+ const nextArgv = [...originalArgv];
110
+ if (!captureFlagExists(nextArgv, captures, hostname, port)) {
111
+ nextArgv.push("--capture", formatCaptureFlagValue(hostname, port));
112
+ }
113
+ const commandPrefix = legacy ? "localpreview" : "localpreview connect";
114
+ return `${commandPrefix} ${nextArgv.join(" ")}`.trim();
115
+ };
116
+ /** Returns true when a missing-capture event should produce a CLI warning. */
117
+ export const shouldReportMissingCapture = (event, primaryTarget, captures) => {
118
+ const { hostname, port } = event.payload;
119
+ if (isPrimaryTargetPort(primaryTarget, hostname, port)) {
120
+ return false;
121
+ }
122
+ if (isCapturedEndpoint(captures, hostname, port)) {
123
+ return false;
124
+ }
125
+ return true;
126
+ };
127
+ /** Formats a capture target for inclusion in warning text. */
128
+ export const formatMissingCaptureTarget = (capture) => formatCaptureOrigin(capture);
@@ -4,12 +4,16 @@ import { CliConfig } from "./config.js";
4
4
  import { RelayConnectionError, RelayProtocolError } from "./errors.js";
5
5
  import { LocalProxy } from "./local-proxy.js";
6
6
  export type RelayClientShape = {
7
- readonly connectAndServe: (tunnel: CreateTunnelResponse, target: TunnelTarget, captures: ReadonlyArray<CaptureTarget>) => Effect.Effect<void, RelayConnectionError | RelayProtocolError>;
7
+ readonly connectAndServe: (tunnel: CreateTunnelResponse, target: TunnelTarget, captures: ReadonlyArray<CaptureTarget>, sessionMetadata: RelaySessionMetadata) => Effect.Effect<void, RelayConnectionError | RelayProtocolError>;
8
+ };
9
+ export type RelaySessionMetadata = {
10
+ readonly legacy: boolean;
11
+ readonly originalArgv: ReadonlyArray<string>;
8
12
  };
9
13
  declare const RelayClient_base: Context.ServiceClass<RelayClient, "RelayClient", RelayClientShape>;
10
14
  export declare class RelayClient extends RelayClient_base {
11
15
  }
12
- export declare const RelayClientLive: Layer.Layer<RelayClient, never, CliConfig | LocalProxy>;
16
+ export declare const RelayClientLive: Layer.Layer<RelayClient, never, LocalProxy | CliConfig>;
13
17
  export declare const isRetryableCloseCode: (code: number) => boolean;
14
18
  export {};
15
19
  //# sourceMappingURL=relay-client.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"relay-client.d.ts","sourceRoot":"","sources":["../src/relay-client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,oBAAoB,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAChG,OAAO,EAAW,OAAO,EAAY,MAAM,EAAE,KAAK,EAAY,MAAM,QAAQ,CAAC;AAE7E,OAAO,EAAE,SAAS,EAAuB,MAAM,aAAa,CAAC;AAC7D,OAAO,EAAE,oBAAoB,EAAE,kBAAkB,EAAgB,MAAM,aAAa,CAAC;AACrF,OAAO,EAAE,UAAU,EAAwB,MAAM,kBAAkB,CAAC;AAEpE,MAAM,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,CAAC,eAAe,EAAE,CACxB,MAAM,EAAE,oBAAoB,EAC5B,MAAM,EAAE,YAAY,EACpB,QAAQ,EAAE,aAAa,CAAC,aAAa,CAAC,KACnC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,oBAAoB,GAAG,kBAAkB,CAAC,CAAC;CACrE,CAAC;;AAEF,qBAAa,WAAY,SAAQ,gBAA+D;CAAG;AAEnG,eAAO,MAAM,eAAe,yDAU3B,CAAC;AA6LF,eAAO,MAAM,oBAAoB,GAAI,MAAM,MAAM,KAAG,OAAwB,CAAC"}
1
+ {"version":3,"file":"relay-client.d.ts","sourceRoot":"","sources":["../src/relay-client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,oBAAoB,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAChG,OAAO,EAAW,OAAO,EAAY,MAAM,EAAE,KAAK,EAAY,MAAM,QAAQ,CAAC;AAG7E,OAAO,EAAE,SAAS,EAAuB,MAAM,aAAa,CAAC;AAC7D,OAAO,EAAE,oBAAoB,EAAE,kBAAkB,EAAgB,MAAM,aAAa,CAAC;AACrF,OAAO,EAAE,UAAU,EAAwB,MAAM,kBAAkB,CAAC;AAEpE,MAAM,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,CAAC,eAAe,EAAE,CACxB,MAAM,EAAE,oBAAoB,EAC5B,MAAM,EAAE,YAAY,EACpB,QAAQ,EAAE,aAAa,CAAC,aAAa,CAAC,EACtC,eAAe,EAAE,oBAAoB,KAClC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,oBAAoB,GAAG,kBAAkB,CAAC,CAAC;CACrE,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,YAAY,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;CAC9C,CAAC;;AAEF,qBAAa,WAAY,SAAQ,gBAA+D;CAAG;AAEnG,eAAO,MAAM,eAAe,yDAU3B,CAAC;AAwMF,eAAO,MAAM,oBAAoB,GAAI,MAAM,MAAM,KAAG,OAAwB,CAAC"}
@@ -1,5 +1,6 @@
1
1
  import { Console, Context, Deferred, Effect, Layer, Schedule } from "effect";
2
2
  import WebSocket from "ws";
3
+ import { formatRelayReconnect, formatStatus } from "./cli-ui.js";
3
4
  import { CliConfig } from "./config.js";
4
5
  import { RelayConnectionError, RelayProtocolError, errorMessage } from "./errors.js";
5
6
  import { LocalProxy } from "./local-proxy.js";
@@ -9,22 +10,28 @@ export const RelayClientLive = Layer.effect(RelayClient)(Effect.gen(function* ()
9
10
  const config = yield* CliConfig;
10
11
  const localProxy = yield* LocalProxy;
11
12
  return {
12
- connectAndServe: (tunnel, target, captures) => connectAndServe(config, localProxy, tunnel, target, captures),
13
+ connectAndServe: (tunnel, target, captures, sessionMetadata) => connectAndServe(config, localProxy, tunnel, target, captures, sessionMetadata),
13
14
  };
14
15
  }));
15
- const connectAndServe = (config, localProxy, tunnel, target, captures) => serveWithReconnect(config, localProxy, tunnel, target, captures);
16
- const serveWithReconnect = (config, localProxy, tunnel, target, captures) => serveOnce(config, localProxy, tunnel, target, captures).pipe(Effect.catch((error) => {
16
+ const connectAndServe = (config, localProxy, tunnel, target, captures, sessionMetadata) => serveWithReconnect(config, localProxy, tunnel, target, captures, sessionMetadata);
17
+ const serveWithReconnect = (config, localProxy, tunnel, target, captures, sessionMetadata) => serveOnce(config, localProxy, tunnel, target, captures, sessionMetadata).pipe(Effect.catch((error) => {
17
18
  if (error instanceof RelayConnectionError && error.retryable === true) {
18
- return Console.error(`${error.message}; reconnecting...`).pipe(Effect.andThen(Effect.sleep("1 second")), Effect.andThen(serveWithReconnect(config, localProxy, tunnel, target, captures)));
19
+ return Console.error(formatRelayReconnect(error.message)).pipe(Effect.andThen(Effect.sleep("1 second")), Effect.andThen(serveWithReconnect(config, localProxy, tunnel, target, captures, sessionMetadata)));
19
20
  }
20
21
  return Effect.fail(error);
21
22
  }));
22
- const serveOnce = (config, localProxy, tunnel, target, captures) => Effect.gen(function* () {
23
+ const serveOnce = (config, localProxy, tunnel, target, captures, sessionMetadata) => Effect.gen(function* () {
23
24
  const socket = yield* openSocket(tunnel).pipe(Effect.retry({
24
25
  schedule: Schedule.recurs(Math.ceil(config.relayConnectTimeoutMs / 250)),
25
26
  }));
26
- yield* Console.log("Connected to relay.");
27
- const session = { captures, target };
27
+ yield* Console.log(formatStatus("Connected to relay."));
28
+ const session = {
29
+ captures,
30
+ legacy: sessionMetadata.legacy,
31
+ originalArgv: sessionMetadata.originalArgv,
32
+ reportedMissingCaptures: new Set(),
33
+ target,
34
+ };
28
35
  const done = yield* Deferred.make();
29
36
  const handleSignal = () => {
30
37
  socket.close(1000, "Interrupted");
@@ -0,0 +1,2 @@
1
+ export declare const CLI_PACKAGE_VERSION: string;
2
+ //# sourceMappingURL=version.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,mBAAmB,QAAsB,CAAC"}
@@ -0,0 +1,2 @@
1
+ import packageJson from "../package.json" with { type: "json" };
2
+ export const CLI_PACKAGE_VERSION = packageJson.version;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "localpreview",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "bin": {
5
5
  "localpreview": "./dist/index.js"
6
6
  },
@@ -21,8 +21,9 @@
21
21
  },
22
22
  "dependencies": {
23
23
  "@effect/platform-node": "beta",
24
- "@localpreview/protocol": "^0.2.1",
24
+ "@localpreview/protocol": "workspace:*",
25
25
  "effect": "beta",
26
+ "picocolors": "^1.1.1",
26
27
  "ws": "latest"
27
28
  },
28
29
  "devDependencies": {