localpreview 0.2.4 → 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.
@@ -26,6 +26,7 @@ export type ResolvedRoute = {
26
26
  readonly kind: "capture";
27
27
  readonly target: TunnelTarget;
28
28
  } | {
29
+ readonly capturePath: ParsedCapturePath;
29
30
  readonly kind: "invalid-capture";
30
31
  readonly message: string;
31
32
  } | {
@@ -1 +1 @@
1
- {"version":3,"file":"capture-route.d.ts","sourceRoot":"","sources":["../src/capture-route.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,aAAa,EAClB,KAAK,YAAY,EAClB,MAAM,wBAAwB,CAAC;AAEhC,eAAO,MAAM,mBAAmB,4BAA4B,CAAC;AAE7D,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAC,UAAU,CAAC,CAAC;CAC7C,CAAC;AAEF,KAAK,sBAAsB,GACvB;IACE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAClB,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAC;CACpC,GACD;IACE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;CACpB,CAAC;AAEN;;;GAGG;AACH,eAAO,MAAM,gBAAgB,GAAI,aAAa,MAAM,KAAG,sBA+CtD,CAAC;AAEF,oEAAoE;AACpE,eAAO,MAAM,4BAA4B,GAAI,QAAQ,iBAAiB,KAAG,MAC6B,CAAC;AAEvG,2DAA2D;AAC3D,eAAO,MAAM,qBAAqB,GAAI,UAAU,MAAM,KAAG,MACA,CAAC;AAE1D,MAAM,MAAM,aAAa,GACrB;IACE,QAAQ,CAAC,WAAW,EAAE,iBAAiB,CAAC;IACxC,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;CAC/B,GACD;IACE,QAAQ,CAAC,IAAI,EAAE,iBAAiB,CAAC;IACjC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B,GACD;IACE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;CAC/B,CAAC;AAEN,qFAAqF;AACrF,eAAO,MAAM,YAAY,GACvB,aAAa,MAAM,EACnB,eAAe,YAAY,EAC3B,UAAU,aAAa,CAAC,aAAa,CAAC,KACrC,aAyBF,CAAC"}
1
+ {"version":3,"file":"capture-route.d.ts","sourceRoot":"","sources":["../src/capture-route.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,aAAa,EAClB,KAAK,YAAY,EAClB,MAAM,wBAAwB,CAAC;AAEhC,eAAO,MAAM,mBAAmB,4BAA4B,CAAC;AAE7D,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAC,UAAU,CAAC,CAAC;CAC7C,CAAC;AAEF,KAAK,sBAAsB,GACvB;IACE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAClB,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAC;CACpC,GACD;IACE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;CACpB,CAAC;AAEN;;;GAGG;AACH,eAAO,MAAM,gBAAgB,GAAI,aAAa,MAAM,KAAG,sBA+CtD,CAAC;AAEF,oEAAoE;AACpE,eAAO,MAAM,4BAA4B,GAAI,QAAQ,iBAAiB,KAAG,MAC6B,CAAC;AAEvG,2DAA2D;AAC3D,eAAO,MAAM,qBAAqB,GAAI,UAAU,MAAM,KAAG,MACA,CAAC;AAE1D,MAAM,MAAM,aAAa,GACrB;IACE,QAAQ,CAAC,WAAW,EAAE,iBAAiB,CAAC;IACxC,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;CAC/B,GACD;IACE,QAAQ,CAAC,WAAW,EAAE,iBAAiB,CAAC;IACxC,QAAQ,CAAC,IAAI,EAAE,iBAAiB,CAAC;IACjC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B,GACD;IACE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;CAC/B,CAAC;AAEN,qFAAqF;AACrF,eAAO,MAAM,YAAY,GACvB,aAAa,MAAM,EACnB,eAAe,YAAY,EAC3B,UAAU,aAAa,CAAC,aAAa,CAAC,KACrC,aA0BF,CAAC"}
@@ -59,6 +59,7 @@ export const resolveRoute = (requestPath, primaryTarget, captures) => {
59
59
  const capture = findCapture(captures, parsed.hostname, parsed.port);
60
60
  if (capture === undefined) {
61
61
  return {
62
+ capturePath: parsed,
62
63
  kind: "invalid-capture",
63
64
  message: `Capture route is not allowlisted: ${parsed.hostname}:${parsed.port}.`,
64
65
  };
@@ -1,8 +1,12 @@
1
1
  import type { CaptureTarget } from "@localpreview/protocol";
2
2
  import { encodeCaptureHostname } from "./capture-route.js";
3
3
  export { encodeCaptureHostname };
4
+ type CaptureShimPrimaryTarget = {
5
+ readonly hostname: string;
6
+ readonly port: number;
7
+ };
4
8
  /** Generates the inline browser shim injected into proxied HTML responses. */
5
- export declare const buildCaptureShimScript: (captures: ReadonlyArray<CaptureTarget>) => string;
9
+ export declare const buildCaptureShimScript: (primary: CaptureShimPrimaryTarget, captures: ReadonlyArray<CaptureTarget>) => string;
6
10
  /** Injects the capture shim into an HTML document string. */
7
11
  export declare const injectCaptureShim: (html: string, shimScript: string) => string;
8
12
  /** Returns true when the response looks like HTML suitable for shim injection. */
@@ -1 +1 @@
1
- {"version":3,"file":"capture-shim.d.ts","sourceRoot":"","sources":["../src/capture-shim.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAC5D,OAAO,EAAuB,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAEhF,OAAO,EAAE,qBAAqB,EAAE,CAAC;AAEjC,8EAA8E;AAC9E,eAAO,MAAM,sBAAsB,GAAI,UAAU,aAAa,CAAC,aAAa,CAAC,KAAG,MAU/E,CAAC;AAEF,6DAA6D;AAC7D,eAAO,MAAM,iBAAiB,GAAI,MAAM,MAAM,EAAE,YAAY,MAAM,KAAG,MAcpE,CAAC;AAEF,kFAAkF;AAClF,eAAO,MAAM,cAAc,GAAI,aAAa,MAAM,GAAG,IAAI,KAAG,OACa,CAAC;AAE1E,6DAA6D;AAC7D,eAAO,MAAM,0BAA0B,GACrC,SAAS,aAAa,CAAC,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,KAChD,KAAK,CAAC,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAI9B,CAAC;AAEL,qFAAqF;AACrF,eAAO,MAAM,mBAAmB,GAAI,QAAQ;IAC1C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;CACrC,KAAG,MAAkE,CAAC"}
1
+ {"version":3,"file":"capture-shim.d.ts","sourceRoot":"","sources":["../src/capture-shim.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAC5D,OAAO,EAAuB,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAEhF,OAAO,EAAE,qBAAqB,EAAE,CAAC;AAEjC,KAAK,wBAAwB,GAAG;IAC9B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,8EAA8E;AAC9E,eAAO,MAAM,sBAAsB,GACjC,SAAS,wBAAwB,EACjC,UAAU,aAAa,CAAC,aAAa,CAAC,KACrC,MAWF,CAAC;AAEF,6DAA6D;AAC7D,eAAO,MAAM,iBAAiB,GAAI,MAAM,MAAM,EAAE,YAAY,MAAM,KAAG,MAcpE,CAAC;AAEF,kFAAkF;AAClF,eAAO,MAAM,cAAc,GAAI,aAAa,MAAM,GAAG,IAAI,KAAG,OACa,CAAC;AAE1E,6DAA6D;AAC7D,eAAO,MAAM,0BAA0B,GACrC,SAAS,aAAa,CAAC,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,KAChD,KAAK,CAAC,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAI9B,CAAC;AAEL,qFAAqF;AACrF,eAAO,MAAM,mBAAmB,GAAI,QAAQ;IAC1C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;CACrC,KAAG,MAAkE,CAAC"}
@@ -1,15 +1,16 @@
1
1
  import { CAPTURE_PATH_PREFIX, encodeCaptureHostname } from "./capture-route.js";
2
2
  export { encodeCaptureHostname };
3
3
  /** Generates the inline browser shim injected into proxied HTML responses. */
4
- export const buildCaptureShimScript = (captures) => {
4
+ export const buildCaptureShimScript = (primary, captures) => {
5
5
  const config = JSON.stringify({
6
6
  captures: captures.map((capture) => ({
7
7
  hostname: capture.hostname,
8
8
  port: capture.port,
9
9
  })),
10
10
  prefix: CAPTURE_PATH_PREFIX,
11
+ primary,
11
12
  });
12
- return `(function(){var cfg=${config};var caps=cfg.captures;var prefix=cfg.prefix;function normHost(h){return h==="::1"?"[::1]":h.toLowerCase();}function matchesCapture(url){var host=normHost(url.hostname);var port=url.port?Number(url.port):(url.protocol==="https:"?443:80);for(var i=0;i<caps.length;i++){var cap=caps[i];if(normHost(cap.hostname)===host&&cap.port===port){return true;}}return false;}function rewriteUrl(input){var url=new URL(input,window.location.href);if(!matchesCapture(url)){return input;}var proto=url.protocol.replace(":","");var host=encodeURIComponent(normHost(url.hostname));var path=url.pathname+url.search;return prefix+"/"+proto+"/"+host+"/"+url.port+path;}function warnUnsupported(api,url){console.error("[localpreview] "+api+" to captured local backend is not supported in v1: "+url+". Use fetch/XHR or run localpreview connect for that service directly.");}var origFetch=window.fetch;window.fetch=function(input,init){try{var url=typeof input==="string"?input:input instanceof URL?input.href:input.url;if(typeof url==="string"&&/^https?:\\/\\//.test(url)){var rewritten=rewriteUrl(url);if(rewritten!==url){return origFetch.call(this,rewritten,init);}}}catch(e){}return origFetch.call(this,input,init);};if(typeof XMLHttpRequest!=="undefined"){var origOpen=XMLHttpRequest.prototype.open;XMLHttpRequest.prototype.open=function(method,url){try{if(typeof url==="string"&&/^https?:\\/\\//.test(url)){var rewritten=rewriteUrl(url);if(rewritten!==url){return origOpen.call(this,method,rewritten);}}}catch(e){}return origOpen.apply(this,arguments);};}if(typeof window.WebSocket!=="undefined"){var OrigWebSocket=window.WebSocket;window.WebSocket=function(url,protocols){try{var parsed=new URL(url,window.location.href);if(matchesCapture(parsed)){warnUnsupported("WebSocket",url);throw new Error("LocalPreview does not proxy WebSocket connections to captured backends in v1.");}}catch(e){if(e instanceof Error&&e.message.indexOf("LocalPreview")!==-1){throw e;}}return protocols===undefined?new OrigWebSocket(url):new OrigWebSocket(url,protocols);};if(OrigWebSocket.prototype){window.WebSocket.prototype=OrigWebSocket.prototype;}}if(typeof window.EventSource!=="undefined"){var OrigEventSource=window.EventSource;window.EventSource=function(url,init){try{var parsed=new URL(url,window.location.href);if(matchesCapture(parsed)){warnUnsupported("EventSource",url);throw new Error("LocalPreview does not proxy EventSource connections to captured backends in v1.");}}catch(e){if(e instanceof Error&&e.message.indexOf("LocalPreview")!==-1){throw e;}}return init===undefined?new OrigEventSource(url):new OrigEventSource(url,init);};if(OrigEventSource.prototype){window.EventSource.prototype=OrigEventSource.prototype;}}})();`;
13
+ return `(function(){var cfg=${config};var caps=cfg.captures;var primary=cfg.primary;var prefix=cfg.prefix;var eventsPath="/__localpreview/events";var reported={};function normHost(h){return h==="::1"?"[::1]":h.toLowerCase();}function isLoopbackHost(h){var lower=normHost(h);if(lower==="localhost"||lower==="127.0.0.1"||lower==="[::1]")return true;return lower.endsWith(".localhost");}function loopbackGroup(h){var lower=normHost(h);if(lower==="localhost"||lower==="127.0.0.1"||lower==="[::1]")return "__loopback__";return lower;}function sameLoopbackPort(host,port,otherHost,otherPort){return port===otherPort&&loopbackGroup(host)===loopbackGroup(otherHost);}function dedupeKey(proto,host,port){var group=loopbackGroup(host);return proto+"://"+(group==="__loopback__"?group:normHost(host))+":"+port;}function urlPort(url){return url.port?Number(url.port):(url.protocol==="https:"||url.protocol==="wss:"?443:80);}function matchesCapture(url){var host=normHost(url.hostname);var port=urlPort(url);for(var i=0;i<caps.length;i++){var cap=caps[i];if(sameLoopbackPort(host,port,cap.hostname,cap.port))return true;}return false;}function isPrimaryPort(url){return sameLoopbackPort(url.hostname,urlPort(url),primary.hostname,primary.port);}function isAllowlisted(url){return matchesCapture(url)||isPrimaryPort(url);}function reportMissing(url,transport,method){try{if(!isLoopbackHost(url.hostname))return;if(isAllowlisted(url))return;var proto=url.protocol.replace(":","");if(proto!=="http"&&proto!=="https"&&proto!=="ws"&&proto!=="wss")return;var host=normHost(url.hostname);var port=urlPort(url);var key=dedupeKey(proto,host,port);if(reported[key])return;reported[key]=true;var payload={protocol:proto,hostname:host,port:port,transport:transport};if(typeof method==="string"&&method){payload.method=method;}var body=JSON.stringify({type:"missing-capture",payload:payload});if(typeof navigator!=="undefined"&&navigator.sendBeacon){navigator.sendBeacon(eventsPath,body);}else{fetch(eventsPath,{method:"POST",body:body,keepalive:true});}}catch(e){}}function rewriteUrl(input){var url=new URL(input,window.location.href);if(!matchesCapture(url)){return input;}var proto=url.protocol.replace(":","");var host=encodeURIComponent(normHost(url.hostname));var path=url.pathname+url.search;return prefix+"/"+proto+"/"+host+"/"+url.port+path;}function warnUnsupported(api,url){console.error("[localpreview] "+api+" to captured local backend is not supported in v1: "+url+". Use fetch/XHR or run localpreview connect for that service directly.");}function fetchMethod(input,init){if(init&&typeof init==="object"&&typeof init.method==="string")return init.method.toUpperCase();if(typeof Request!=="undefined"&&input instanceof Request)return input.method.toUpperCase();return "GET";}var origFetch=window.fetch;window.fetch=function(input,init){try{var url=typeof input==="string"?input:input instanceof URL?input.href:input.url;if(typeof url==="string"&&/^https?:\\/\\//.test(url)){var parsed=new URL(url,window.location.href);reportMissing(parsed,"fetch",fetchMethod(input,init));var rewritten=rewriteUrl(url);if(rewritten!==url){return origFetch.call(this,rewritten,init);}}}catch(e){}return origFetch.call(this,input,init);};if(typeof XMLHttpRequest!=="undefined"){var origOpen=XMLHttpRequest.prototype.open;XMLHttpRequest.prototype.open=function(method,url){try{if(typeof url==="string"&&/^https?:\\/\\//.test(url)){var parsed=new URL(url,window.location.href);reportMissing(parsed,"xhr",typeof method==="string"?method.toUpperCase():undefined);var rewritten=rewriteUrl(url);if(rewritten!==url){return origOpen.call(this,method,rewritten);}}}catch(e){}return origOpen.apply(this,arguments);};}if(typeof window.WebSocket!=="undefined"){var OrigWebSocket=window.WebSocket;window.WebSocket=function(url,protocols){try{var parsed=new URL(url,window.location.href);reportMissing(parsed,"websocket");if(matchesCapture(parsed)){warnUnsupported("WebSocket",url);throw new Error("LocalPreview does not proxy WebSocket connections to captured backends in v1.");}}catch(e){if(e instanceof Error&&e.message.indexOf("LocalPreview")!==-1){throw e;}}return protocols===undefined?new OrigWebSocket(url):new OrigWebSocket(url,protocols);};if(OrigWebSocket.prototype){window.WebSocket.prototype=OrigWebSocket.prototype;}}if(typeof window.EventSource!=="undefined"){var OrigEventSource=window.EventSource;window.EventSource=function(url,init){try{var parsed=new URL(url,window.location.href);reportMissing(parsed,"eventsource");if(matchesCapture(parsed)){warnUnsupported("EventSource",url);throw new Error("LocalPreview does not proxy EventSource connections to captured backends in v1.");}}catch(e){if(e instanceof Error&&e.message.indexOf("LocalPreview")!==-1){throw e;}}return init===undefined?new OrigEventSource(url):new OrigEventSource(url,init);};if(OrigEventSource.prototype){window.EventSource.prototype=OrigEventSource.prototype;}}})();`;
13
14
  };
14
15
  /** Injects the capture shim into an HTML document string. */
15
16
  export const injectCaptureShim = (html, shimScript) => {
package/dist/cli-ui.d.ts CHANGED
@@ -6,8 +6,22 @@ export declare const formatCliError: (message: string) => string;
6
6
  export declare const formatStatus: (message: string) => string;
7
7
  export declare const formatRelayReconnect: (message: string) => string;
8
8
  export declare const formatRequestOutbound: (method: string, path: string) => string;
9
+ export declare const formatCapturedRequestOutbound: (method: string, targetUrl: string) => string;
9
10
  export declare const formatRequestInbound: (status: number, path: string, durationMs: number) => string;
11
+ export declare const formatCapturedRequestInbound: (status: number, targetUrl: string, durationMs: number) => string;
12
+ export declare const formatMissingCaptureWarning: (options: {
13
+ readonly origin: string;
14
+ readonly method?: string;
15
+ readonly suggestedCommand: string;
16
+ readonly transport?: string;
17
+ }) => string;
10
18
  export declare const formatRequestError: (path: string, message: string) => string;
19
+ export declare const formatInvalidCaptureRouteError: (options: {
20
+ readonly hostname: string;
21
+ readonly method: string;
22
+ readonly path: string;
23
+ readonly port: number;
24
+ }) => string;
11
25
  export declare const removedAdminTokenFlagMessage: () => string;
12
26
  export declare const adminTokenRequiredMessage: () => string;
13
27
  export type HelpRenderOptions = {
@@ -1 +1 @@
1
- {"version":3,"file":"cli-ui.d.ts","sourceRoot":"","sources":["../src/cli-ui.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,mBAAmB,EAAkB,MAAM,wBAAwB,CAAC;AAIlF,eAAO,MAAM,eAAe,6BAA6B,CAAC;AAE1D,eAAO,MAAM,kBAAkB,GAAI,MAAK,MAAM,CAAC,UAAwB,KAAG,OAGzE,CAAC;AAEF,eAAO,MAAM,aAAa,GAAI,SAAS,MAAM,KAAG,MAQ/C,CAAC;AAEF,eAAO,MAAM,cAAc,GAAI,SAAS,MAAM,KAAG,MAQhD,CAAC;AAEF,eAAO,MAAM,YAAY,GAAI,SAAS,MAAM,KAAG,MAM9C,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAAI,SAAS,MAAM,KAAG,MACT,CAAC;AAE/C,eAAO,MAAM,qBAAqB,GAAI,QAAQ,MAAM,EAAE,MAAM,MAAM,KAAG,MASpE,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAC/B,QAAQ,MAAM,EACd,MAAM,MAAM,EACZ,YAAY,MAAM,KACjB,MAWF,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAAI,MAAM,MAAM,EAAE,SAAS,MAAM,KAAG,MASlE,CAAC;AAKF,eAAO,MAAM,4BAA4B,QAAO,MAA0C,CAAC;AAE3F,eAAO,MAAM,yBAAyB,QAAO,MACkD,CAAC;AAEhG,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CAClC,CAAC;AAiDF,eAAO,MAAM,gBAAgB,GAAI,UAAS,iBAAsB,KAAG,MASlE,CAAC;AAEF,eAAO,MAAM,iBAAiB,QAAO,MAuBjC,CAAC;AAEL,eAAO,MAAM,eAAe,GAAI,UAAS,iBAAsB,KAAG,MAmCjE,CAAC;AAEF,eAAO,MAAM,cAAc,GAAI,UAAS,iBAAsB,KAAG,MAiChE,CAAC;AAaF,eAAO,MAAM,uBAAuB,GAAI,UAAU,mBAAmB,KAAG,MA0DvE,CAAC;AAwCF,eAAO,MAAM,gBAAgB,GAAI,aAAa,MAAM,GAAG,SAAS,KAAG,MA0BlE,CAAC"}
1
+ {"version":3,"file":"cli-ui.d.ts","sourceRoot":"","sources":["../src/cli-ui.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,mBAAmB,EAAkB,MAAM,wBAAwB,CAAC;AAIlF,eAAO,MAAM,eAAe,6BAA6B,CAAC;AAE1D,eAAO,MAAM,kBAAkB,GAAI,MAAK,MAAM,CAAC,UAAwB,KAAG,OAGzE,CAAC;AAEF,eAAO,MAAM,aAAa,GAAI,SAAS,MAAM,KAAG,MAQ/C,CAAC;AAEF,eAAO,MAAM,cAAc,GAAI,SAAS,MAAM,KAAG,MAQhD,CAAC;AAEF,eAAO,MAAM,YAAY,GAAI,SAAS,MAAM,KAAG,MAM9C,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAAI,SAAS,MAAM,KAAG,MACT,CAAC;AAE/C,eAAO,MAAM,qBAAqB,GAAI,QAAQ,MAAM,EAAE,MAAM,MAAM,KAAG,MASpE,CAAC;AAEF,eAAO,MAAM,6BAA6B,GAAI,QAAQ,MAAM,EAAE,WAAW,MAAM,KAAG,MAUjF,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAAI,QAAQ,MAAM,EAAE,MAAM,MAAM,EAAE,YAAY,MAAM,KAAG,MAgBvF,CAAC;AAEF,eAAO,MAAM,4BAA4B,GACvC,QAAQ,MAAM,EACd,WAAW,MAAM,EACjB,YAAY,MAAM,KACjB,MAiBF,CAAC;AAIF,eAAO,MAAM,2BAA2B,GAAI,SAAS;IACnD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B,KAAG,MAiBH,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAAI,MAAM,MAAM,EAAE,SAAS,MAAM,KAAG,MASlE,CAAC;AAEF,eAAO,MAAM,8BAA8B,GAAI,SAAS;IACtD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB,KAAG,MAaH,CAAC;AAKF,eAAO,MAAM,4BAA4B,QAAO,MAA0C,CAAC;AAE3F,eAAO,MAAM,yBAAyB,QAAO,MACkD,CAAC;AAEhG,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CAClC,CAAC;AAiDF,eAAO,MAAM,gBAAgB,GAAI,UAAS,iBAAsB,KAAG,MASlE,CAAC;AAEF,eAAO,MAAM,iBAAiB,QAAO,MAuBjC,CAAC;AAEL,eAAO,MAAM,eAAe,GAAI,UAAS,iBAAsB,KAAG,MA+BjE,CAAC;AAEF,eAAO,MAAM,cAAc,GAAI,UAAS,iBAAsB,KAAG,MA+BhE,CAAC;AAaF,eAAO,MAAM,uBAAuB,GAAI,UAAU,mBAAmB,KAAG,MAkEvE,CAAC;AAwCF,eAAO,MAAM,gBAAgB,GAAI,aAAa,MAAM,GAAG,SAAS,KAAG,MA0BlE,CAAC"}
package/dist/cli-ui.js CHANGED
@@ -35,15 +35,58 @@ export const formatRequestOutbound = (method, path) => {
35
35
  }
36
36
  return `${pc.dim(arrow)} ${pc.bold(method)} ${path}`;
37
37
  };
38
+ export const formatCapturedRequestOutbound = (method, targetUrl) => {
39
+ const arrow = "→";
40
+ const label = "CAPTURED";
41
+ const line = `${arrow} ${label} ${method} ${targetUrl}`;
42
+ if (!pc.isColorSupported) {
43
+ return line;
44
+ }
45
+ return `${pc.dim(arrow)} ${formatCaptureBadge(label)} ${pc.bold(method)} ${targetUrl}`;
46
+ };
38
47
  export const formatRequestInbound = (status, path, durationMs) => {
39
48
  const arrow = "←";
40
49
  const line = `${arrow} ${status} ${path} ${durationMs}ms`;
41
50
  if (!pc.isColorSupported) {
42
51
  return line;
43
52
  }
44
- const statusText = status >= 500 ? pc.red(String(status)) : status >= 400 ? pc.yellow(String(status)) : pc.green(String(status));
53
+ const statusText = status >= 500
54
+ ? pc.red(String(status))
55
+ : status >= 400
56
+ ? pc.yellow(String(status))
57
+ : pc.green(String(status));
45
58
  return `${pc.dim(arrow)} ${statusText} ${path} ${pc.dim(`${durationMs}ms`)}`;
46
59
  };
60
+ export const formatCapturedRequestInbound = (status, targetUrl, durationMs) => {
61
+ const arrow = "←";
62
+ const label = "CAPTURED";
63
+ const line = `${arrow} ${label} ${status} ${targetUrl} ${durationMs}ms`;
64
+ if (!pc.isColorSupported) {
65
+ return line;
66
+ }
67
+ const statusText = status >= 500
68
+ ? pc.red(String(status))
69
+ : status >= 400
70
+ ? pc.yellow(String(status))
71
+ : pc.green(String(status));
72
+ return `${pc.dim(arrow)} ${formatCaptureBadge(label)} ${statusText} ${targetUrl} ${pc.dim(`${durationMs}ms`)}`;
73
+ };
74
+ const formatCaptureBadge = (label) => pc.yellow(pc.dim(` ${label} `));
75
+ export const formatMissingCaptureWarning = (options) => {
76
+ const transportNote = options.transport === "websocket" || options.transport === "eventsource"
77
+ ? " WebSocket/EventSource may still fail in preview."
78
+ : "";
79
+ const arrow = "←";
80
+ const requestLabel = options.method ?? options.transport ?? "request";
81
+ const lines = [
82
+ `${arrow} ${requestLabel} ${options.origin}`,
83
+ ` Browser tried to reach uncaptured loopback backend ${options.origin}. Restart with: ${options.suggestedCommand}.${transportNote}`,
84
+ ];
85
+ if (!pc.isColorSupported) {
86
+ return lines.join("\n");
87
+ }
88
+ return lines.map((line) => pc.red(line)).join("\n");
89
+ };
47
90
  export const formatRequestError = (path, message) => {
48
91
  const arrow = "←";
49
92
  const line = `${arrow} error ${path} ${message}`;
@@ -52,6 +95,18 @@ export const formatRequestError = (path, message) => {
52
95
  }
53
96
  return `${pc.dim(arrow)} ${pc.red("error")} ${path} ${pc.red(message)}`;
54
97
  };
98
+ export const formatInvalidCaptureRouteError = (options) => {
99
+ const arrow = "←";
100
+ const origin = `${options.hostname}:${options.port}`;
101
+ const lines = [
102
+ `${arrow} ${options.method} ${options.path}`,
103
+ ` Browser tried to reach blocked loopback backend ${origin}. Allow it with --capture ${origin}.`,
104
+ ];
105
+ if (!pc.isColorSupported) {
106
+ return lines.join("\n");
107
+ }
108
+ return lines.map((line) => pc.red(line)).join("\n");
109
+ };
55
110
  const REMOVED_ADMIN_TOKEN_FLAG_MESSAGE = "The --admin-token flag was removed. Set the LOCALPREVIEW_ADMIN_TOKEN environment variable instead.";
56
111
  export const removedAdminTokenFlagMessage = () => REMOVED_ADMIN_TOKEN_FLAG_MESSAGE;
57
112
  export const adminTokenRequiredMessage = () => `Admin commands require ${ADMIN_TOKEN_ENV}. Export it in your shell, then rerun the command.`;
@@ -126,11 +181,7 @@ export const renderConnectHelp = () => joinLines([
126
181
  export const renderCleanHelp = (options = {}) => {
127
182
  const env = options.env ?? process.env;
128
183
  if (!hasAdminTokenInEnv(env)) {
129
- return joinLines([
130
- renderGlobalHelp(options),
131
- "",
132
- adminTokenRequiredMessage(),
133
- ]);
184
+ return joinLines([renderGlobalHelp(options), "", adminTokenRequiredMessage()]);
134
185
  }
135
186
  return joinLines([
136
187
  heading("localpreview clean — admin subdomain cleanup"),
@@ -160,11 +211,7 @@ export const renderCleanHelp = (options = {}) => {
160
211
  export const renderListHelp = (options = {}) => {
161
212
  const env = options.env ?? process.env;
162
213
  if (!hasAdminTokenInEnv(env)) {
163
- return joinLines([
164
- renderGlobalHelp(options),
165
- "",
166
- adminTokenRequiredMessage(),
167
- ]);
214
+ return joinLines([renderGlobalHelp(options), "", adminTokenRequiredMessage()]);
168
215
  }
169
216
  return joinLines([
170
217
  heading("localpreview list — inspect tunnel inventory"),
package/dist/command.d.ts CHANGED
@@ -2,4 +2,5 @@ import { Effect } from "effect";
2
2
  import { CliUsageError, type CliRuntimeError } from "./errors.js";
3
3
  export declare const runCli: (argv: ReadonlyArray<string>) => Effect.Effect<void, CliRuntimeError | CliUsageError>;
4
4
  export declare const normalizeCliArgs: (argv: ReadonlyArray<string>) => ReadonlyArray<string>;
5
+ export declare const buildConnectOriginalArgv: (argv: ReadonlyArray<string>, legacy: boolean) => ReadonlyArray<string>;
5
6
  //# sourceMappingURL=command.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"command.d.ts","sourceRoot":"","sources":["../src/command.ts"],"names":[],"mappings":"AAQA,OAAO,EAAW,MAAM,EAAiB,MAAM,QAAQ,CAAC;AAkBxD,OAAO,EAAE,aAAa,EAAgB,KAAK,eAAe,EAAE,MAAM,aAAa,CAAC;AAwChF,eAAO,MAAM,MAAM,GACjB,MAAM,aAAa,CAAC,MAAM,CAAC,KAC1B,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,eAAe,GAAG,aAAa,CAmBrD,CAAC;AAEF,eAAO,MAAM,gBAAgB,GAAI,MAAM,aAAa,CAAC,MAAM,CAAC,KAAG,aAAa,CAAC,MAAM,CAC1C,CAAC"}
1
+ {"version":3,"file":"command.d.ts","sourceRoot":"","sources":["../src/command.ts"],"names":[],"mappings":"AAQA,OAAO,EAAW,MAAM,EAAiB,MAAM,QAAQ,CAAC;AAkBxD,OAAO,EAAE,aAAa,EAAgB,KAAK,eAAe,EAAE,MAAM,aAAa,CAAC;AAwChF,eAAO,MAAM,MAAM,GACjB,MAAM,aAAa,CAAC,MAAM,CAAC,KAC1B,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,eAAe,GAAG,aAAa,CAmBrD,CAAC;AAEF,eAAO,MAAM,gBAAgB,GAAI,MAAM,aAAa,CAAC,MAAM,CAAC,KAAG,aAAa,CAAC,MAAM,CAC1C,CAAC;AAE1C,eAAO,MAAM,wBAAwB,GACnC,MAAM,aAAa,CAAC,MAAM,CAAC,EAC3B,QAAQ,OAAO,KACd,aAAa,CAAC,MAAM,CAAmE,CAAC"}
package/dist/command.js CHANGED
@@ -20,9 +20,10 @@ export const runCli = (argv) => {
20
20
  if (!parsed.ok) {
21
21
  return Effect.fail(new CliUsageError({ message: parsed.message }));
22
22
  }
23
- return runSubcommand(parsed.command);
23
+ return runSubcommand(parsed.command, normalized);
24
24
  };
25
25
  export const normalizeCliArgs = (argv) => argv[0] === "--" ? argv.slice(1) : argv;
26
+ export const buildConnectOriginalArgv = (argv, legacy) => (legacy ? argv : argv[0] === "connect" ? argv.slice(1) : argv);
26
27
  const hasHelpFlag = (argv) => argv.includes("--help") || argv.includes("-h");
27
28
  const resolveHelpKind = (argv) => {
28
29
  if (argv.length === 0) {
@@ -439,10 +440,10 @@ const readRequiredOptionValue = (argv, index, option) => {
439
440
  };
440
441
  };
441
442
  const toOption = (value) => value === undefined ? Option.none() : Option.some(value);
442
- const runSubcommand = (command) => {
443
+ const runSubcommand = (command, originalArgv) => {
443
444
  const controlPlane = Option.getOrUndefined(command.config.controlPlane);
444
445
  const effect = command.kind === "connect"
445
- ? runConnect(command.config, command.legacy)
446
+ ? runConnect(command.config, command.legacy, originalArgv)
446
447
  : command.kind === "clean"
447
448
  ? runClean(command.config)
448
449
  : runList(command.config);
@@ -455,7 +456,7 @@ const makeCommandLayer = (input) => {
455
456
  const RelayLive = RelayClientLive.pipe(Layer.provide(Layer.mergeAll(ConfigLive, ProxyLive)));
456
457
  return Layer.mergeAll(ConfigLive, ControlPlaneClientLive, ProxyLive, RelayLive);
457
458
  };
458
- const runConnect = (config, legacy) => Effect.scoped(Effect.gen(function* () {
459
+ const runConnect = (config, legacy, originalArgv) => Effect.scoped(Effect.gen(function* () {
459
460
  if (legacy) {
460
461
  yield* Console.error(formatWarning("`localpreview <target>` is deprecated. Use `localpreview connect <target>`."));
461
462
  }
@@ -474,7 +475,10 @@ const runConnect = (config, legacy) => Effect.scoped(Effect.gen(function* () {
474
475
  const origins = config.captures.map((capture) => formatCaptureOrigin(capture)).join(", ");
475
476
  yield* Console.error(formatWarning(`Captured local backends (${origins}) are exposed through this preview URL. Anyone with the link can reach them.`));
476
477
  }
477
- yield* relay.connectAndServe(tunnel, target.target, config.captures);
478
+ yield* relay.connectAndServe(tunnel, target.target, config.captures, {
479
+ legacy,
480
+ originalArgv: buildConnectOriginalArgv(originalArgv, legacy),
481
+ });
478
482
  }));
479
483
  const validateRequestedName = (value) => Option.match(value, {
480
484
  onNone: () => Effect.succeed(undefined),
package/dist/errors.d.ts CHANGED
@@ -39,6 +39,10 @@ declare const LocalRequestError_base: new <A extends Record<string, any> = {}>(a
39
39
  readonly _tag: "LocalRequestError";
40
40
  } & Readonly<A>;
41
41
  export declare class LocalRequestError extends LocalRequestError_base<{
42
+ readonly invalidCapture?: {
43
+ readonly hostname: string;
44
+ readonly port: number;
45
+ };
42
46
  readonly message: string;
43
47
  readonly requestId: string;
44
48
  }> {
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":";;;AAEA,qBAAa,aAAc,SAAQ,mBAAkC;IACnE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B,CAAC;CAAG;;;;AAEL,qBAAa,cAAe,SAAQ,oBAAmC;IACrE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B,CAAC;CAAG;;;;AAEL,qBAAa,iBAAkB,SAAQ,uBAAsC;IAC3E,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;CAC9B,CAAC;CAAG;;;;AAEL,qBAAa,oBAAqB,SAAQ,0BAAyC;IACjF,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;CAC9B,CAAC;CAAG;;;;AAEL,qBAAa,kBAAmB,SAAQ,wBAAuC;IAC7E,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B,CAAC;CAAG;;;;AAEL,qBAAa,iBAAkB,SAAQ,uBAAsC;IAC3E,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B,CAAC;CAAG;;;;AAEL,qBAAa,iBAAkB,SAAQ,uBAAsC;IAC3E,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B,CAAC;CAAG;AAEL,MAAM,MAAM,eAAe,GACvB,cAAc,GACd,iBAAiB,GACjB,oBAAoB,GACpB,kBAAkB,GAClB,iBAAiB,GACjB,iBAAiB,CAAC;AAEtB,eAAO,MAAM,YAAY,GAAI,OAAO,OAAO,KAAG,MAM7C,CAAC"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":";;;AAEA,qBAAa,aAAc,SAAQ,mBAAkC;IACnE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B,CAAC;CAAG;;;;AAEL,qBAAa,cAAe,SAAQ,oBAAmC;IACrE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B,CAAC;CAAG;;;;AAEL,qBAAa,iBAAkB,SAAQ,uBAAsC;IAC3E,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;CAC9B,CAAC;CAAG;;;;AAEL,qBAAa,oBAAqB,SAAQ,0BAAyC;IACjF,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;CAC9B,CAAC;CAAG;;;;AAEL,qBAAa,kBAAmB,SAAQ,wBAAuC;IAC7E,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B,CAAC;CAAG;;;;AAEL,qBAAa,iBAAkB,SAAQ,uBAAsC;IAC3E,QAAQ,CAAC,cAAc,CAAC,EAAE;QACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;QAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;KACvB,CAAC;IACF,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B,CAAC;CAAG;;;;AAEL,qBAAa,iBAAkB,SAAQ,uBAAsC;IAC3E,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B,CAAC;CAAG;AAEL,MAAM,MAAM,eAAe,GACvB,cAAc,GACd,iBAAiB,GACjB,oBAAoB,GACpB,kBAAkB,GAClB,iBAAiB,GACjB,iBAAiB,CAAC;AAEtB,eAAO,MAAM,YAAY,GAAI,OAAO,OAAO,KAAG,MAM7C,CAAC"}
@@ -5,6 +5,9 @@ import { CliConfig } from "./config.js";
5
5
  import { RelayProtocolError } from "./errors.js";
6
6
  export type ProxySession = {
7
7
  readonly captures: ReadonlyArray<CaptureTarget>;
8
+ readonly legacy: boolean;
9
+ readonly originalArgv: ReadonlyArray<string>;
10
+ readonly reportedMissingCaptures: Set<string>;
8
11
  readonly target: TunnelTarget;
9
12
  };
10
13
  export type LocalProxyShape = {
@@ -1 +1 @@
1
- {"version":3,"file":"local-proxy.d.ts","sourceRoot":"","sources":["../src/local-proxy.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,aAAa,EAClB,KAAK,WAAW,EAEhB,KAAK,YAAY,EAClB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAW,OAAO,EAAE,MAAM,EAAS,KAAK,EAAO,MAAM,QAAQ,CAAC;AACrE,OAAO,KAAK,SAAS,MAAM,IAAI,CAAC;AAchC,OAAO,EAAE,SAAS,EAAuB,MAAM,aAAa,CAAC;AAC7D,OAAO,EAEL,kBAAkB,EAGnB,MAAM,aAAa,CAAC;AAWrB,MAAM,MAAM,YAAY,GAAG;IACzB,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC,aAAa,CAAC,CAAC;IAChD,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;CAC/B,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,CAAC,aAAa,EAAE,CACtB,MAAM,EAAE,SAAS,EACjB,OAAO,EAAE,YAAY,EACrB,KAAK,EAAE,MAAM,KACV,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,kBAAkB,CAAC,CAAC;CAC9C,CAAC;;AAEF,qBAAa,UAAW,SAAQ,eAA4D;CAAG;AAE/F,eAAO,MAAM,cAAc,2CAU1B,CAAC;AAgVF,eAAO,MAAM,uBAAuB,GAAI,QAAQ,MAAM,EAAE,YAAY,MAAM,KAAG,MAqC5E,CAAC;AA0BF,eAAO,MAAM,0BAA0B,GAAI,SAAS,OAAO,KAAG,WAqB7D,CAAC"}
1
+ {"version":3,"file":"local-proxy.d.ts","sourceRoot":"","sources":["../src/local-proxy.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,aAAa,EAClB,KAAK,WAAW,EAEhB,KAAK,YAAY,EAClB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAW,OAAO,EAAE,MAAM,EAAS,KAAK,EAAO,MAAM,QAAQ,CAAC;AACrE,OAAO,KAAK,SAAS,MAAM,IAAI,CAAC;AA8BhC,OAAO,EAAE,SAAS,EAAuB,MAAM,aAAa,CAAC;AAC7D,OAAO,EAEL,kBAAkB,EAGnB,MAAM,aAAa,CAAC;AAWrB,MAAM,MAAM,YAAY,GAAG;IACzB,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC,aAAa,CAAC,CAAC;IAChD,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,YAAY,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC7C,QAAQ,CAAC,uBAAuB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC9C,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;CAC/B,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,CAAC,aAAa,EAAE,CACtB,MAAM,EAAE,SAAS,EACjB,OAAO,EAAE,YAAY,EACrB,KAAK,EAAE,MAAM,KACV,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,kBAAkB,CAAC,CAAC;CAC9C,CAAC;;AAEF,qBAAa,UAAW,SAAQ,eAA4D;CAAG;AAE/F,eAAO,MAAM,cAAc,2CAU1B,CAAC;AA6aF,eAAO,MAAM,uBAAuB,GAAI,QAAQ,MAAM,EAAE,YAAY,MAAM,KAAG,MAqC5E,CAAC;AA0CF,eAAO,MAAM,0BAA0B,GAAI,SAAS,OAAO,KAAG,WAqB7D,CAAC"}
@@ -2,7 +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 { formatRequestError, formatRequestInbound, formatRequestOutbound } from "./cli-ui.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";
6
7
  import { CliConfig } from "./config.js";
7
8
  import { LocalRequestError, RelayProtocolError, RequestLimitError, errorMessage, } from "./errors.js";
8
9
  export class LocalProxy extends Context.Service()("LocalProxy") {
@@ -24,7 +25,7 @@ const handleMessage = (config, requests, socket, session, input) => Effect.gen(f
24
25
  const message = decoded.message;
25
26
  switch (message.type) {
26
27
  case "request-start":
27
- return yield* handleRequestStart(config, requests, socket, message);
28
+ return yield* handleRequestStart(config, requests, socket, session, message);
28
29
  case "request-chunk":
29
30
  return yield* handleRequestChunk(config, requests, socket, message);
30
31
  case "request-end":
@@ -33,7 +34,7 @@ const handleMessage = (config, requests, socket, session, input) => Effect.gen(f
33
34
  return yield* handleCancel(requests, message.requestId);
34
35
  }
35
36
  });
36
- const handleRequestStart = (config, requests, socket, message) => Effect.gen(function* () {
37
+ const handleRequestStart = (config, requests, socket, session, message) => Effect.gen(function* () {
37
38
  const current = yield* Ref.get(requests);
38
39
  if (current.size >= config.maxInFlightRequests) {
39
40
  yield* send(socket, {
@@ -52,7 +53,16 @@ const handleRequestStart = (config, requests, socket, message) => Effect.gen(fun
52
53
  totalBytes: 0,
53
54
  });
54
55
  yield* Ref.set(requests, next);
55
- yield* Console.log(formatRequestOutbound(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
+ }
56
66
  });
57
67
  const handleRequestChunk = (config, requests, socket, message) => Effect.gen(function* () {
58
68
  const current = yield* Ref.get(requests);
@@ -95,7 +105,7 @@ const handleRequestEnd = (config, requests, socket, session, message) => Effect.
95
105
  message: error.message,
96
106
  requestId: message.requestId,
97
107
  type: "response-error",
98
- }).pipe(Effect.andThen(Console.log(formatRequestError(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) => {
99
109
  const next = new Map(map);
100
110
  next.delete(message.requestId);
101
111
  return next;
@@ -115,9 +125,18 @@ const handleCancel = (requests, requestId) => Effect.gen(function* () {
115
125
  yield* Ref.set(requests, next);
116
126
  });
117
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
+ }
118
133
  const route = resolveRoute(request.path, session.target, session.captures);
119
134
  if (route.kind === "invalid-capture") {
120
135
  return yield* Effect.fail(new LocalRequestError({
136
+ invalidCapture: {
137
+ hostname: route.capturePath.hostname,
138
+ port: route.capturePath.port,
139
+ },
121
140
  message: route.message,
122
141
  requestId,
123
142
  }));
@@ -170,12 +189,10 @@ const proxyRequest = (config, socket, session, requestId, request) => Effect.gen
170
189
  }));
171
190
  }
172
191
  const contentType = response.headers.get("content-type");
173
- const shouldInjectShim = route.kind === "primary" &&
174
- session.captures.length > 0 &&
175
- isHtmlResponse(contentType);
192
+ const shouldInjectShim = route.kind === "primary" && isHtmlResponse(contentType);
176
193
  if (shouldInjectShim) {
177
194
  const html = responseBody.toString("utf8");
178
- const shimScript = buildCaptureShimScript(session.captures);
195
+ const shimScript = buildCaptureShimScript(session.target, session.captures);
179
196
  responseBody = Buffer.from(injectCaptureShim(html, shimScript), "utf8");
180
197
  }
181
198
  let responseHeaders = filterLocalResponseHeaders(response.headers);
@@ -203,7 +220,54 @@ const proxyRequest = (config, socket, session, requestId, request) => Effect.gen
203
220
  requestId,
204
221
  type: "response-end",
205
222
  });
206
- yield* Console.log(formatRequestInbound(response.status, logPath(request.path), Date.now() - startedAt));
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
+ });
207
271
  });
208
272
  const rewriteCaptureResponseHeaders = (headers, capturePath) => {
209
273
  const pathPrefix = buildCaptureCookiePathPrefix(capturePath);
@@ -250,6 +314,17 @@ const sendRequestError = (socket, requestId, error) => send(socket, {
250
314
  requestId,
251
315
  type: "response-error",
252
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
+ };
253
328
  const send = (socket, message) => Effect.sync(() => {
254
329
  if (socket.readyState === socket.OPEN) {
255
330
  socket.send(encodeRelayMessage(message));
@@ -283,4 +358,6 @@ const logPath = (path) => {
283
358
  return path.split("?")[0] ?? path;
284
359
  }
285
360
  };
361
+ const formatCaptureTargetUrl = (capturePath) => `${capturePath.protocol}://${capturePath.hostname}:${capturePath.port}${capturePath.path}`;
362
+ const shouldLogOutboundRequest = (method, path) => !(method === "POST" && isLocalPreviewEventsPath(path));
286
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;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,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"}
@@ -10,22 +10,28 @@ export const RelayClientLive = Layer.effect(RelayClient)(Effect.gen(function* ()
10
10
  const config = yield* CliConfig;
11
11
  const localProxy = yield* LocalProxy;
12
12
  return {
13
- connectAndServe: (tunnel, target, captures) => connectAndServe(config, localProxy, tunnel, target, captures),
13
+ connectAndServe: (tunnel, target, captures, sessionMetadata) => connectAndServe(config, localProxy, tunnel, target, captures, sessionMetadata),
14
14
  };
15
15
  }));
16
- const connectAndServe = (config, localProxy, tunnel, target, captures) => serveWithReconnect(config, localProxy, tunnel, target, captures);
17
- 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) => {
18
18
  if (error instanceof RelayConnectionError && error.retryable === true) {
19
- return Console.error(formatRelayReconnect(error.message)).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)));
20
20
  }
21
21
  return Effect.fail(error);
22
22
  }));
23
- const serveOnce = (config, localProxy, tunnel, target, captures) => Effect.gen(function* () {
23
+ const serveOnce = (config, localProxy, tunnel, target, captures, sessionMetadata) => Effect.gen(function* () {
24
24
  const socket = yield* openSocket(tunnel).pipe(Effect.retry({
25
25
  schedule: Schedule.recurs(Math.ceil(config.relayConnectTimeoutMs / 250)),
26
26
  }));
27
27
  yield* Console.log(formatStatus("Connected to relay."));
28
- const session = { captures, target };
28
+ const session = {
29
+ captures,
30
+ legacy: sessionMetadata.legacy,
31
+ originalArgv: sessionMetadata.originalArgv,
32
+ reportedMissingCaptures: new Set(),
33
+ target,
34
+ };
29
35
  const done = yield* Deferred.make();
30
36
  const handleSignal = () => {
31
37
  socket.close(1000, "Interrupted");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "localpreview",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "bin": {
5
5
  "localpreview": "./dist/index.js"
6
6
  },
@@ -10,26 +10,28 @@
10
10
  "README.md"
11
11
  ],
12
12
  "types": "./dist/index.d.ts",
13
- "dependencies": {
14
- "@effect/platform-node": "beta",
15
- "effect": "beta",
16
- "picocolors": "^1.1.1",
17
- "ws": "latest",
18
- "@localpreview/protocol": "0.2.2"
19
- },
20
- "devDependencies": {
21
- "@effect/vitest": "beta",
22
- "@types/node": "latest",
23
- "@types/ws": "latest",
24
- "tsx": "latest",
25
- "typescript": "latest",
26
- "vitest": "latest"
27
- },
28
13
  "scripts": {
29
14
  "build": "tsc -p tsconfig.build.json",
30
15
  "dev": "tsx src/index.ts",
31
16
  "lint": "oxlint .",
17
+ "prepack": "pnpm build",
18
+ "prepublishOnly": "pnpm test && pnpm typecheck && pnpm build",
32
19
  "test": "vitest run --passWithNoTests",
33
20
  "typecheck": "tsc -p tsconfig.json --noEmit"
21
+ },
22
+ "dependencies": {
23
+ "@effect/platform-node": "beta",
24
+ "@localpreview/protocol": "workspace:*",
25
+ "effect": "beta",
26
+ "picocolors": "^1.1.1",
27
+ "ws": "latest"
28
+ },
29
+ "devDependencies": {
30
+ "@effect/vitest": "catalog:",
31
+ "@types/node": "catalog:",
32
+ "@types/ws": "catalog:",
33
+ "tsx": "catalog:",
34
+ "typescript": "catalog:",
35
+ "vitest": "catalog:"
34
36
  }
35
- }
37
+ }