localpreview 0.1.0 → 0.2.1
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 +38 -0
- package/dist/capture-route.d.ts.map +1 -0
- package/dist/capture-route.js +91 -0
- package/dist/capture-shim.d.ts +18 -0
- package/dist/capture-shim.d.ts.map +1 -0
- package/dist/capture-shim.js +34 -0
- package/dist/command.d.ts.map +1 -1
- package/dist/command.js +123 -13
- package/dist/config.d.ts +9 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +71 -0
- package/dist/control-plane.d.ts.map +1 -1
- package/dist/control-plane.js +4 -4
- package/dist/local-proxy.d.ts +7 -2
- package/dist/local-proxy.d.ts.map +1 -1
- package/dist/local-proxy.js +82 -11
- package/dist/relay-client.d.ts +2 -2
- package/dist/relay-client.d.ts.map +1 -1
- package/dist/relay-client.js +7 -6
- package/package.json +18 -16
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type CaptureTarget, type TunnelTarget } from "@localpreview/protocol";
|
|
2
|
+
export declare const CAPTURE_PATH_PREFIX = "/__localpreview/capture";
|
|
3
|
+
export type ParsedCapturePath = {
|
|
4
|
+
readonly hostname: string;
|
|
5
|
+
readonly path: string;
|
|
6
|
+
readonly port: number;
|
|
7
|
+
readonly protocol: TunnelTarget["protocol"];
|
|
8
|
+
};
|
|
9
|
+
type ParseCapturePathResult = {
|
|
10
|
+
readonly ok: true;
|
|
11
|
+
readonly parsed: ParsedCapturePath;
|
|
12
|
+
} | {
|
|
13
|
+
readonly ok: false;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Parses an internal capture route:
|
|
17
|
+
* `/__localpreview/capture/<protocol>/<host>/<port>/<original-path>`.
|
|
18
|
+
*/
|
|
19
|
+
export declare const parseCapturePath: (requestPath: string) => ParseCapturePathResult;
|
|
20
|
+
/** Builds the internal capture path prefix for cookie isolation. */
|
|
21
|
+
export declare const buildCaptureCookiePathPrefix: (parsed: ParsedCapturePath) => string;
|
|
22
|
+
/** Encodes a hostname for use in capture path segments. */
|
|
23
|
+
export declare const encodeCaptureHostname: (hostname: string) => string;
|
|
24
|
+
export type ResolvedRoute = {
|
|
25
|
+
readonly capturePath: ParsedCapturePath;
|
|
26
|
+
readonly kind: "capture";
|
|
27
|
+
readonly target: TunnelTarget;
|
|
28
|
+
} | {
|
|
29
|
+
readonly kind: "invalid-capture";
|
|
30
|
+
readonly message: string;
|
|
31
|
+
} | {
|
|
32
|
+
readonly kind: "primary";
|
|
33
|
+
readonly target: TunnelTarget;
|
|
34
|
+
};
|
|
35
|
+
/** Resolves a proxied request to either the primary target or a captured backend. */
|
|
36
|
+
export declare const resolveRoute: (requestPath: string, primaryTarget: TunnelTarget, captures: ReadonlyArray<CaptureTarget>) => ResolvedRoute;
|
|
37
|
+
export {};
|
|
38
|
+
//# sourceMappingURL=capture-route.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { captureToTunnelTarget, findCapture, normalizeLoopbackHostname, } from "@localpreview/protocol";
|
|
2
|
+
export const CAPTURE_PATH_PREFIX = "/__localpreview/capture";
|
|
3
|
+
/**
|
|
4
|
+
* Parses an internal capture route:
|
|
5
|
+
* `/__localpreview/capture/<protocol>/<host>/<port>/<original-path>`.
|
|
6
|
+
*/
|
|
7
|
+
export const parseCapturePath = (requestPath) => {
|
|
8
|
+
const pathname = splitPathname(requestPath);
|
|
9
|
+
if (!pathname.startsWith(`${CAPTURE_PATH_PREFIX}/`)) {
|
|
10
|
+
return { ok: false };
|
|
11
|
+
}
|
|
12
|
+
const suffix = pathname.slice(CAPTURE_PATH_PREFIX.length + 1);
|
|
13
|
+
const segments = suffix.split("/");
|
|
14
|
+
if (segments.length < 4) {
|
|
15
|
+
return { ok: false };
|
|
16
|
+
}
|
|
17
|
+
const [protocolText, encodedHost, portText, ...rest] = segments;
|
|
18
|
+
const protocol = parseProtocol(protocolText);
|
|
19
|
+
if (protocol === undefined) {
|
|
20
|
+
return { ok: false };
|
|
21
|
+
}
|
|
22
|
+
const port = Number(portText);
|
|
23
|
+
if (!Number.isInteger(port) || port < 1 || port > 65_535) {
|
|
24
|
+
return { ok: false };
|
|
25
|
+
}
|
|
26
|
+
let hostname;
|
|
27
|
+
try {
|
|
28
|
+
hostname = normalizeLoopbackHostname(decodeURIComponent(encodedHost ?? ""));
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return { ok: false };
|
|
32
|
+
}
|
|
33
|
+
const pathSuffix = rest.length === 0 ? "/" : `/${rest.join("/")}`;
|
|
34
|
+
const query = splitQuery(requestPath);
|
|
35
|
+
return {
|
|
36
|
+
ok: true,
|
|
37
|
+
parsed: {
|
|
38
|
+
hostname,
|
|
39
|
+
path: query === undefined ? pathSuffix : `${pathSuffix}?${query}`,
|
|
40
|
+
port,
|
|
41
|
+
protocol,
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
/** Builds the internal capture path prefix for cookie isolation. */
|
|
46
|
+
export const buildCaptureCookiePathPrefix = (parsed) => `${CAPTURE_PATH_PREFIX}/${parsed.protocol}/${encodeCaptureHostname(parsed.hostname)}/${parsed.port}`;
|
|
47
|
+
/** Encodes a hostname for use in capture path segments. */
|
|
48
|
+
export const encodeCaptureHostname = (hostname) => encodeURIComponent(normalizeLoopbackHostname(hostname));
|
|
49
|
+
/** Resolves a proxied request to either the primary target or a captured backend. */
|
|
50
|
+
export const resolveRoute = (requestPath, primaryTarget, captures) => {
|
|
51
|
+
const parsedCapture = parseCapturePath(requestPath);
|
|
52
|
+
if (!parsedCapture.ok) {
|
|
53
|
+
return {
|
|
54
|
+
kind: "primary",
|
|
55
|
+
target: primaryTarget,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const { parsed } = parsedCapture;
|
|
59
|
+
const capture = findCapture(captures, parsed.hostname, parsed.port);
|
|
60
|
+
if (capture === undefined) {
|
|
61
|
+
return {
|
|
62
|
+
kind: "invalid-capture",
|
|
63
|
+
message: `Capture route is not allowlisted: ${parsed.hostname}:${parsed.port}.`,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
capturePath: parsed,
|
|
68
|
+
kind: "capture",
|
|
69
|
+
target: captureToTunnelTarget(capture, parsed.protocol),
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
const parseProtocol = (value) => {
|
|
73
|
+
if (value === "http" || value === "https") {
|
|
74
|
+
return value;
|
|
75
|
+
}
|
|
76
|
+
return undefined;
|
|
77
|
+
};
|
|
78
|
+
const splitPathname = (requestPath) => {
|
|
79
|
+
const questionMark = requestPath.indexOf("?");
|
|
80
|
+
if (questionMark === -1) {
|
|
81
|
+
return requestPath;
|
|
82
|
+
}
|
|
83
|
+
return requestPath.slice(0, questionMark);
|
|
84
|
+
};
|
|
85
|
+
const splitQuery = (requestPath) => {
|
|
86
|
+
const questionMark = requestPath.indexOf("?");
|
|
87
|
+
if (questionMark === -1) {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
return requestPath.slice(questionMark + 1);
|
|
91
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { CaptureTarget } from "@localpreview/protocol";
|
|
2
|
+
import { encodeCaptureHostname } from "./capture-route.js";
|
|
3
|
+
export { encodeCaptureHostname };
|
|
4
|
+
/** Generates the inline browser shim injected into proxied HTML responses. */
|
|
5
|
+
export declare const buildCaptureShimScript: (captures: ReadonlyArray<CaptureTarget>) => string;
|
|
6
|
+
/** Injects the capture shim into an HTML document string. */
|
|
7
|
+
export declare const injectCaptureShim: (html: string, shimScript: string) => string;
|
|
8
|
+
/** Returns true when the response looks like HTML suitable for shim injection. */
|
|
9
|
+
export declare const isHtmlResponse: (contentType: string | null) => boolean;
|
|
10
|
+
/** Strips CSP headers that would block the injected shim. */
|
|
11
|
+
export declare const stripContentSecurityPolicy: (headers: ReadonlyArray<readonly [string, string]>) => Array<readonly [string, string]>;
|
|
12
|
+
/** Builds the canonical local frontend origin used for Origin/Referer simulation. */
|
|
13
|
+
export declare const buildFrontendOrigin: (target: {
|
|
14
|
+
readonly hostname: string;
|
|
15
|
+
readonly port: number;
|
|
16
|
+
readonly protocol: "http" | "https";
|
|
17
|
+
}) => string;
|
|
18
|
+
//# sourceMappingURL=capture-shim.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { CAPTURE_PATH_PREFIX, encodeCaptureHostname } from "./capture-route.js";
|
|
2
|
+
export { encodeCaptureHostname };
|
|
3
|
+
/** Generates the inline browser shim injected into proxied HTML responses. */
|
|
4
|
+
export const buildCaptureShimScript = (captures) => {
|
|
5
|
+
const config = JSON.stringify({
|
|
6
|
+
captures: captures.map((capture) => ({
|
|
7
|
+
hostname: capture.hostname,
|
|
8
|
+
port: capture.port,
|
|
9
|
+
})),
|
|
10
|
+
prefix: CAPTURE_PATH_PREFIX,
|
|
11
|
+
});
|
|
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
|
+
};
|
|
14
|
+
/** Injects the capture shim into an HTML document string. */
|
|
15
|
+
export const injectCaptureShim = (html, shimScript) => {
|
|
16
|
+
const scriptTag = `<script>${shimScript}</script>`;
|
|
17
|
+
if (html.includes("</head>")) {
|
|
18
|
+
return html.replace("</head>", `${scriptTag}</head>`);
|
|
19
|
+
}
|
|
20
|
+
const bodyMatch = html.match(/<body[^>]*>/i);
|
|
21
|
+
if (bodyMatch !== null) {
|
|
22
|
+
return html.replace(bodyMatch[0], `${bodyMatch[0]}${scriptTag}`);
|
|
23
|
+
}
|
|
24
|
+
return `${scriptTag}${html}`;
|
|
25
|
+
};
|
|
26
|
+
/** Returns true when the response looks like HTML suitable for shim injection. */
|
|
27
|
+
export const isHtmlResponse = (contentType) => contentType !== null && contentType.toLowerCase().includes("text/html");
|
|
28
|
+
/** Strips CSP headers that would block the injected shim. */
|
|
29
|
+
export const stripContentSecurityPolicy = (headers) => headers.filter(([name]) => {
|
|
30
|
+
const lower = name.toLowerCase();
|
|
31
|
+
return lower !== "content-security-policy" && lower !== "content-security-policy-report-only";
|
|
32
|
+
});
|
|
33
|
+
/** Builds the canonical local frontend origin used for Origin/Referer simulation. */
|
|
34
|
+
export const buildFrontendOrigin = (target) => `${target.protocol}://${target.hostname}:${target.port}`;
|
package/dist/command.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"command.d.ts","sourceRoot":"","sources":["../src/command.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"command.d.ts","sourceRoot":"","sources":["../src/command.ts"],"names":[],"mappings":"AAQA,OAAO,EAAW,MAAM,EAAiB,MAAM,QAAQ,CAAC;AAOxD,OAAO,EAAE,aAAa,EAAgB,KAAK,eAAe,EAAE,MAAM,aAAa,CAAC;AA8BhF,eAAO,MAAM,MAAM,GACjB,MAAM,aAAa,CAAC,MAAM,CAAC,KAC1B,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,eAAe,GAAG,aAAa,CAkBrD,CAAC;AAEF,eAAO,MAAM,gBAAgB,GAAI,MAAM,aAAa,CAAC,MAAM,CAAC,KAAG,aAAa,CAAC,MAAM,CAC1C,CAAC"}
|
package/dist/command.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { parseTarget, validateRequestedSubdomain, } from "@localpreview/protocol";
|
|
1
|
+
import { formatCaptureOrigin, parseCaptureHostPort, parseTarget, validateRequestedSubdomain, } from "@localpreview/protocol";
|
|
2
2
|
import { Console, Effect, Layer, Option } from "effect";
|
|
3
|
-
import { CliConfig, CliConfigLive, LOCAL_CONTROL_PLANE_URL } from "./config.js";
|
|
3
|
+
import { CliConfig, CliConfigLive, LOCAL_CONTROL_PLANE_URL, normalizeControlPlaneUrl } from "./config.js";
|
|
4
4
|
import { ControlPlaneClient, ControlPlaneClientLive, } from "./control-plane.js";
|
|
5
5
|
import { CliUsageError, errorMessage } from "./errors.js";
|
|
6
6
|
import { LocalProxyLive } from "./local-proxy.js";
|
|
@@ -30,16 +30,19 @@ const isLegacyConnectInvocation = (argv) => {
|
|
|
30
30
|
return first !== "connect" && !first.startsWith("-");
|
|
31
31
|
};
|
|
32
32
|
const printHelp = () => Console.log([
|
|
33
|
-
"Usage: localpreview connect <port|target-url> [--name subdomain] [-l|--local]",
|
|
34
|
-
" localpreview clean <subdomain> [--force] [-l|--local] [--admin-token token]",
|
|
35
|
-
" localpreview clean --all --force [-l|--local] [--admin-token token]",
|
|
33
|
+
"Usage: localpreview connect <port|target-url> [--name subdomain] [--capture host:port] [-l|--local] [--control-plane url]",
|
|
34
|
+
" localpreview clean <subdomain> [--force] [-l|--local] [--control-plane url] [--admin-token token]",
|
|
35
|
+
" localpreview clean --all --force [-l|--local] [--control-plane url] [--admin-token token]",
|
|
36
36
|
"",
|
|
37
37
|
"Examples:",
|
|
38
38
|
" localpreview connect 3000",
|
|
39
|
+
" localpreview connect 5173 --capture localhost:4000",
|
|
39
40
|
" localpreview connect https://localhost:3000 --name proyecto",
|
|
40
41
|
" localpreview connect 3000 -l",
|
|
42
|
+
" localpreview connect 3000 --control-plane https://staging.localpreview.dev",
|
|
41
43
|
" localpreview clean proyecto --force",
|
|
42
44
|
" localpreview clean --all --force",
|
|
45
|
+
" localpreview clean demo --control-plane https://staging.localpreview.dev --admin-token ...",
|
|
43
46
|
].join("\n"));
|
|
44
47
|
const parseCommand = (argv) => {
|
|
45
48
|
if (argv[0] === "clean") {
|
|
@@ -75,14 +78,20 @@ const parseConnectArgs = (argv) => {
|
|
|
75
78
|
const target = rest.shift();
|
|
76
79
|
if (target === undefined || target.startsWith("-")) {
|
|
77
80
|
return {
|
|
78
|
-
message: "Usage: localpreview connect <port|target-url> [--name subdomain] [-l|--local]",
|
|
81
|
+
message: "Usage: localpreview connect <port|target-url> [--name subdomain] [--capture host:port] [-l|--local] [--control-plane url]",
|
|
79
82
|
ok: false,
|
|
80
83
|
};
|
|
81
84
|
}
|
|
82
85
|
let controlPlane;
|
|
83
86
|
let requestedName;
|
|
87
|
+
let usedLocalControlPlane = false;
|
|
88
|
+
let usedControlPlaneFlag = false;
|
|
89
|
+
const captures = [];
|
|
84
90
|
for (let index = 0; index < rest.length; index += 1) {
|
|
85
91
|
const arg = rest[index];
|
|
92
|
+
if (arg === undefined) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
86
95
|
if (arg === "--name") {
|
|
87
96
|
const value = readRequiredOptionValue(rest, index, "--name");
|
|
88
97
|
if (!value.ok) {
|
|
@@ -92,8 +101,38 @@ const parseConnectArgs = (argv) => {
|
|
|
92
101
|
index += 1;
|
|
93
102
|
continue;
|
|
94
103
|
}
|
|
95
|
-
if (arg === "
|
|
96
|
-
|
|
104
|
+
if (arg === "--capture") {
|
|
105
|
+
const value = readRequiredOptionValue(rest, index, "--capture");
|
|
106
|
+
if (!value.ok) {
|
|
107
|
+
return value;
|
|
108
|
+
}
|
|
109
|
+
const parsedCapture = parseCaptureHostPort(value.value);
|
|
110
|
+
if (!parsedCapture.ok) {
|
|
111
|
+
return {
|
|
112
|
+
message: parsedCapture.message,
|
|
113
|
+
ok: false,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
captures.push(parsedCapture.capture);
|
|
117
|
+
index += 1;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const controlPlaneFlag = readControlPlaneFlag(rest, index, arg, {
|
|
121
|
+
usedControlPlaneFlag,
|
|
122
|
+
usedLocalControlPlane,
|
|
123
|
+
});
|
|
124
|
+
if (!controlPlaneFlag.ok) {
|
|
125
|
+
return controlPlaneFlag;
|
|
126
|
+
}
|
|
127
|
+
if (controlPlaneFlag.handled) {
|
|
128
|
+
if (controlPlaneFlag.usedLocalControlPlane) {
|
|
129
|
+
usedLocalControlPlane = true;
|
|
130
|
+
}
|
|
131
|
+
if (controlPlaneFlag.usedControlPlaneFlag) {
|
|
132
|
+
usedControlPlaneFlag = true;
|
|
133
|
+
}
|
|
134
|
+
controlPlane = controlPlaneFlag.controlPlane ?? controlPlane;
|
|
135
|
+
index = controlPlaneFlag.nextIndex;
|
|
97
136
|
continue;
|
|
98
137
|
}
|
|
99
138
|
return {
|
|
@@ -103,6 +142,7 @@ const parseConnectArgs = (argv) => {
|
|
|
103
142
|
}
|
|
104
143
|
return {
|
|
105
144
|
config: {
|
|
145
|
+
captures,
|
|
106
146
|
controlPlane: toOption(controlPlane),
|
|
107
147
|
requestedName: toOption(requestedName),
|
|
108
148
|
target,
|
|
@@ -117,8 +157,13 @@ const parseCleanArgs = (argv) => {
|
|
|
117
157
|
let controlPlane;
|
|
118
158
|
let force = false;
|
|
119
159
|
let subdomain;
|
|
160
|
+
let usedLocalControlPlane = false;
|
|
161
|
+
let usedControlPlaneFlag = false;
|
|
120
162
|
for (let index = 0; index < rest.length; index += 1) {
|
|
121
163
|
const arg = rest[index];
|
|
164
|
+
if (arg === undefined) {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
122
167
|
if (arg === "--admin-token") {
|
|
123
168
|
const value = readRequiredOptionValue(rest, index, "--admin-token");
|
|
124
169
|
if (!value.ok) {
|
|
@@ -136,8 +181,22 @@ const parseCleanArgs = (argv) => {
|
|
|
136
181
|
force = true;
|
|
137
182
|
continue;
|
|
138
183
|
}
|
|
139
|
-
|
|
140
|
-
|
|
184
|
+
const controlPlaneFlag = readControlPlaneFlag(rest, index, arg, {
|
|
185
|
+
usedControlPlaneFlag,
|
|
186
|
+
usedLocalControlPlane,
|
|
187
|
+
});
|
|
188
|
+
if (!controlPlaneFlag.ok) {
|
|
189
|
+
return controlPlaneFlag;
|
|
190
|
+
}
|
|
191
|
+
if (controlPlaneFlag.handled) {
|
|
192
|
+
if (controlPlaneFlag.usedLocalControlPlane) {
|
|
193
|
+
usedLocalControlPlane = true;
|
|
194
|
+
}
|
|
195
|
+
if (controlPlaneFlag.usedControlPlaneFlag) {
|
|
196
|
+
usedControlPlaneFlag = true;
|
|
197
|
+
}
|
|
198
|
+
controlPlane = controlPlaneFlag.controlPlane ?? controlPlane;
|
|
199
|
+
index = controlPlaneFlag.nextIndex;
|
|
141
200
|
continue;
|
|
142
201
|
}
|
|
143
202
|
if (arg !== undefined && !arg.startsWith("-") && subdomain === undefined) {
|
|
@@ -163,7 +222,7 @@ const parseCleanArgs = (argv) => {
|
|
|
163
222
|
}
|
|
164
223
|
if (!all && subdomain === undefined) {
|
|
165
224
|
return {
|
|
166
|
-
message: "Usage: localpreview clean <subdomain> [--force] [-l|--local] [--admin-token token]\n localpreview clean --all --force [-l|--local] [--admin-token token]",
|
|
225
|
+
message: "Usage: localpreview clean <subdomain> [--force] [-l|--local] [--control-plane url] [--admin-token token]\n localpreview clean --all --force [-l|--local] [--control-plane url] [--admin-token token]",
|
|
167
226
|
ok: false,
|
|
168
227
|
};
|
|
169
228
|
}
|
|
@@ -178,6 +237,53 @@ const parseCleanArgs = (argv) => {
|
|
|
178
237
|
ok: true,
|
|
179
238
|
};
|
|
180
239
|
};
|
|
240
|
+
const readControlPlaneFlag = (argv, index, arg, state) => {
|
|
241
|
+
if (arg === "-l" || arg === "--local") {
|
|
242
|
+
if (state.usedControlPlaneFlag) {
|
|
243
|
+
return {
|
|
244
|
+
message: "Use either -l/--local or --control-plane, not both.",
|
|
245
|
+
ok: false,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
controlPlane: LOCAL_CONTROL_PLANE_URL,
|
|
250
|
+
handled: true,
|
|
251
|
+
nextIndex: index,
|
|
252
|
+
ok: true,
|
|
253
|
+
usedLocalControlPlane: true,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
if (arg === "--control-plane") {
|
|
257
|
+
if (state.usedLocalControlPlane) {
|
|
258
|
+
return {
|
|
259
|
+
message: "Use either -l/--local or --control-plane, not both.",
|
|
260
|
+
ok: false,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
const value = readRequiredOptionValue(argv, index, "--control-plane");
|
|
264
|
+
if (!value.ok) {
|
|
265
|
+
return value;
|
|
266
|
+
}
|
|
267
|
+
const normalized = normalizeControlPlaneUrl(value.value);
|
|
268
|
+
if (!normalized.ok) {
|
|
269
|
+
return {
|
|
270
|
+
message: normalized.message,
|
|
271
|
+
ok: false,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
return {
|
|
275
|
+
controlPlane: normalized.url,
|
|
276
|
+
handled: true,
|
|
277
|
+
nextIndex: index + 1,
|
|
278
|
+
ok: true,
|
|
279
|
+
usedControlPlaneFlag: true,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
return {
|
|
283
|
+
handled: false,
|
|
284
|
+
ok: true,
|
|
285
|
+
};
|
|
286
|
+
};
|
|
181
287
|
const readRequiredOptionValue = (argv, index, option) => {
|
|
182
288
|
const value = argv[index + 1];
|
|
183
289
|
if (value === undefined || value.startsWith("-")) {
|
|
@@ -221,7 +327,11 @@ const runConnect = (config, legacy) => Effect.scoped(Effect.gen(function* () {
|
|
|
221
327
|
const tunnel = yield* Effect.acquireRelease(controlPlane.createTunnel(cliConfig.controlPlaneUrl, requestedSubdomain === undefined ? {} : { requestedSubdomain }), (tunnel) => closeTunnelBestEffort(controlPlane, cliConfig.controlPlaneUrl, tunnel));
|
|
222
328
|
yield* Console.log(`Tunnel ready: ${tunnel.publicUrl}`);
|
|
223
329
|
yield* Console.log(`Forwarding to ${target.target.protocol}://${target.target.hostname}:${target.target.port}`);
|
|
224
|
-
|
|
330
|
+
if (config.captures.length > 0) {
|
|
331
|
+
const origins = config.captures.map((capture) => formatCaptureOrigin(capture)).join(", ");
|
|
332
|
+
yield* Console.error(`Warning: captured local backends (${origins}) are exposed through this preview URL. Anyone with the link can reach them.`);
|
|
333
|
+
}
|
|
334
|
+
yield* relay.connectAndServe(tunnel, target.target, config.captures);
|
|
225
335
|
}));
|
|
226
336
|
const validateRequestedName = (value) => Option.match(value, {
|
|
227
337
|
onNone: () => Effect.succeed(undefined),
|
|
@@ -270,7 +380,7 @@ const runClean = (config) => Effect.gen(function* () {
|
|
|
270
380
|
}
|
|
271
381
|
if (subdomain === undefined) {
|
|
272
382
|
return yield* Effect.fail(new CliUsageError({
|
|
273
|
-
message: "Usage: localpreview clean <subdomain> [--force] [-l|--local] [--admin-token token]",
|
|
383
|
+
message: "Usage: localpreview clean <subdomain> [--force] [-l|--local] [--control-plane url] [--admin-token token]",
|
|
274
384
|
}));
|
|
275
385
|
}
|
|
276
386
|
const validation = validateRequestedSubdomain(subdomain);
|
package/dist/config.d.ts
CHANGED
|
@@ -14,6 +14,15 @@ export declare class CliConfig extends CliConfig_base {
|
|
|
14
14
|
}
|
|
15
15
|
export declare const PUBLIC_CONTROL_PLANE_URL = "https://localpreview.dev";
|
|
16
16
|
export declare const LOCAL_CONTROL_PLANE_URL = "http://localhost:3000";
|
|
17
|
+
export type NormalizeControlPlaneUrlResult = {
|
|
18
|
+
readonly ok: true;
|
|
19
|
+
readonly url: string;
|
|
20
|
+
} | {
|
|
21
|
+
readonly message: string;
|
|
22
|
+
readonly ok: false;
|
|
23
|
+
};
|
|
24
|
+
/** Normalizes and validates a control-plane base URL from `--control-plane`. */
|
|
25
|
+
export declare const normalizeControlPlaneUrl: (value: string) => NormalizeControlPlaneUrlResult;
|
|
17
26
|
export declare const makeCliConfig: (input: {
|
|
18
27
|
readonly controlPlaneUrl?: string;
|
|
19
28
|
readonly env?: NodeJS.ProcessEnv;
|
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,iBAAiB,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/C,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;AAE/D,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,cAoBH,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"}
|
|
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,iBAAiB,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/C,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;AAE/D,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,cAoBH,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
|
@@ -4,6 +4,77 @@ export class CliConfig extends Context.Service()("CliConfig") {
|
|
|
4
4
|
}
|
|
5
5
|
export const PUBLIC_CONTROL_PLANE_URL = LOCALPREVIEW_PUBLIC_ORIGIN;
|
|
6
6
|
export const LOCAL_CONTROL_PLANE_URL = "http://localhost:3000";
|
|
7
|
+
/** Normalizes and validates a control-plane base URL from `--control-plane`. */
|
|
8
|
+
export const normalizeControlPlaneUrl = (value) => {
|
|
9
|
+
let parsed;
|
|
10
|
+
try {
|
|
11
|
+
parsed = new URL(value);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return {
|
|
15
|
+
message: "Control plane URL must be a valid URL.",
|
|
16
|
+
ok: false,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
if (parsed.username.length > 0 || parsed.password.length > 0) {
|
|
20
|
+
return {
|
|
21
|
+
message: "Control plane URL must not include credentials.",
|
|
22
|
+
ok: false,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
if (parsed.search.length > 0 || parsed.hash.length > 0) {
|
|
26
|
+
return {
|
|
27
|
+
message: "Control plane URL must not include query parameters or fragments.",
|
|
28
|
+
ok: false,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
if (parsed.pathname !== "/" && parsed.pathname.length > 0) {
|
|
32
|
+
return {
|
|
33
|
+
message: "Control plane URL must not include a path.",
|
|
34
|
+
ok: false,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
38
|
+
if (parsed.protocol === "https:") {
|
|
39
|
+
return {
|
|
40
|
+
ok: true,
|
|
41
|
+
url: `${parsed.protocol}//${parsed.host}`,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
if (parsed.protocol === "http:" && isLocalControlPlaneHostname(hostname)) {
|
|
45
|
+
return {
|
|
46
|
+
ok: true,
|
|
47
|
+
url: `${parsed.protocol}//${parsed.host}`,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
if (parsed.protocol === "http:") {
|
|
51
|
+
return {
|
|
52
|
+
message: "Remote control plane URLs must use HTTPS.",
|
|
53
|
+
ok: false,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
message: "Control plane URL must use HTTP or HTTPS.",
|
|
58
|
+
ok: false,
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
const isLocalControlPlaneHostname = (hostname) => hostname === "localhost" || hostname.endsWith(".localhost") || isIpv4LoopbackAddress(hostname);
|
|
62
|
+
const isIpv4LoopbackAddress = (hostname) => {
|
|
63
|
+
const parts = hostname.split(".");
|
|
64
|
+
if (parts.length !== 4) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
for (const part of parts) {
|
|
68
|
+
if (!/^\d{1,3}$/.test(part)) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
const octet = Number(part);
|
|
72
|
+
if (!Number.isInteger(octet) || octet < 0 || octet > 255) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return Number(parts[0]) === 127;
|
|
77
|
+
};
|
|
7
78
|
export const makeCliConfig = (input) => {
|
|
8
79
|
const env = input.env ?? process.env;
|
|
9
80
|
const controlPlaneUrl = input.controlPlaneUrl ?? PUBLIC_CONTROL_PLANE_URL;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"control-plane.d.ts","sourceRoot":"","sources":["../src/control-plane.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
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,4 +1,4 @@
|
|
|
1
|
-
import { LOCALPREVIEW_PROTOCOL_VERSION, LOCALPREVIEW_RELAY_SNAPSHOT_VERSION, } from "@localpreview/protocol";
|
|
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
3
|
import { ControlPlaneError } from "./errors.js";
|
|
4
4
|
export class ControlPlaneClient extends Context.Service()("ControlPlaneClient") {
|
|
@@ -38,7 +38,7 @@ const createTunnelEffect = (controlPlaneUrl, body) => Effect.promise(async () =>
|
|
|
38
38
|
}
|
|
39
39
|
const json = await readJson(response, "Tunnel creation");
|
|
40
40
|
if (!response.ok) {
|
|
41
|
-
if (json.error?.code ===
|
|
41
|
+
if (json.error?.code === LOCALPREVIEW_ERROR_CODES.SUBDOMAIN_TAKEN) {
|
|
42
42
|
throw new ControlPlaneError({
|
|
43
43
|
message: `Subdomain "${json.error.subdomain ?? "requested"}" is already in use.`,
|
|
44
44
|
});
|
|
@@ -81,7 +81,7 @@ const cleanSubdomainEffect = (controlPlaneUrl, input) => Effect.promise(async ()
|
|
|
81
81
|
try {
|
|
82
82
|
response = await fetch(url, {
|
|
83
83
|
headers: {
|
|
84
|
-
|
|
84
|
+
[LOCALPREVIEW_ADMIN_TOKEN_HEADER]: input.adminToken,
|
|
85
85
|
},
|
|
86
86
|
method: "DELETE",
|
|
87
87
|
});
|
|
@@ -109,7 +109,7 @@ const cleanAllSubdomainsEffect = (controlPlaneUrl, input) => Effect.promise(asyn
|
|
|
109
109
|
try {
|
|
110
110
|
response = await fetch(url, {
|
|
111
111
|
headers: {
|
|
112
|
-
|
|
112
|
+
[LOCALPREVIEW_ADMIN_TOKEN_HEADER]: input.adminToken,
|
|
113
113
|
},
|
|
114
114
|
method: "DELETE",
|
|
115
115
|
});
|
package/dist/local-proxy.d.ts
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
|
-
import { type HeaderPairs, type TunnelTarget } from "@localpreview/protocol";
|
|
1
|
+
import { type CaptureTarget, type HeaderPairs, type TunnelTarget } from "@localpreview/protocol";
|
|
2
2
|
import { Context, Effect, Layer } from "effect";
|
|
3
3
|
import type WebSocket from "ws";
|
|
4
4
|
import { CliConfig } from "./config.js";
|
|
5
5
|
import { RelayProtocolError } from "./errors.js";
|
|
6
|
+
export type ProxySession = {
|
|
7
|
+
readonly captures: ReadonlyArray<CaptureTarget>;
|
|
8
|
+
readonly target: TunnelTarget;
|
|
9
|
+
};
|
|
6
10
|
export type LocalProxyShape = {
|
|
7
|
-
readonly handleMessage: (socket: WebSocket,
|
|
11
|
+
readonly handleMessage: (socket: WebSocket, session: ProxySession, input: string) => Effect.Effect<void, RelayProtocolError>;
|
|
8
12
|
};
|
|
9
13
|
declare const LocalProxy_base: Context.ServiceClass<LocalProxy, "LocalProxy", LocalProxyShape>;
|
|
10
14
|
export declare class LocalProxy extends LocalProxy_base {
|
|
11
15
|
}
|
|
12
16
|
export declare const LocalProxyLive: Layer.Layer<LocalProxy, never, CliConfig>;
|
|
17
|
+
export declare const rewriteCaptureSetCookie: (cookie: string, pathPrefix: string) => string;
|
|
13
18
|
export declare const filterLocalResponseHeaders: (headers: Headers) => HeaderPairs;
|
|
14
19
|
export {};
|
|
15
20
|
//# sourceMappingURL=local-proxy.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"local-proxy.d.ts","sourceRoot":"","sources":["../src/local-proxy.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
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;AAahC,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;AA4UF,eAAO,MAAM,uBAAuB,GAAI,QAAQ,MAAM,EAAE,YAAY,MAAM,KAAG,MAqC5E,CAAC;AA0BF,eAAO,MAAM,0BAA0B,GAAI,SAAS,OAAO,KAAG,WAqB7D,CAAC"}
|
package/dist/local-proxy.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { decodeServerRelayMessage, encodeRelayMessage, filterEndToEndHeaderPairs, } from "@localpreview/protocol";
|
|
1
|
+
import { decodeServerRelayMessage, encodeRelayMessage, filterEndToEndHeaderPairs, isLoopbackHost, } from "@localpreview/protocol";
|
|
2
2
|
import { Console, Context, Effect, Fiber, Layer, Ref } from "effect";
|
|
3
|
+
import { buildCaptureCookiePathPrefix, resolveRoute, } from "./capture-route.js";
|
|
4
|
+
import { buildCaptureShimScript, buildFrontendOrigin, injectCaptureShim, isHtmlResponse, stripContentSecurityPolicy, } from "./capture-shim.js";
|
|
3
5
|
import { CliConfig } from "./config.js";
|
|
4
6
|
import { LocalRequestError, RelayProtocolError, RequestLimitError, errorMessage, } from "./errors.js";
|
|
5
7
|
export class LocalProxy extends Context.Service()("LocalProxy") {
|
|
@@ -8,10 +10,10 @@ export const LocalProxyLive = Layer.effect(LocalProxy)(Effect.gen(function* () {
|
|
|
8
10
|
const config = yield* CliConfig;
|
|
9
11
|
const requests = yield* Ref.make(new Map());
|
|
10
12
|
return {
|
|
11
|
-
handleMessage: (socket,
|
|
13
|
+
handleMessage: (socket, session, input) => handleMessage(config, requests, socket, session, input),
|
|
12
14
|
};
|
|
13
15
|
}));
|
|
14
|
-
const handleMessage = (config, requests, socket,
|
|
16
|
+
const handleMessage = (config, requests, socket, session, input) => Effect.gen(function* () {
|
|
15
17
|
const decoded = decodeServerRelayMessage(input);
|
|
16
18
|
if (!decoded.ok) {
|
|
17
19
|
return yield* Effect.fail(new RelayProtocolError({
|
|
@@ -25,7 +27,7 @@ const handleMessage = (config, requests, socket, target, input) => Effect.gen(fu
|
|
|
25
27
|
case "request-chunk":
|
|
26
28
|
return yield* handleRequestChunk(config, requests, socket, message);
|
|
27
29
|
case "request-end":
|
|
28
|
-
return yield* handleRequestEnd(config, requests, socket,
|
|
30
|
+
return yield* handleRequestEnd(config, requests, socket, session, message);
|
|
29
31
|
case "cancel":
|
|
30
32
|
return yield* handleCancel(requests, message.requestId);
|
|
31
33
|
}
|
|
@@ -77,7 +79,7 @@ const handleRequestChunk = (config, requests, socket, message) => Effect.gen(fun
|
|
|
77
79
|
});
|
|
78
80
|
yield* Ref.set(requests, next);
|
|
79
81
|
});
|
|
80
|
-
const handleRequestEnd = (config, requests, socket,
|
|
82
|
+
const handleRequestEnd = (config, requests, socket, session, message) => Effect.gen(function* () {
|
|
81
83
|
const current = yield* Ref.get(requests);
|
|
82
84
|
const request = current.get(message.requestId);
|
|
83
85
|
if (request === undefined) {
|
|
@@ -88,7 +90,7 @@ const handleRequestEnd = (config, requests, socket, target, message) => Effect.g
|
|
|
88
90
|
});
|
|
89
91
|
return;
|
|
90
92
|
}
|
|
91
|
-
const fiber = yield*
|
|
93
|
+
const fiber = yield* proxyRequest(config, socket, session, message.requestId, request).pipe(Effect.catch((error) => send(socket, {
|
|
92
94
|
message: error.message,
|
|
93
95
|
requestId: message.requestId,
|
|
94
96
|
type: "response-error",
|
|
@@ -111,15 +113,28 @@ const handleCancel = (requests, requestId) => Effect.gen(function* () {
|
|
|
111
113
|
next.delete(requestId);
|
|
112
114
|
yield* Ref.set(requests, next);
|
|
113
115
|
});
|
|
114
|
-
const
|
|
116
|
+
const proxyRequest = (config, socket, session, requestId, request) => Effect.gen(function* () {
|
|
117
|
+
const route = resolveRoute(request.path, session.target, session.captures);
|
|
118
|
+
if (route.kind === "invalid-capture") {
|
|
119
|
+
return yield* Effect.fail(new LocalRequestError({
|
|
120
|
+
message: route.message,
|
|
121
|
+
requestId,
|
|
122
|
+
}));
|
|
123
|
+
}
|
|
115
124
|
const startedAt = Date.now();
|
|
116
|
-
const
|
|
125
|
+
const upstreamPath = route.kind === "capture" ? route.capturePath.path : request.path;
|
|
126
|
+
const targetUrl = new URL(upstreamPath, `${route.target.protocol}://${route.target.hostname}:${route.target.port}`);
|
|
117
127
|
const headers = new Headers(filterEndToEndHeaderPairs(request.headers).map(([name, value]) => [
|
|
118
128
|
name,
|
|
119
129
|
value,
|
|
120
130
|
]));
|
|
121
131
|
headers.set("accept-encoding", "identity");
|
|
122
|
-
headers.set("host", `${target.hostname}:${target.port}`);
|
|
132
|
+
headers.set("host", `${route.target.hostname}:${route.target.port}`);
|
|
133
|
+
if (route.kind === "capture") {
|
|
134
|
+
const frontendOrigin = buildFrontendOrigin(session.target);
|
|
135
|
+
headers.set("origin", frontendOrigin);
|
|
136
|
+
headers.set("referer", `${frontendOrigin}/`);
|
|
137
|
+
}
|
|
123
138
|
const body = Buffer.concat(request.chunks);
|
|
124
139
|
const init = {
|
|
125
140
|
headers,
|
|
@@ -137,7 +152,7 @@ const proxyToLocalTarget = (config, socket, target, requestId, request) => Effec
|
|
|
137
152
|
message: errorMessage(error),
|
|
138
153
|
requestId,
|
|
139
154
|
})));
|
|
140
|
-
|
|
155
|
+
let responseBody = yield* Effect.promise(() => response.arrayBuffer()).pipe(timeoutFail(config.requestTimeoutMs, new LocalRequestError({
|
|
141
156
|
message: "Local request timed out.",
|
|
142
157
|
requestId,
|
|
143
158
|
})), Effect.map((arrayBuffer) => Buffer.from(arrayBuffer)), Effect.mapError((error) => error instanceof LocalRequestError
|
|
@@ -153,8 +168,24 @@ const proxyToLocalTarget = (config, socket, target, requestId, request) => Effec
|
|
|
153
168
|
requestId,
|
|
154
169
|
}));
|
|
155
170
|
}
|
|
171
|
+
const contentType = response.headers.get("content-type");
|
|
172
|
+
const shouldInjectShim = route.kind === "primary" &&
|
|
173
|
+
session.captures.length > 0 &&
|
|
174
|
+
isHtmlResponse(contentType);
|
|
175
|
+
if (shouldInjectShim) {
|
|
176
|
+
const html = responseBody.toString("utf8");
|
|
177
|
+
const shimScript = buildCaptureShimScript(session.captures);
|
|
178
|
+
responseBody = Buffer.from(injectCaptureShim(html, shimScript), "utf8");
|
|
179
|
+
}
|
|
180
|
+
let responseHeaders = filterLocalResponseHeaders(response.headers);
|
|
181
|
+
if (route.kind === "capture") {
|
|
182
|
+
responseHeaders = rewriteCaptureResponseHeaders(responseHeaders, route.capturePath);
|
|
183
|
+
}
|
|
184
|
+
if (shouldInjectShim) {
|
|
185
|
+
responseHeaders = stripContentSecurityPolicy(responseHeaders);
|
|
186
|
+
}
|
|
156
187
|
yield* send(socket, {
|
|
157
|
-
headers:
|
|
188
|
+
headers: responseHeaders,
|
|
158
189
|
requestId,
|
|
159
190
|
status: response.status,
|
|
160
191
|
type: "response-start",
|
|
@@ -173,6 +204,46 @@ const proxyToLocalTarget = (config, socket, target, requestId, request) => Effec
|
|
|
173
204
|
});
|
|
174
205
|
yield* Console.log(`<- ${response.status} ${logPath(request.path)} ${Date.now() - startedAt}ms`);
|
|
175
206
|
});
|
|
207
|
+
const rewriteCaptureResponseHeaders = (headers, capturePath) => {
|
|
208
|
+
const pathPrefix = buildCaptureCookiePathPrefix(capturePath);
|
|
209
|
+
const next = [];
|
|
210
|
+
for (const [name, value] of headers) {
|
|
211
|
+
if (name.toLowerCase() === "set-cookie") {
|
|
212
|
+
next.push(["set-cookie", rewriteCaptureSetCookie(value, pathPrefix)]);
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
next.push([name, value]);
|
|
216
|
+
}
|
|
217
|
+
return next;
|
|
218
|
+
};
|
|
219
|
+
export const rewriteCaptureSetCookie = (cookie, pathPrefix) => {
|
|
220
|
+
const parts = cookie.split(";").map((part) => part.trim());
|
|
221
|
+
const nameValue = parts[0];
|
|
222
|
+
if (nameValue === undefined) {
|
|
223
|
+
return cookie;
|
|
224
|
+
}
|
|
225
|
+
const preserved = [];
|
|
226
|
+
let pathValue;
|
|
227
|
+
for (const attribute of parts.slice(1)) {
|
|
228
|
+
const lower = attribute.toLowerCase();
|
|
229
|
+
if (lower.startsWith("domain=")) {
|
|
230
|
+
const domain = attribute.slice("domain=".length).trim().replace(/^\./, "");
|
|
231
|
+
if (isLoopbackHost(domain) || domain === "localhost") {
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (lower.startsWith("path=")) {
|
|
236
|
+
pathValue = attribute.slice("path=".length).trim();
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
preserved.push(attribute);
|
|
240
|
+
}
|
|
241
|
+
const originalPath = pathValue ?? "/";
|
|
242
|
+
const isolatedPath = originalPath === "/"
|
|
243
|
+
? `${pathPrefix}/`
|
|
244
|
+
: `${pathPrefix}${originalPath.startsWith("/") ? originalPath : `/${originalPath}`}`;
|
|
245
|
+
return [nameValue, `Path=${isolatedPath}`, ...preserved].join("; ");
|
|
246
|
+
};
|
|
176
247
|
const sendRequestError = (socket, requestId, error) => send(socket, {
|
|
177
248
|
message: `${error.message} Limit is ${error.limitBytes} bytes.`,
|
|
178
249
|
requestId,
|
package/dist/relay-client.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type { CreateTunnelResponse, TunnelTarget } from "@localpreview/protocol";
|
|
1
|
+
import type { CaptureTarget, CreateTunnelResponse, TunnelTarget } from "@localpreview/protocol";
|
|
2
2
|
import { Context, 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) => Effect.Effect<void, RelayConnectionError | RelayProtocolError>;
|
|
7
|
+
readonly connectAndServe: (tunnel: CreateTunnelResponse, target: TunnelTarget, captures: ReadonlyArray<CaptureTarget>) => Effect.Effect<void, RelayConnectionError | RelayProtocolError>;
|
|
8
8
|
};
|
|
9
9
|
declare const RelayClient_base: Context.ServiceClass<RelayClient, "RelayClient", RelayClientShape>;
|
|
10
10
|
export declare class RelayClient extends RelayClient_base {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"relay-client.d.ts","sourceRoot":"","sources":["../src/relay-client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,YAAY,EAAE,MAAM,wBAAwB,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;AAE7E,OAAO,EAAE,SAAS,EAAuB,MAAM,aAAa,CAAC;AAC7D,OAAO,EAAE,oBAAoB,EAAE,kBAAkB,EAAgB,MAAM,aAAa,CAAC;AACrF,OAAO,EAAE,UAAU,EAAwB,MAAM,kBAAkB,CAAC;AAEpE,MAAM,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,CAAC,eAAe,EAAE,CACxB,MAAM,EAAE,oBAAoB,EAC5B,MAAM,EAAE,YAAY,EACpB,QAAQ,EAAE,aAAa,CAAC,aAAa,CAAC,KACnC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,oBAAoB,GAAG,kBAAkB,CAAC,CAAC;CACrE,CAAC;;AAEF,qBAAa,WAAY,SAAQ,gBAA+D;CAAG;AAEnG,eAAO,MAAM,eAAe,yDAU3B,CAAC;AA6LF,eAAO,MAAM,oBAAoB,GAAI,MAAM,MAAM,KAAG,OAAwB,CAAC"}
|
package/dist/relay-client.js
CHANGED
|
@@ -9,21 +9,22 @@ export const RelayClientLive = Layer.effect(RelayClient)(Effect.gen(function* ()
|
|
|
9
9
|
const config = yield* CliConfig;
|
|
10
10
|
const localProxy = yield* LocalProxy;
|
|
11
11
|
return {
|
|
12
|
-
connectAndServe: (tunnel, target) => connectAndServe(config, localProxy, tunnel, target),
|
|
12
|
+
connectAndServe: (tunnel, target, captures) => connectAndServe(config, localProxy, tunnel, target, captures),
|
|
13
13
|
};
|
|
14
14
|
}));
|
|
15
|
-
const connectAndServe = (config, localProxy, tunnel, target) => serveWithReconnect(config, localProxy, tunnel, target);
|
|
16
|
-
const serveWithReconnect = (config, localProxy, tunnel, target) => serveOnce(config, localProxy, tunnel, target).pipe(Effect.catch((error) => {
|
|
15
|
+
const connectAndServe = (config, localProxy, tunnel, target, captures) => serveWithReconnect(config, localProxy, tunnel, target, captures);
|
|
16
|
+
const serveWithReconnect = (config, localProxy, tunnel, target, captures) => serveOnce(config, localProxy, tunnel, target, captures).pipe(Effect.catch((error) => {
|
|
17
17
|
if (error instanceof RelayConnectionError && error.retryable === true) {
|
|
18
|
-
return Console.error(`${error.message}; reconnecting...`).pipe(Effect.andThen(Effect.sleep("1 second")), Effect.andThen(serveWithReconnect(config, localProxy, tunnel, target)));
|
|
18
|
+
return Console.error(`${error.message}; reconnecting...`).pipe(Effect.andThen(Effect.sleep("1 second")), Effect.andThen(serveWithReconnect(config, localProxy, tunnel, target, captures)));
|
|
19
19
|
}
|
|
20
20
|
return Effect.fail(error);
|
|
21
21
|
}));
|
|
22
|
-
const serveOnce = (config, localProxy, tunnel, target) => Effect.gen(function* () {
|
|
22
|
+
const serveOnce = (config, localProxy, tunnel, target, captures) => Effect.gen(function* () {
|
|
23
23
|
const socket = yield* openSocket(tunnel).pipe(Effect.retry({
|
|
24
24
|
schedule: Schedule.recurs(Math.ceil(config.relayConnectTimeoutMs / 250)),
|
|
25
25
|
}));
|
|
26
26
|
yield* Console.log("Connected to relay.");
|
|
27
|
+
const session = { captures, target };
|
|
27
28
|
const done = yield* Deferred.make();
|
|
28
29
|
const handleSignal = () => {
|
|
29
30
|
socket.close(1000, "Interrupted");
|
|
@@ -34,7 +35,7 @@ const serveOnce = (config, localProxy, tunnel, target) => Effect.gen(function* (
|
|
|
34
35
|
process.once("SIGINT", handleSignal);
|
|
35
36
|
process.once("SIGTERM", handleSignal);
|
|
36
37
|
socket.on("message", (data) => {
|
|
37
|
-
const effect = localProxy.handleMessage(socket,
|
|
38
|
+
const effect = localProxy.handleMessage(socket, session, data.toString()).pipe(Effect.catch((error) => Effect.gen(function* () {
|
|
38
39
|
socket.close(1002, error.message);
|
|
39
40
|
yield* Deferred.fail(done, error);
|
|
40
41
|
})));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "localpreview",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"bin": {
|
|
5
5
|
"localpreview": "./dist/index.js"
|
|
6
6
|
},
|
|
@@ -10,25 +10,27 @@
|
|
|
10
10
|
"README.md"
|
|
11
11
|
],
|
|
12
12
|
"types": "./dist/index.d.ts",
|
|
13
|
-
"dependencies": {
|
|
14
|
-
"@effect/platform-node": "beta",
|
|
15
|
-
"effect": "beta",
|
|
16
|
-
"ws": "latest",
|
|
17
|
-
"@localpreview/protocol": "0.1.0"
|
|
18
|
-
},
|
|
19
|
-
"devDependencies": {
|
|
20
|
-
"@effect/vitest": "beta",
|
|
21
|
-
"@types/node": "latest",
|
|
22
|
-
"@types/ws": "latest",
|
|
23
|
-
"tsx": "latest",
|
|
24
|
-
"typescript": "latest",
|
|
25
|
-
"vitest": "latest"
|
|
26
|
-
},
|
|
27
13
|
"scripts": {
|
|
28
14
|
"build": "tsc -p tsconfig.build.json",
|
|
29
15
|
"dev": "tsx src/index.ts",
|
|
30
16
|
"lint": "oxlint .",
|
|
17
|
+
"prepack": "pnpm build",
|
|
18
|
+
"prepublishOnly": "pnpm test && pnpm typecheck && pnpm build",
|
|
31
19
|
"test": "vitest run --passWithNoTests",
|
|
32
20
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@effect/platform-node": "beta",
|
|
24
|
+
"@localpreview/protocol": "^0.2.0",
|
|
25
|
+
"effect": "beta",
|
|
26
|
+
"ws": "latest"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@effect/vitest": "catalog:",
|
|
30
|
+
"@types/node": "catalog:",
|
|
31
|
+
"@types/ws": "catalog:",
|
|
32
|
+
"tsx": "catalog:",
|
|
33
|
+
"typescript": "catalog:",
|
|
34
|
+
"vitest": "catalog:"
|
|
33
35
|
}
|
|
34
|
-
}
|
|
36
|
+
}
|