localpreview 0.2.4 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/capture-route.d.ts +1 -0
- package/dist/capture-route.d.ts.map +1 -1
- package/dist/capture-route.js +1 -0
- package/dist/capture-shim.d.ts +5 -1
- package/dist/capture-shim.d.ts.map +1 -1
- package/dist/capture-shim.js +3 -2
- package/dist/cli-ui.d.ts +14 -0
- package/dist/cli-ui.d.ts.map +1 -1
- package/dist/cli-ui.js +60 -11
- package/dist/command.d.ts +1 -0
- package/dist/command.d.ts.map +1 -1
- package/dist/command.js +16 -10
- package/dist/config.d.ts +3 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +3 -1
- package/dist/control-plane.d.ts +3 -7
- package/dist/control-plane.d.ts.map +1 -1
- package/dist/control-plane.js +4 -2
- package/dist/errors.d.ts +4 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/local-proxy.d.ts +3 -0
- package/dist/local-proxy.d.ts.map +1 -1
- package/dist/local-proxy.js +87 -10
- package/dist/missing-capture.d.ts +44 -0
- package/dist/missing-capture.d.ts.map +1 -0
- package/dist/missing-capture.js +128 -0
- package/dist/relay-client.d.ts +12 -3
- package/dist/relay-client.d.ts.map +1 -1
- package/dist/relay-client.js +48 -13
- package/package.json +2 -2
package/dist/capture-route.d.ts
CHANGED
|
@@ -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,
|
|
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"}
|
package/dist/capture-route.js
CHANGED
|
@@ -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
|
};
|
package/dist/capture-shim.d.ts
CHANGED
|
@@ -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,
|
|
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"}
|
package/dist/capture-shim.js
CHANGED
|
@@ -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
|
|
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 = {
|
package/dist/cli-ui.d.ts.map
CHANGED
|
@@ -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,
|
|
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;AAcF,eAAO,MAAM,uBAAuB,GAAI,UAAU,mBAAmB,KAAG,MAkEvE,CAAC;AAyCF,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
|
|
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"),
|
|
@@ -191,6 +238,7 @@ export const renderListHelp = (options = {}) => {
|
|
|
191
238
|
};
|
|
192
239
|
const LIST_COLUMNS = [
|
|
193
240
|
"kind",
|
|
241
|
+
"state",
|
|
194
242
|
"subdomain",
|
|
195
243
|
"tunnel",
|
|
196
244
|
"age",
|
|
@@ -242,6 +290,7 @@ export const formatListTunnelsOutput = (response) => {
|
|
|
242
290
|
};
|
|
243
291
|
const formatListTunnelRow = (item) => [
|
|
244
292
|
formatListItemKind(item.kind),
|
|
293
|
+
item.status ?? "-",
|
|
245
294
|
item.subdomain ?? "-",
|
|
246
295
|
formatShortId(item.tunnelId),
|
|
247
296
|
formatCompactAge(item.activeForMs),
|
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
|
package/dist/command.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"command.d.ts","sourceRoot":"","sources":["../src/command.ts"],"names":[],"mappings":"AAQA,OAAO,EAAW,MAAM,EAAiB,MAAM,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"command.d.ts","sourceRoot":"","sources":["../src/command.ts"],"names":[],"mappings":"AAQA,OAAO,EAAW,MAAM,EAAiB,MAAM,QAAQ,CAAC;AAuBxD,OAAO,EAAE,aAAa,EAAgB,KAAK,eAAe,EAAE,MAAM,aAAa,CAAC;AAyChF,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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { formatCaptureOrigin, parseCaptureHostPort, parseTarget, validateRequestedSubdomain, } from "@localpreview/protocol";
|
|
2
2
|
import { Console, Effect, Layer, Option } from "effect";
|
|
3
|
-
import { adminTokenRequiredMessage,
|
|
4
|
-
import { CliConfig, CliConfigLive, LOCAL_CONTROL_PLANE_URL, normalizeControlPlaneUrl } from "./config.js";
|
|
3
|
+
import { adminTokenRequiredMessage, formatWarning, formatListTunnelsOutput, renderCleanHelp, renderConnectHelp, renderGlobalHelp, renderListHelp, removedAdminTokenFlagMessage, } from "./cli-ui.js";
|
|
4
|
+
import { CliConfig, CliConfigLive, LOCAL_CONTROL_PLANE_PUBLIC_HOST, LOCAL_CONTROL_PLANE_URL, normalizeControlPlaneUrl, } from "./config.js";
|
|
5
5
|
import { ControlPlaneClient, ControlPlaneClientLive, } from "./control-plane.js";
|
|
6
6
|
import { CliUsageError, errorMessage } from "./errors.js";
|
|
7
7
|
import { LocalProxyLive } from "./local-proxy.js";
|
|
@@ -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) {
|
|
@@ -177,6 +178,7 @@ const parseConnectArgs = (argv) => {
|
|
|
177
178
|
config: {
|
|
178
179
|
captures,
|
|
179
180
|
controlPlane: toOption(controlPlane),
|
|
181
|
+
localMode: usedLocalControlPlane,
|
|
180
182
|
requestedName: toOption(requestedName),
|
|
181
183
|
target,
|
|
182
184
|
},
|
|
@@ -439,10 +441,10 @@ const readRequiredOptionValue = (argv, index, option) => {
|
|
|
439
441
|
};
|
|
440
442
|
};
|
|
441
443
|
const toOption = (value) => value === undefined ? Option.none() : Option.some(value);
|
|
442
|
-
const runSubcommand = (command) => {
|
|
444
|
+
const runSubcommand = (command, originalArgv) => {
|
|
443
445
|
const controlPlane = Option.getOrUndefined(command.config.controlPlane);
|
|
444
446
|
const effect = command.kind === "connect"
|
|
445
|
-
? runConnect(command.config, command.legacy)
|
|
447
|
+
? runConnect(command.config, command.legacy, originalArgv)
|
|
446
448
|
: command.kind === "clean"
|
|
447
449
|
? runClean(command.config)
|
|
448
450
|
: runList(command.config);
|
|
@@ -455,7 +457,7 @@ const makeCommandLayer = (input) => {
|
|
|
455
457
|
const RelayLive = RelayClientLive.pipe(Layer.provide(Layer.mergeAll(ConfigLive, ProxyLive)));
|
|
456
458
|
return Layer.mergeAll(ConfigLive, ControlPlaneClientLive, ProxyLive, RelayLive);
|
|
457
459
|
};
|
|
458
|
-
const runConnect = (config, legacy) => Effect.scoped(Effect.gen(function* () {
|
|
460
|
+
const runConnect = (config, legacy, originalArgv) => Effect.scoped(Effect.gen(function* () {
|
|
459
461
|
if (legacy) {
|
|
460
462
|
yield* Console.error(formatWarning("`localpreview <target>` is deprecated. Use `localpreview connect <target>`."));
|
|
461
463
|
}
|
|
@@ -467,14 +469,18 @@ const runConnect = (config, legacy) => Effect.scoped(Effect.gen(function* () {
|
|
|
467
469
|
const controlPlane = yield* ControlPlaneClient;
|
|
468
470
|
const relay = yield* RelayClient;
|
|
469
471
|
const cliConfig = yield* CliConfig;
|
|
470
|
-
const tunnel = yield* Effect.acquireRelease(controlPlane.createTunnel(cliConfig.controlPlaneUrl,
|
|
471
|
-
|
|
472
|
-
|
|
472
|
+
const tunnel = yield* Effect.acquireRelease(controlPlane.createTunnel(cliConfig.controlPlaneUrl, {
|
|
473
|
+
...(config.localMode ? { publicHost: LOCAL_CONTROL_PLANE_PUBLIC_HOST } : {}),
|
|
474
|
+
...(requestedSubdomain === undefined ? {} : { requestedSubdomain }),
|
|
475
|
+
}), (tunnel) => closeTunnelBestEffort(controlPlane, cliConfig.controlPlaneUrl, tunnel));
|
|
473
476
|
if (config.captures.length > 0) {
|
|
474
477
|
const origins = config.captures.map((capture) => formatCaptureOrigin(capture)).join(", ");
|
|
475
478
|
yield* Console.error(formatWarning(`Captured local backends (${origins}) are exposed through this preview URL. Anyone with the link can reach them.`));
|
|
476
479
|
}
|
|
477
|
-
yield* relay.connectAndServe(tunnel, target.target, config.captures
|
|
480
|
+
yield* relay.connectAndServe(tunnel, target.target, config.captures, {
|
|
481
|
+
legacy,
|
|
482
|
+
originalArgv: buildConnectOriginalArgv(originalArgv, legacy),
|
|
483
|
+
});
|
|
478
484
|
}));
|
|
479
485
|
const validateRequestedName = (value) => Option.match(value, {
|
|
480
486
|
onNone: () => Effect.succeed(undefined),
|
package/dist/config.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export type CliConfigShape = {
|
|
|
4
4
|
readonly controlPlaneUrl: string;
|
|
5
5
|
readonly maxInFlightRequests: number;
|
|
6
6
|
readonly relayConnectTimeoutMs: number;
|
|
7
|
+
readonly relayReadyTimeoutMs: number;
|
|
7
8
|
readonly requestBodyLimitBytes: number;
|
|
8
9
|
readonly requestTimeoutMs: number;
|
|
9
10
|
readonly responseBodyLimitBytes: number;
|
|
@@ -13,7 +14,8 @@ declare const CliConfig_base: Context.ServiceClass<CliConfig, "CliConfig", CliCo
|
|
|
13
14
|
export declare class CliConfig extends CliConfig_base {
|
|
14
15
|
}
|
|
15
16
|
export declare const PUBLIC_CONTROL_PLANE_URL = "https://localpreview.dev";
|
|
16
|
-
export declare const LOCAL_CONTROL_PLANE_URL = "http://
|
|
17
|
+
export declare const LOCAL_CONTROL_PLANE_URL = "http://127.0.0.1:3000";
|
|
18
|
+
export declare const LOCAL_CONTROL_PLANE_PUBLIC_HOST = "localhost:3000";
|
|
17
19
|
export type NormalizeControlPlaneUrlResult = {
|
|
18
20
|
readonly ok: true;
|
|
19
21
|
readonly url: string;
|
package/dist/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AAEhD,MAAM,MAAM,cAAc,GAAG;IAC3B,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;IACxC,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAC;IACrC,QAAQ,CAAC,qBAAqB,EAAE,MAAM,CAAC;IACvC,QAAQ,CAAC,qBAAqB,EAAE,MAAM,CAAC;IACvC,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,sBAAsB,EAAE,MAAM,CAAC;IACxC,QAAQ,CAAC,sBAAsB,EAAE,MAAM,CAAC;CACzC,CAAC;;AAEF,qBAAa,SAAU,SAAQ,cAAyD;CAAG;AAE3F,eAAO,MAAM,wBAAwB,6BAA6B,CAAC;AACnE,eAAO,MAAM,uBAAuB,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AAEhD,MAAM,MAAM,cAAc,GAAG;IAC3B,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;IACxC,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAC;IACrC,QAAQ,CAAC,qBAAqB,EAAE,MAAM,CAAC;IACvC,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAC;IACrC,QAAQ,CAAC,qBAAqB,EAAE,MAAM,CAAC;IACvC,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,sBAAsB,EAAE,MAAM,CAAC;IACxC,QAAQ,CAAC,sBAAsB,EAAE,MAAM,CAAC;CACzC,CAAC;;AAEF,qBAAa,SAAU,SAAQ,cAAyD;CAAG;AAE3F,eAAO,MAAM,wBAAwB,6BAA6B,CAAC;AACnE,eAAO,MAAM,uBAAuB,0BAA0B,CAAC;AAC/D,eAAO,MAAM,+BAA+B,mBAAmB,CAAC;AAEhE,MAAM,MAAM,8BAA8B,GACtC;IACE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAClB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB,GACD;IACE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;CACpB,CAAC;AAEN,gFAAgF;AAChF,eAAO,MAAM,wBAAwB,GAAI,OAAO,MAAM,KAAG,8BA4DxD,CAAC;AA2BF,eAAO,MAAM,aAAa,GAAI,OAAO;IACnC,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CAClC,KAAG,cAmBH,CAAC;AAEF,eAAO,MAAM,aAAa,GAAI,OAAO;IACnC,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CAClC,yCAAmD,CAAC;AAWrD,eAAO,MAAM,aAAa,GAAI,OAAO;IACnC,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CAClC,KAAG,MAAM,CAAC,MAAM,CAAC,cAAc,CAA4C,CAAC"}
|
package/dist/config.js
CHANGED
|
@@ -3,7 +3,8 @@ import { Context, Effect, Layer } from "effect";
|
|
|
3
3
|
export class CliConfig extends Context.Service()("CliConfig") {
|
|
4
4
|
}
|
|
5
5
|
export const PUBLIC_CONTROL_PLANE_URL = LOCALPREVIEW_PUBLIC_ORIGIN;
|
|
6
|
-
export const LOCAL_CONTROL_PLANE_URL = "http://
|
|
6
|
+
export const LOCAL_CONTROL_PLANE_URL = "http://127.0.0.1:3000";
|
|
7
|
+
export const LOCAL_CONTROL_PLANE_PUBLIC_HOST = "localhost:3000";
|
|
7
8
|
/** Normalizes and validates a control-plane base URL from `--control-plane`. */
|
|
8
9
|
export const normalizeControlPlaneUrl = (value) => {
|
|
9
10
|
let parsed;
|
|
@@ -84,6 +85,7 @@ export const makeCliConfig = (input) => {
|
|
|
84
85
|
controlPlaneUrl,
|
|
85
86
|
maxInFlightRequests: readNumber(env.LOCALPREVIEW_MAX_IN_FLIGHT_REQUESTS, 100),
|
|
86
87
|
relayConnectTimeoutMs: readNumber(env.LOCALPREVIEW_RELAY_CONNECT_TIMEOUT_MS, 10_000),
|
|
88
|
+
relayReadyTimeoutMs: readNumber(env.LOCALPREVIEW_RELAY_READY_TIMEOUT_MS, 60_000),
|
|
87
89
|
requestBodyLimitBytes: readNumber(env.LOCALPREVIEW_REQUEST_BODY_LIMIT_BYTES, 10 * 1024 * 1024),
|
|
88
90
|
requestTimeoutMs: readNumber(env.LOCALPREVIEW_REQUEST_TIMEOUT_MS, 30_000),
|
|
89
91
|
responseBodyLimitBytes: readNumber(env.LOCALPREVIEW_RESPONSE_BODY_LIMIT_BYTES, 50 * 1024 * 1024),
|
package/dist/control-plane.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type CreateTunnelResponse, type ListTunnelsResponse } from "@localpreview/protocol";
|
|
1
|
+
import { type CreateTunnelRequest, type CreateTunnelResponse, type ListTunnelsResponse } from "@localpreview/protocol";
|
|
2
2
|
import { Context, Effect, Layer } from "effect";
|
|
3
3
|
import { ControlPlaneError } from "./errors.js";
|
|
4
4
|
export type ControlPlaneClientShape = {
|
|
@@ -12,9 +12,7 @@ export type ControlPlaneClientShape = {
|
|
|
12
12
|
readonly subdomain: string;
|
|
13
13
|
}) => Effect.Effect<CleanSubdomainResponse, ControlPlaneError>;
|
|
14
14
|
readonly closeTunnel: (controlPlaneUrl: string, tunnel: Pick<CreateTunnelResponse, "clientToken" | "tunnelId">) => Effect.Effect<void, ControlPlaneError>;
|
|
15
|
-
readonly createTunnel: (controlPlaneUrl: string, body:
|
|
16
|
-
readonly requestedSubdomain?: string;
|
|
17
|
-
}) => Effect.Effect<CreateTunnelResponse, ControlPlaneError>;
|
|
15
|
+
readonly createTunnel: (controlPlaneUrl: string, body: CreateTunnelRequest) => Effect.Effect<CreateTunnelResponse, ControlPlaneError>;
|
|
18
16
|
readonly listTunnels: (controlPlaneUrl: string, input: {
|
|
19
17
|
readonly adminToken: string;
|
|
20
18
|
readonly limit: number;
|
|
@@ -47,9 +45,7 @@ declare const ControlPlaneClient_base: Context.ServiceClass<ControlPlaneClient,
|
|
|
47
45
|
export declare class ControlPlaneClient extends ControlPlaneClient_base {
|
|
48
46
|
}
|
|
49
47
|
export declare const ControlPlaneClientLive: Layer.Layer<ControlPlaneClient, never, never>;
|
|
50
|
-
export declare const createTunnel: (controlPlaneUrl: string, body:
|
|
51
|
-
readonly requestedSubdomain?: string;
|
|
52
|
-
}) => Promise<CreateTunnelResponse>;
|
|
48
|
+
export declare const createTunnel: (controlPlaneUrl: string, body: CreateTunnelRequest) => Promise<CreateTunnelResponse>;
|
|
53
49
|
export declare const closeTunnel: (controlPlaneUrl: string, tunnel: Pick<CreateTunnelResponse, "clientToken" | "tunnelId">) => Promise<void>;
|
|
54
50
|
export declare const cleanSubdomain: (controlPlaneUrl: string, input: {
|
|
55
51
|
readonly adminToken: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"control-plane.d.ts","sourceRoot":"","sources":["../src/control-plane.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,oBAAoB,EACzB,KAAK,mBAAmB,EAEzB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAY,MAAM,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"control-plane.d.ts","sourceRoot":"","sources":["../src/control-plane.ts"],"names":[],"mappings":"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"}
|
package/dist/control-plane.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { LOCALPREVIEW_ADMIN_TOKEN_HEADER, LOCALPREVIEW_ERROR_CODES, LOCALPREVIEW_PROTOCOL_VERSION, LOCALPREVIEW_RELAY_SNAPSHOT_VERSION, } from "@localpreview/protocol";
|
|
2
2
|
import { Context, Effect, Layer, Schedule } from "effect";
|
|
3
|
+
import { LOCAL_CONTROL_PLANE_URL } from "./config.js";
|
|
3
4
|
import { ControlPlaneError } from "./errors.js";
|
|
4
5
|
export class ControlPlaneClient extends Context.Service()("ControlPlaneClient") {
|
|
5
6
|
}
|
|
@@ -20,8 +21,9 @@ export const cleanAllSubdomains = (controlPlaneUrl, input) => Effect.runPromise(
|
|
|
20
21
|
export const listTunnels = (controlPlaneUrl, input) => Effect.runPromise(listTunnelsEffect(controlPlaneUrl, input));
|
|
21
22
|
const createTunnelEffect = (controlPlaneUrl, body) => Effect.promise(async () => {
|
|
22
23
|
let response;
|
|
24
|
+
const createTunnelUrl = new URL("/api/tunnels", controlPlaneUrl);
|
|
23
25
|
try {
|
|
24
|
-
response = await fetch(
|
|
26
|
+
response = await fetch(createTunnelUrl, {
|
|
25
27
|
body: JSON.stringify(body),
|
|
26
28
|
headers: {
|
|
27
29
|
"content-type": "application/json",
|
|
@@ -33,7 +35,7 @@ const createTunnelEffect = (controlPlaneUrl, body) => Effect.promise(async () =>
|
|
|
33
35
|
throw new ControlPlaneError({
|
|
34
36
|
message: [
|
|
35
37
|
`Could not reach localpreview control-plane at ${controlPlaneUrl}.`,
|
|
36
|
-
|
|
38
|
+
`Use "-l" or "--local" as shorthand for --control-plane ${LOCAL_CONTROL_PLANE_URL}.`,
|
|
37
39
|
].join("\n"),
|
|
38
40
|
retryable: false,
|
|
39
41
|
});
|
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
|
}> {
|
package/dist/errors.d.ts.map
CHANGED
|
@@ -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"}
|
package/dist/local-proxy.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/local-proxy.js
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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
|
-
|
|
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":"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"}
|
|
@@ -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);
|
package/dist/relay-client.d.ts
CHANGED
|
@@ -1,15 +1,24 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import { Context, Effect, Layer } from "effect";
|
|
1
|
+
import { type CaptureTarget, type CreateTunnelResponse, type TunnelTarget } from "@localpreview/protocol";
|
|
2
|
+
import { Context, Deferred, Effect, Layer } from "effect";
|
|
3
3
|
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
|
|
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
16
|
export declare const RelayClientLive: Layer.Layer<RelayClient, never, CliConfig | LocalProxy>;
|
|
17
|
+
export declare const waitForTunnelReady: (input: {
|
|
18
|
+
readonly ready: Deferred.Deferred<void, RelayConnectionError>;
|
|
19
|
+
readonly terminatedBeforeReady: Deferred.Deferred<void, RelayConnectionError>;
|
|
20
|
+
readonly timeoutMs: number;
|
|
21
|
+
}) => Effect.Effect<void, RelayConnectionError>;
|
|
13
22
|
export declare const isRetryableCloseCode: (code: number) => boolean;
|
|
14
23
|
export {};
|
|
15
24
|
//# 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,
|
|
1
|
+
{"version":3,"file":"relay-client.d.ts","sourceRoot":"","sources":["../src/relay-client.ts"],"names":[],"mappings":"AAAA,OAAO,EAA4B,KAAK,aAAa,EAAE,KAAK,oBAAoB,EAAE,KAAK,YAAY,EAAE,MAAM,wBAAwB,CAAC;AACpI,OAAO,EAAW,OAAO,EAAE,QAAQ,EAAE,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;AA0OF,eAAO,MAAM,kBAAkB,GAAI,OAAO;IACxC,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC,QAAQ,CAAC,IAAI,EAAE,oBAAoB,CAAC,CAAC;IAC9D,QAAQ,CAAC,qBAAqB,EAAE,QAAQ,CAAC,QAAQ,CAAC,IAAI,EAAE,oBAAoB,CAAC,CAAC;IAC9E,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B,KAAG,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,oBAAoB,CAczC,CAAC;AAEJ,eAAO,MAAM,oBAAoB,GAAI,MAAM,MAAM,KAAG,OAAwB,CAAC"}
|
package/dist/relay-client.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { decodeServerRelayMessage } from "@localpreview/protocol";
|
|
1
2
|
import { Console, Context, Deferred, Effect, Layer, Schedule } from "effect";
|
|
2
3
|
import WebSocket from "ws";
|
|
3
4
|
import { formatRelayReconnect, formatStatus } from "./cli-ui.js";
|
|
@@ -10,23 +11,32 @@ export const RelayClientLive = Layer.effect(RelayClient)(Effect.gen(function* ()
|
|
|
10
11
|
const config = yield* CliConfig;
|
|
11
12
|
const localProxy = yield* LocalProxy;
|
|
12
13
|
return {
|
|
13
|
-
connectAndServe: (tunnel, target, captures) => connectAndServe(config, localProxy, tunnel, target, captures),
|
|
14
|
+
connectAndServe: (tunnel, target, captures, sessionMetadata) => connectAndServe(config, localProxy, tunnel, target, captures, sessionMetadata),
|
|
14
15
|
};
|
|
15
16
|
}));
|
|
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) => {
|
|
17
|
+
const connectAndServe = (config, localProxy, tunnel, target, captures, sessionMetadata) => serveWithReconnect(config, localProxy, tunnel, target, captures, sessionMetadata);
|
|
18
|
+
const serveWithReconnect = (config, localProxy, tunnel, target, captures, sessionMetadata) => serveOnce(config, localProxy, tunnel, target, captures, sessionMetadata).pipe(Effect.catch((error) => {
|
|
18
19
|
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)));
|
|
20
|
+
return Console.error(formatRelayReconnect(error.message)).pipe(Effect.andThen(Effect.sleep("1 second")), Effect.andThen(serveWithReconnect(config, localProxy, tunnel, target, captures, sessionMetadata)));
|
|
20
21
|
}
|
|
21
22
|
return Effect.fail(error);
|
|
22
23
|
}));
|
|
23
|
-
const serveOnce = (config, localProxy, tunnel, target, captures) => Effect.gen(function* () {
|
|
24
|
+
const serveOnce = (config, localProxy, tunnel, target, captures, sessionMetadata) => Effect.gen(function* () {
|
|
24
25
|
const socket = yield* openSocket(tunnel).pipe(Effect.retry({
|
|
25
26
|
schedule: Schedule.recurs(Math.ceil(config.relayConnectTimeoutMs / 250)),
|
|
26
27
|
}));
|
|
27
28
|
yield* Console.log(formatStatus("Connected to relay."));
|
|
28
|
-
const session = {
|
|
29
|
+
const session = {
|
|
30
|
+
captures,
|
|
31
|
+
legacy: sessionMetadata.legacy,
|
|
32
|
+
originalArgv: sessionMetadata.originalArgv,
|
|
33
|
+
reportedMissingCaptures: new Set(),
|
|
34
|
+
target,
|
|
35
|
+
};
|
|
29
36
|
const done = yield* Deferred.make();
|
|
37
|
+
const ready = yield* Deferred.make();
|
|
38
|
+
const terminatedBeforeReady = yield* Deferred.make();
|
|
39
|
+
let tunnelReady = false;
|
|
30
40
|
const handleSignal = () => {
|
|
31
41
|
socket.close(1000, "Interrupted");
|
|
32
42
|
Effect.runFork(Deferred.succeed(done, undefined));
|
|
@@ -36,6 +46,14 @@ const serveOnce = (config, localProxy, tunnel, target, captures) => Effect.gen(f
|
|
|
36
46
|
process.once("SIGINT", handleSignal);
|
|
37
47
|
process.once("SIGTERM", handleSignal);
|
|
38
48
|
socket.on("message", (data) => {
|
|
49
|
+
const decoded = decodeServerRelayMessage(data.toString());
|
|
50
|
+
if (decoded.ok &&
|
|
51
|
+
decoded.message.type === "tunnel-ready" &&
|
|
52
|
+
decoded.message.tunnelId === tunnel.tunnelId) {
|
|
53
|
+
tunnelReady = true;
|
|
54
|
+
Effect.runFork(Deferred.succeed(ready, undefined));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
39
57
|
const effect = localProxy.handleMessage(socket, session, data.toString()).pipe(Effect.catch((error) => Effect.gen(function* () {
|
|
40
58
|
socket.close(1002, error.message);
|
|
41
59
|
yield* Deferred.fail(done, error);
|
|
@@ -43,19 +61,23 @@ const serveOnce = (config, localProxy, tunnel, target, captures) => Effect.gen(f
|
|
|
43
61
|
Effect.runFork(effect);
|
|
44
62
|
});
|
|
45
63
|
socket.on("error", (error) => {
|
|
46
|
-
|
|
64
|
+
const relayError = new RelayConnectionError({
|
|
47
65
|
message: error.message,
|
|
48
|
-
})
|
|
66
|
+
});
|
|
67
|
+
Effect.runFork(Deferred.fail(done, relayError).pipe(Effect.andThen(Deferred.fail(terminatedBeforeReady, relayError))));
|
|
49
68
|
});
|
|
50
69
|
socket.on("close", (code, reason) => {
|
|
70
|
+
const relayError = new RelayConnectionError({
|
|
71
|
+
message: tunnelReady
|
|
72
|
+
? `Relay connection closed (${code}): ${reason.toString()}`
|
|
73
|
+
: `Relay connection closed before tunnel-ready (${code}): ${reason.toString()}`,
|
|
74
|
+
retryable: isRetryableCloseCode(code),
|
|
75
|
+
});
|
|
51
76
|
if (code === 1000 || code === 1001) {
|
|
52
|
-
Effect.runFork(Deferred.succeed(done, undefined));
|
|
77
|
+
Effect.runFork(Deferred.fail(terminatedBeforeReady, relayError).pipe(Effect.andThen(Deferred.succeed(done, undefined))));
|
|
53
78
|
return;
|
|
54
79
|
}
|
|
55
|
-
Effect.runFork(Deferred.fail(done,
|
|
56
|
-
message: `Relay connection closed (${code}): ${reason.toString()}`,
|
|
57
|
-
retryable: isRetryableCloseCode(code),
|
|
58
|
-
})));
|
|
80
|
+
Effect.runFork(Deferred.fail(done, relayError).pipe(Effect.andThen(Deferred.fail(terminatedBeforeReady, relayError))));
|
|
59
81
|
});
|
|
60
82
|
}), () => Effect.sync(() => {
|
|
61
83
|
process.off("SIGINT", handleSignal);
|
|
@@ -64,6 +86,13 @@ const serveOnce = (config, localProxy, tunnel, target, captures) => Effect.gen(f
|
|
|
64
86
|
socket.close(1000, "Closing");
|
|
65
87
|
}
|
|
66
88
|
}));
|
|
89
|
+
yield* waitForTunnelReady({
|
|
90
|
+
ready,
|
|
91
|
+
terminatedBeforeReady,
|
|
92
|
+
timeoutMs: config.relayReadyTimeoutMs,
|
|
93
|
+
});
|
|
94
|
+
yield* Console.log(formatStatus(`Tunnel ready: ${tunnel.publicUrl}`));
|
|
95
|
+
yield* Console.log(formatStatus(`Forwarding to ${target.protocol}://${target.hostname}:${target.port}`));
|
|
67
96
|
yield* Deferred.await(done);
|
|
68
97
|
}));
|
|
69
98
|
});
|
|
@@ -114,4 +143,10 @@ const openSocket = (tunnel) => Effect.callback((resume) => {
|
|
|
114
143
|
}).pipe(Effect.mapError((error) => new RelayConnectionError({
|
|
115
144
|
message: errorMessage(error),
|
|
116
145
|
})));
|
|
146
|
+
export const waitForTunnelReady = (input) => Effect.raceFirst(Deferred.await(input.ready), Deferred.await(input.terminatedBeforeReady)).pipe(Effect.timeoutOrElse({
|
|
147
|
+
duration: `${input.timeoutMs} millis`,
|
|
148
|
+
orElse: () => Effect.fail(new RelayConnectionError({
|
|
149
|
+
message: "Timed out waiting for relay tunnel-ready acknowledgement.",
|
|
150
|
+
})),
|
|
151
|
+
}));
|
|
117
152
|
export const isRetryableCloseCode = (code) => code === 1006;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "localpreview",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
4
4
|
"bin": {
|
|
5
5
|
"localpreview": "./dist/index.js"
|
|
6
6
|
},
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"effect": "beta",
|
|
16
16
|
"picocolors": "^1.1.1",
|
|
17
17
|
"ws": "latest",
|
|
18
|
-
"@localpreview/protocol": "0.2.
|
|
18
|
+
"@localpreview/protocol": "0.2.3"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"@effect/vitest": "beta",
|