landfall 0.5.1 → 0.5.3
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/README.md +10 -2
- package/dist/cli.mjs +126 -112
- package/dist/index.html +69 -6
- package/dist/server-BxTFpFmw.mjs +9 -0
- package/package.json +1 -1
- package/dist/server-DA-e5nav.mjs +0 -9
package/dist/index.html
CHANGED
|
@@ -176,6 +176,9 @@ td.acts button { margin-left: 4px; }
|
|
|
176
176
|
.note.info { background: var(--accent-bg); color: var(--accent); }
|
|
177
177
|
.kv { display: grid; grid-template-columns: max-content 1fr; gap: 6px 18px; font-size: var(--fs); margin: 0; }
|
|
178
178
|
.kv dt { color: var(--faint); } .kv dd { margin: 0; }
|
|
179
|
+
details.where summary { cursor: pointer; color: var(--faint); font-size: var(--fs-sm); }
|
|
180
|
+
details.where h3 { font-size: var(--fs-sm); margin: 12px 0 6px; }
|
|
181
|
+
details.where dl { margin: 0; font-size: var(--fs-sm); } details.where dt { overflow-wrap: anywhere; margin-top: 6px; } details.where dd { margin: 0; }
|
|
179
182
|
pre.block { background: var(--hover); border: 1px solid var(--line); padding: 10px 12px; border-radius: var(--r-sm); overflow-x: auto; font-size: var(--fs-sm); margin: 6px 0 0; }
|
|
180
183
|
.detail td { background: color-mix(in srgb, var(--hover) 60%, transparent); }
|
|
181
184
|
.releases, .services { margin: 0; padding: 0; list-style: none; }
|
|
@@ -285,6 +288,7 @@ body.dragging::after {
|
|
|
285
288
|
<header>
|
|
286
289
|
<a class="brand" href="#/"><svg viewBox="0 0 16 16" aria-hidden="true"><rect width="16" height="16" rx="4" fill="var(--accent)"/><path d="M4 5h8M4 8h8M4 11h5" stroke="#fff" stroke-width="1.6" stroke-linecap="round"/></svg>landfall <small id="version"></small></a>
|
|
287
290
|
<div class="project" id="project"></div>
|
|
291
|
+
<button type="button" class="sm ghost" id="app-settings" title="landfall's defaults for every server (lf config)">Defaults</button>
|
|
288
292
|
</header>
|
|
289
293
|
<!-- The filter stays put across renders, so typing keeps its focus. -->
|
|
290
294
|
<nav class="servers" id="nav">
|
|
@@ -743,7 +747,7 @@ function renderJob() {
|
|
|
743
747
|
* onOk(values) runs on submit; return false to keep it open, or {field, error} to keep it open and show why.
|
|
744
748
|
* typeToConfirm: a name the user types before the (dangerous) action is enabled.
|
|
745
749
|
*/
|
|
746
|
-
function dialog({ title, text = "", fields = [], ok = "OK", danger = false, onOk, extra = "", typeToConfirm = "" }) {
|
|
750
|
+
function dialog({ title, text = "", fields = [], ok = "OK", danger = false, onOk, extra = "", after = "", typeToConfirm = "" }) {
|
|
747
751
|
const dlg = openDlg();
|
|
748
752
|
const field = (f) => {
|
|
749
753
|
const id = `f-${f.name}`;
|
|
@@ -766,6 +770,7 @@ function dialog({ title, text = "", fields = [], ok = "OK", danger = false, onOk
|
|
|
766
770
|
${text ? `<div class="text">${esc(text)}</div>` : ""}
|
|
767
771
|
${extra}
|
|
768
772
|
${fields.map(group).join("")}
|
|
773
|
+
${after}
|
|
769
774
|
${typeToConfirm ? `<label>Type <b class="mono">${esc(typeToConfirm)}</b> to confirm<input id="dlg-confirm" autocomplete="off" spellcheck="false" autocapitalize="off"></label>` : ""}
|
|
770
775
|
<div class="note bad dlg-err" id="dlg-err" role="alert" hidden></div>
|
|
771
776
|
<div class="btns">
|
|
@@ -958,6 +963,43 @@ function addServer() {
|
|
|
958
963
|
|
|
959
964
|
const defaultServer = () => S.sel ?? S.state.servers.find((s) => s.isDefault)?.name;
|
|
960
965
|
const mainDomain = (n) => S.state.servers.find((s) => s.name === n)?.saved?.domains?.[0];
|
|
966
|
+
/** The server deploys new sites with no domain unless one is given: its own setting, else landfall's default. */
|
|
967
|
+
const tunnelOnly = (n) => S.state.servers.find((s) => s.name === n)?.saved?.tunnelOnly ?? S.state.defaults.tunnelOnly;
|
|
968
|
+
|
|
969
|
+
/** landfall's defaults for every server (lf config); a server's own setting wins. */
|
|
970
|
+
function defaultsDialog() {
|
|
971
|
+
const d = S.state.defaults;
|
|
972
|
+
dialog({
|
|
973
|
+
title: "landfall defaults",
|
|
974
|
+
text: `For every server, unless it sets its own. Saved in ${d.configFile}.`,
|
|
975
|
+
fields: [
|
|
976
|
+
{ name: "default", label: "Default server", type: "select", value: d.default ?? "", options: [{ value: "", label: "none" }, ...S.state.servers.map((s) => ({ value: s.name, label: s.name }))], hint: "Commands in a terminal use it without --server." },
|
|
977
|
+
{ name: "tunnelOnly", label: "New sites get no domain: tunnel only", type: "check", value: d.tunnelOnly, hint: "Nothing is served on the internet until a site gets a domain. A server's Settings can override it." },
|
|
978
|
+
{ name: "email", label: "Let's Encrypt e-mail (optional)", value: d.email ?? "", hint: "For servers that set none, when landfall installs Traefik." },
|
|
979
|
+
{ name: "keep", label: "Releases to keep for rollback", value: String(d.keep), placeholder: "5" },
|
|
980
|
+
],
|
|
981
|
+
after: whereList(d.locations),
|
|
982
|
+
ok: "Save",
|
|
983
|
+
onOk: (v) => {
|
|
984
|
+
const argv = ["config"];
|
|
985
|
+
if (v.default !== (d.default ?? "")) argv.push(...(v.default ? ["--default", v.default] : ["--unset", "default"]));
|
|
986
|
+
if (v.tunnelOnly !== d.tunnelOnly) argv.push(v.tunnelOnly ? "--tunnel-only" : "--no-tunnel-only");
|
|
987
|
+
const email = v.email.trim();
|
|
988
|
+
if (email !== (d.email ?? "")) argv.push(...(email ? ["--email", email] : ["--unset", "email"]));
|
|
989
|
+
const keep = v.keep.trim();
|
|
990
|
+
if (keep && (!/^\d+$/.test(keep) || !Number(keep))) return invalid("keep", "A whole number from 1 up");
|
|
991
|
+
if (keep !== String(d.keep)) argv.push(...(keep ? ["--keep", keep] : ["--unset", "keep"]));
|
|
992
|
+
if (argv.length === 1) return;
|
|
993
|
+
run(argv, { after: (j) => (j.info.code === 0 ? void (S.sel && load(S.sel, "settings", true), refreshState()) : undefined) });
|
|
994
|
+
},
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
/** Every place landfall writes to, from `lf config`: nothing it installs is hidden. */
|
|
998
|
+
const whereList = (loc) => {
|
|
999
|
+
const kv = (list) => `<dl>${list.map((l) => `<dt class="mono">${esc(l.path)}</dt><dd class="dim">${esc(l.what)}</dd>`).join("")}</dl>`;
|
|
1000
|
+
return `<details class="where"><summary>Where landfall puts things</summary>
|
|
1001
|
+
<h3>On this machine</h3>${kv(loc.local)}<h3>On each server</h3>${kv(loc.server)}</details>`;
|
|
1002
|
+
};
|
|
961
1003
|
/** What `lf deploy` picks without a domain: <site>.<main domain>, else the free sslip.io address. */
|
|
962
1004
|
function autoDomain(n, name) {
|
|
963
1005
|
const main = mainDomain(n);
|
|
@@ -1002,7 +1044,7 @@ function deployDialog(det, o = {}) {
|
|
|
1002
1044
|
? `Empty: <name>.${main}. A bare name like blog means blog.${main}, @ means ${main} itself (another site serving it hands it over). Other domains need an A record pointing at the server.`
|
|
1003
1045
|
: "Empty: a free <name>.<ip>.sslip.io address, no DNS needed. Own domains need an A record pointing at the server.",
|
|
1004
1046
|
},
|
|
1005
|
-
{ name: "noDomain", label: "No domain: reach it through a tunnel only", type: "check", value:
|
|
1047
|
+
{ name: "noDomain", label: "No domain: reach it through a tunnel only", type: "check", value: p?.domains ? !p.domains.length : !!tunnelOnly(target), hint: "Nothing is served on the internet. Give it a domain later with Domains… on the site." },
|
|
1006
1048
|
...(det.kind === "static"
|
|
1007
1049
|
? [
|
|
1008
1050
|
{ name: "build", label: "Build command (runs here, before upload)", value: det.build ?? "", placeholder: "none" },
|
|
@@ -1022,7 +1064,8 @@ function deployDialog(det, o = {}) {
|
|
|
1022
1064
|
if (!n) return invalid("name", "Give the site a name");
|
|
1023
1065
|
if (!v.server) return invalid("server", "Pick a server");
|
|
1024
1066
|
const domains = v.domains.split(/[\s,]+/).filter(Boolean);
|
|
1025
|
-
|
|
1067
|
+
// The server's default would leave it unrouted though the box is unticked.
|
|
1068
|
+
if (!v.noDomain && !domains.length && !p?.domains?.length && tunnelOnly(v.server)) return invalid("domains", `${v.server} gives new sites no domain: name one, or tick No domain`);
|
|
1026
1069
|
const argv = ["deploy", "-y", ...sv(v.server), "-n", n, ...(v.noDomain ? ["--no-domain"] : domains.flatMap((d) => ["-d", d]))];
|
|
1027
1070
|
if (v.build?.trim() && v.build.trim() !== p?.build) argv.push("--build", v.build.trim());
|
|
1028
1071
|
if (v.spa && !p?.spa) argv.push("--spa");
|
|
@@ -1040,6 +1083,16 @@ function deployDialog(det, o = {}) {
|
|
|
1040
1083
|
run(argv, { server: v.server, cwd: det.path, after: openAfter });
|
|
1041
1084
|
},
|
|
1042
1085
|
});
|
|
1086
|
+
noDomainToggle();
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
/** The open dialog's "no domain" box greys out its domains field: ticked, the site gets none. */
|
|
1090
|
+
function noDomainToggle() {
|
|
1091
|
+
const box = $("#f-noDomain"), input = $("#f-domains");
|
|
1092
|
+
if (!box || !input) return;
|
|
1093
|
+
const sync = () => (input.disabled = box.checked);
|
|
1094
|
+
box.addEventListener("change", sync);
|
|
1095
|
+
sync();
|
|
1043
1096
|
}
|
|
1044
1097
|
|
|
1045
1098
|
/** After a deploy: offer the address it printed. */
|
|
@@ -1179,20 +1232,22 @@ function domainsDialog(n, m) {
|
|
|
1179
1232
|
text: `Each domain needs an A record pointing at ${ips.join(", ") || "the server"}. Traefik fetches the certificate once it resolves.`,
|
|
1180
1233
|
fields: [
|
|
1181
1234
|
{ name: "domains", label: "Domains (space separated)", value: m.domains.join(" ") },
|
|
1235
|
+
{ name: "noDomain", label: "No domain: reach it through a tunnel only", type: "check", value: !m.domains.length, hint: "Nothing is served on the internet; the site keeps running. Open via tunnel reaches it." },
|
|
1182
1236
|
...(main && !m.domains.includes(main) ? [{ name: "apex", label: `Also serve ${main}`, type: "check", value: false, hint: servedByOther(n, m.name, main) ? `${servedByOther(n, m.name, main)} serves it now and hands it over.` : "The main domain itself." }] : []),
|
|
1183
1237
|
...(free && !m.domains.includes(free) ? [{ name: "free", label: main ? `Also add ${free}` : `Also add the free address ${free}`, type: "check", value: false, hint: main ? `Under the main domain: works once *.${main} points at the server.` : "Works right away, no DNS setup." }] : []),
|
|
1184
1238
|
],
|
|
1185
1239
|
ok: "Save",
|
|
1186
1240
|
onOk: (v) => {
|
|
1187
|
-
const domains = v.domains.split(/[\s,]+/).filter(Boolean);
|
|
1188
|
-
if (v.free) domains.push(free);
|
|
1189
|
-
if (v.apex) domains.push(main);
|
|
1241
|
+
const domains = v.noDomain ? [] : v.domains.split(/[\s,]+/).filter(Boolean);
|
|
1242
|
+
if (v.free && !v.noDomain) domains.push(free);
|
|
1243
|
+
if (v.apex && !v.noDomain) domains.push(main);
|
|
1190
1244
|
// None left: the site keeps running, unrouted.
|
|
1191
1245
|
const args = domains.length ? domains.flatMap((d) => ["--set", d]) : m.domains.flatMap((d) => ["--rm", d]);
|
|
1192
1246
|
if (!args.length) return;
|
|
1193
1247
|
run(["domains", m.name, ...sv(n), ...args, "-y"], { server: n });
|
|
1194
1248
|
},
|
|
1195
1249
|
});
|
|
1250
|
+
noDomainToggle();
|
|
1196
1251
|
}
|
|
1197
1252
|
|
|
1198
1253
|
function authDialog(n, m) {
|
|
@@ -1945,6 +2000,9 @@ function settingsView(n) {
|
|
|
1945
2000
|
const P = S.state.paths;
|
|
1946
2001
|
const ip = x.dns[0]?.ips[0] ?? "<server ip>";
|
|
1947
2002
|
const records = x.dns.flatMap((r) => [r.apex, r.wildcard]).filter((d) => !d.ok).map((d) => `${esc(d.domain)} A ${esc(ip)}`);
|
|
2003
|
+
const tunnel = x.tunnelOnly;
|
|
2004
|
+
const setTunnel = () =>
|
|
2005
|
+
run(["server", "domain", n, tunnel ? "--no-tunnel-only" : "--tunnel-only"], { server: n, after: (j) => (j.info.code === 0 ? void (load(n, "settings", true), refreshState()) : undefined) });
|
|
1948
2006
|
const saved = x.saved ? Object.entries(x.saved).filter(([k, v]) => v !== undefined && k !== "dashboard").map(([k, v]) => [k, `<span class="mono">${esc(String(v))}</span>`]) : [];
|
|
1949
2007
|
return `
|
|
1950
2008
|
<div class="card">
|
|
@@ -1960,6 +2018,10 @@ function settingsView(n) {
|
|
|
1960
2018
|
<p class="faint">New sites get <span class="mono"><site>.${esc(x.domains[0])}</span>; a bare name like <span class="mono">blog</span> means <span class="mono">blog.${esc(x.domains[0])}</span>, and <span class="mono">@</span> means <span class="mono">${esc(x.domains[0])}</span> itself.</p>
|
|
1961
2019
|
${records.length ? `<div class="note">Add at your DNS provider:<div class="mono">${records.join("<br>")}</div></div>` : ""}`
|
|
1962
2020
|
: `<p class="dim">No main domain set in landfall. New sites get a free <span class="mono"><site>.<ip>.sslip.io</span> address until you set one.</p>`}
|
|
2021
|
+
<div class="drow">
|
|
2022
|
+
<div>New sites: ${tunnel ? `<b>no domain</b> <span class="dim">· tunnel only until you give one (Domains… on the site, or deploy with a domain)</span>` : `<b>get a domain</b> <span class="dim">· unless you tick “No domain” when deploying</span>`}${x.tunnelOnlyOwn ? "" : ` <span class="faint">(landfall default)</span>`}</div>
|
|
2023
|
+
<div class="actions">${btn(tunnel ? "Give them a domain" : "Make them tunnel only", setTunnel, "sm", tunnel ? "lf server domain --no-tunnel-only" : "lf server domain --tunnel-only")}</div>
|
|
2024
|
+
</div>
|
|
1963
2025
|
${x.suggest.length ? `<div class="note">${x.domains.length ? "Other domains" : "Domains"} your sites on this server already use: ${x.suggest.map((g) => `<b>${esc(g.domain)}</b> <span class="faint">(${g.hosts})</span> ${btn("Add", () => run(["server", "domain", n, "--add", g.domain], reload), "sm")}`).join(" ")}</div>` : ""}</div>
|
|
1964
2026
|
</div>
|
|
1965
2027
|
<div class="card">
|
|
@@ -2073,6 +2135,7 @@ $("#filter").addEventListener("input", (e) => {
|
|
|
2073
2135
|
renderNav();
|
|
2074
2136
|
});
|
|
2075
2137
|
$("#add-server").onclick = () => addServer();
|
|
2138
|
+
$("#app-settings").onclick = () => defaultsDialog();
|
|
2076
2139
|
|
|
2077
2140
|
(async () => {
|
|
2078
2141
|
try {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import{A as e,C as t,D as n,E as r,M as i,N as a,O as o,P as s,S as c,T as l,_ as u,a as d,b as f,c as ee,d as te,f as ne,g as re,h as ie,i as p,j as ae,k as oe,l as se,m as ce,n as m,o as h,p as le,r as ue,s as de,t as g,u as _,v,w as y,x as b,y as fe}from"./cli.mjs";import{spawn as pe,spawnSync as x}from"node:child_process";import S from"node:fs";import{homedir as C}from"node:os";import{dirname as w,isAbsolute as me,join as T,resolve as E}from"node:path";import{createHash as D,randomBytes as O,timingSafeEqual as k}from"node:crypto";import{createServer as A}from"node:http";import{Readable as j,Transform as M}from"node:stream";function N(e){let t=E(!e||e===`~`?C():o(e)),n=S.readdirSync(t,{withFileTypes:!0}).filter(e=>!e.name.startsWith(`.`)&&(e.isDirectory()||e.isSymbolicLink()&&P(T(t,e.name)))).map(e=>e.name).sort((e,t)=>e.localeCompare(t));return{path:t,parent:w(t)===t?null:w(t),home:C(),dirs:n,detected:ce(t)}}const P=e=>{try{return S.statSync(e).isDirectory()}catch{return!1}},F=T(n,`uploads`),I=e=>e.split(/[\\/]+/).filter(e=>e&&e!==`.`&&e!==`..`);async function L(e,t){S.mkdirSync(w(e),{recursive:!0}),S.writeFileSync(e,Buffer.from(await t.arrayBuffer()))}async function R(e){S.mkdirSync(F,{recursive:!0,mode:448});for(let e of S.readdirSync(F)){let t=T(F,e);Date.now()-S.statSync(t).mtimeMs>864e5&&S.rmSync(t,{recursive:!0,force:!0})}let t=S.mkdtempSync(T(F,`u-`)),n=e.getAll(`file`).filter(e=>e instanceof File);if(!n.length)throw Error(`no files in the upload`);let r=e.getAll(`path`).map(String);if(n.length===1&&/\.(zip|tar|tgz|tar\.gz)$/i.test(n[0].name)){let e=T(t,`.upload-`+I(n[0].name).pop());await L(e,n[0]);let r=T(t,`files`);S.mkdirSync(r);let i=/\.zip$/i.test(e);if(z(e,i))throw Error(`${n[0].name} contains symlinks or hard links: upload the folder instead`);let a=i?x(`unzip`,[`-q`,e,`-d`,r]):x(`tar`,[`-xf`,e,`-C`,r]);if(a.error||a.status!==0){let t=i?x(`tar`,[`-xf`,e,`-C`,r]):a;if(t.error||t.status!==0)throw Error(`could not unpack ${n[0].name}`)}return S.rmSync(e),he(r),{path:B(r),files:ge(r)}}let i=T(t,`files`);for(let[e,t]of n.entries()){let n=I(r[e]||t.name);n.length&&await L(T(i,...n),t)}return{path:B(i),files:n.length}}function z(e,t){let n=(e,t)=>{let n=x(e,t,{encoding:`utf8`,maxBuffer:256*1024**2});return n.error||n.status!==0?void 0:n.stdout},r=t?n(`zipinfo`,[e])??n(`tar`,[`-tvf`,e]):n(`tar`,[`-tvf`,e]);if(r===void 0)throw Error(`could not read ${e.split(`/`).pop()}`);return r.split(`
|
|
2
|
+
`).some(e=>/^[lh]/.test(e)||/ link to /.test(e))}function he(e){for(let t of S.readdirSync(e,{withFileTypes:!0,recursive:!0}))t.isSymbolicLink()&&S.rmSync(T(t.parentPath,t.name),{force:!0})}function B(e){for(;;){let t=S.readdirSync(e).filter(e=>e!==`__MACOSX`&&e!==`.DS_Store`);if(t.length!==1||!P(T(e,t[0])))return e;e=T(e,t[0])}}function ge(e){return S.readdirSync(e,{withFileTypes:!0,recursive:!0}).filter(e=>e.isFile()).length}function _e(e){if(e!=null&&e!==``){if(typeof e!=`string`||!me(e)||!P(e))throw Error(`cwd must be an existing folder`);return e}}const V=()=>[process.execPath,...process.execArgv,process.argv[1]],H=5e3,U=1e4,W=e=>e.length>U?e.slice(0,U)+`…`:e;function ve(e,t,n={}){let r={...process.env,NO_COLOR:`1`,LANDFALL_REPORT:`1`,...n.env};n.password&&Object.assign(r,se(n.password));let i=pe(V()[0],[...V().slice(1),...e],{stdio:[n.stdin===void 0?`ignore`:`pipe`,`pipe`,`pipe`],env:r,cwd:n.cwd,detached:!0});i.stdin?.on(`error`,()=>{}).end(n.stdin);let a={argv:e,lines:[],dropped:0,report:{},stop(){if(a.code===void 0){a.stopped=!0;try{process.kill(-i.pid,`SIGTERM`)}catch{}}}},o,s=()=>o??=setTimeout(()=>(o=void 0,t()),50),c=()=>{let e=``;return{push(t){let n=(e+t).split(`
|
|
3
|
+
`);e=n.pop(),e.length>U&&(n.push(e),e=``);for(let e of n)if(e.startsWith(``))try{Object.assign(a.report,JSON.parse(e.slice(1)))}catch{}else a.lines.push(W(e.slice(e.lastIndexOf(`\r`,e.length-2)+1).replace(/\r$/,``)));a.lines.length>H&&(a.dropped+=a.lines.splice(0,a.lines.length-H).length),s()},flush:()=>e&&a.lines.push(W(e))}},l=c(),u=c();return i.stdout.setEncoding(`utf8`).on(`data`,l.push),i.stderr.setEncoding(`utf8`).on(`data`,u.push),i.on(`error`,e=>{a.lines.push(`error: ${e.message}`),a.code=1,t()}),i.on(`close`,e=>{l.flush(),u.flush(),a.code=a.stopped?null:e??1,clearTimeout(o),t()}),a}const ye=S.readFileSync(new URL(`./index.html`,import.meta.url),`utf8`),be=512*1024**2,xe=1024**2,G=/* @__PURE__ */ new Map,Se=/* @__PURE__ */ new Set([`ui`,`web`]);function Ce(){let e=T(w(y),`ui-token`);try{let t=S.readFileSync(e,`utf8`).trim();if(t.length>=32)return t}catch{}let t=O(24).toString(`hex`);return S.mkdirSync(w(e),{recursive:!0,mode:448}),S.writeFileSync(e,t+`
|
|
4
|
+
`,{mode:384}),t}const K=(e,t=200)=>Response.json(e,{status:t,headers:{"cache-control":`no-store`}}),q=(e,t=500)=>K({error:e?.message??String(e)},t);function J(e,t){let n=re(),r=/* @__PURE__ */ new Set,{args:i,env:a,stdin:o}=we(e),s=ve(i,()=>r.forEach(e=>e()),{password:t.password,cwd:t.cwd,stdin:o,env:{...a,LANDFALL_RUN:n}}),c={id:n,argv:e,server:t.server,startedAt:(/* @__PURE__ */ new Date()).toISOString(),undoable:_(e),job:s,listeners:r,opts:t};G.set(n,c);for(let[e,t]of G){if(G.size<=50)break;t.job.code!==void 0&&G.delete(e)}return c}const Y=e=>e[0]===`env`||e[0]===`sites`&&e[1]===`env`,X=/^[A-Za-z_][A-Za-z0-9_]*=/;function we(e){let t=[],n={},r=[];for(let a=0;a<e.length;a++){let o=e[a],s=o.match(/^--((?:[\w-]+-)?password)(?:=(.*))?$/s);s&&(s[2]!==void 0||a+1<e.length)?n[i(s[1])]=s[2]??e[++a]:Y(e)&&X.test(o)&&!o.includes(`
|
|
5
|
+
`)&&!e.includes(`--env-file`)?r.push(o):t.push(o)}return r.length&&t.push(`--env-file`,`-`),{args:t,env:n,stdin:r.length?r.join(`
|
|
6
|
+
`)+`
|
|
7
|
+
`:void 0}}const Te=e=>e.map((t,n)=>/^(-p|--([\w-]+-)?password)$/.test(e[n-1])?`***`:Y(e)&&X.test(t)?t.replace(/=.*/s,`=***`):t.replace(/^(--([\w-]+-)?password=).*/,`$1***`)),Z=e=>({id:e.id,argv:Te(e.argv),server:e.server,startedAt:e.startedAt,undoable:e.undoable,code:e.job.code,stopped:!!e.job.stopped,report:e.job.report});function Ee(e,t,n){let r=new TextEncoder,i=t,a=()=>{},o=new ReadableStream({start(t){let o=e=>t.enqueue(r.encode(e)),s=()=>{let{lines:t,dropped:n,code:r}=e.job,s=Math.max(i,n);n+t.length>s&&(o(`data: ${JSON.stringify({from:s,lines:t.slice(s-n),report:e.job.report})}\n\n`),i=n+t.length),r!==void 0&&(o(`event: done\ndata: ${JSON.stringify(Z(e))}\n\n`),a())},c=setInterval(()=>o(`: beat
|
|
8
|
+
|
|
9
|
+
`),15e3);a=()=>{clearInterval(c),e.listeners.delete(s);try{t.close()}catch{}},e.listeners.add(s),n.addEventListener(`abort`,a),s()},cancel:()=>a()});return new Response(o,{headers:{"content-type":`text/event-stream`,"cache-control":`no-store`}})}async function De(n,i){let[a,o,s]=i,u=n.method;if(a===`state`&&u===`GET`){let n=oe();return K({version:g,cwd:process.cwd(),project:e(process.cwd())??null,projects:ie(),defaultImage:v,rules:{loginName:r.source,minPassword:8},paths:{root:b,sites:c,traefik:t,journal:fe,proxy:f},defaults:m(),servers:te().map(e=>({...e,target:ne(e.name),saved:n.servers[e.name]??null})),jobs:[...G.values()].reverse().map(Z)})}if(a===`servers`&&o){let{server:e}=ae(o);if(u===`GET`&&!s)return K(await le(e));if(u===`GET`&&s===`firewall`)return K(await h(e));if(u===`GET`&&s===`config`)return K(await ue(o,e));if(u===`GET`&&s===`history`)return K(await de(e));if(u===`GET`&&s===`facts`)return K(await d(o,e));if(u===`GET`&&s===`updates`)return K(await ee(e,new URL(n.url).searchParams.get(`refresh`)===`1`));if(u===`GET`&&s===`dns`){let t=(new URL(n.url).searchParams.get(`domains`)??``).split(`,`).filter(e=>l.test(e)).slice(0,50);return K(await p(e,t))}}if(a===`jobs`){if(u===`POST`&&!o){let e=await n.json(),t=e.argv;if(!Array.isArray(t)||!t.length||!t.every(e=>typeof e==`string`)||Se.has(t[0]))return q(`argv must be a list of landfall arguments`,400);if([...G.values()].filter(e=>e.job.code===void 0).length>=20)return q(`20 jobs are already running: wait for one to finish or stop one`,429);let r;try{r=_e(e.cwd)}catch(e){return q(e,400)}return K(Z(J(t,{server:e.server,password:e.password||void 0,cwd:r})))}let e=o?G.get(o):void 0;if(o&&!e)return q(`no job ${o}`,404);if(e&&u===`GET`&&s===`events`)return Ee(e,Number(new URL(n.url).searchParams.get(`from`))||0,n.signal);if(e&&u===`GET`&&!s)return K({...Z(e),lines:e.job.lines,from:e.job.dropped});if(e&&u===`POST`&&s===`force`)return e.job.code===void 0||e.argv.includes(`--force`)?q(`only a finished job without --force`,400):[...G.values()].filter(e=>e.job.code===void 0).length>=20?q(`too many jobs running`,429):K(Z(J([...e.argv,`--force`],e.opts)));if(e&&u===`POST`&&s===`stop`)return e.job.stop(),K(Z(e))}if(a===`fs`&&u===`GET`)return K(N(new URL(n.url).searchParams.get(`path`)??void 0));if(a===`upload`&&u===`POST`){let e=await R(await n.formData());return K({...e,detected:N(e.path).detected})}return q(`not found`,404)}const Q=(e,t)=>e.length===t.length&&k(Buffer.from(e),Buffer.from(t)),Oe=e=>(e.headers.get(`cookie`)??``).split(/;\s*/).find(e=>e.startsWith(`landfall=`))?.slice(9)??``,$=e=>D(`sha256`).update(`landfall-ui:${e}`).digest(`hex`);async function ke(e){u(!0);let t=Ce(),n=A(je(async e=>{let n=new URL(e.url),r=e.headers.get(`host`)??``;if(!/^(127\.0\.0\.1|localhost|\[::1\])(:\d+)?$/.test(r))return new Response(`forbidden host`,{status:403});let i=e.headers.get(`origin`);if(i&&new URL(i).host!==r)return new Response(`forbidden origin`,{status:403});if(n.pathname===`/ping`)return new Response($(t));if(n.pathname===`/login`)return Q(n.searchParams.get(`t`)??``,t)?new Response(null,{status:303,headers:{location:`/`,"set-cookie":`landfall=${t}; HttpOnly; SameSite=Strict; Path=/; Max-Age=31536000`}}):new Response(`wrong or missing token, use the link lf ui printed`,{status:403});if(!Q(Oe(e),t))return new Response("Open the link `lf ui` printed (it logs this browser in).",{status:401,headers:{"content-type":`text/plain`}});if(n.pathname===`/`)return new Response(ye,{headers:{"content-type":`text/html; charset=utf-8`,"cache-control":`no-store`}});if(n.pathname.startsWith(`/api/`)){let t=e.headers.get(`sec-fetch-site`);if(t&&t!==`same-origin`&&t!==`none`)return new Response(`forbidden`,{status:403});try{return await De(e,n.pathname.slice(5).split(`/`).filter(Boolean).map(decodeURIComponent))}catch(e){return q(e)}}return new Response(`not found`,{status:404})}));n.requestTimeout=0;let r=e=>new Promise(t=>{n.once(`error`,t),n.listen(e,`127.0.0.1`,()=>(n.off(`error`,t),t(void 0)))}),i=e=>fetch(`http://127.0.0.1:${e}/ping`).then(async e=>e.ok&&await e.text()===$(t),()=>!1),o=e.port??4777,c=e.port===void 0?[...Array.from({length:20},(e,t)=>o+t),0]:[o],l;for(let n of c){if(!(l=await r(n)))break;if(l.code!==`EADDRINUSE`)throw l;if(await i(n)){let r=`http://127.0.0.1:${n}/login?t=${t}`;return console.log(`lf ui already runs: ${a.cyan(r)}`),e.open&&s(r),0}}if(l)return console.error(`${a.red(`error:`)} port ${o} is in use by another program; pick another with --port`),1;let d=n.address().port;d!==o&&console.log(a.dim(`port ${o} is in use by another program: using ${d}`));let f=`http://127.0.0.1:${d}/login?t=${t}`;return console.log(`${a.bold(`lf ui`)} ${a.dim(g)} on ${a.cyan(f)}`),console.log(a.dim(`Only this machine can reach it, and only with that link. Ctrl-C stops it (and any running commands).`)),e.open&&s(f),new Promise(e=>{let t=()=>{for(let e of G.values())e.job.stop();n.closeAllConnections(),n.close(),e(0)};process.once(`SIGINT`,t),process.once(`SIGTERM`,t)})}function Ae(e,t){let n=0;return e.pipe(new M({transform(e,r,i){n+=e.length,i(n>t?/* @__PURE__ */ Error(`request too large`):null,e)}}))}function je(e){return async(t,n)=>{let r=new AbortController;n.on(`close`,()=>r.abort());let i;try{let a=new Headers;for(let[e,n]of Object.entries(t.headers))for(let t of[n??[]].flat())a.append(e,t);let o=t.url?.startsWith(`/api/upload`)?be:xe;if(Number(t.headers[`content-length`])>o){n.writeHead(413).end(`request too large`);return}let s=t.method===`GET`||t.method===`HEAD`?void 0:j.toWeb(Ae(t,o));i=await e(new Request(`http://${t.headers.host??`127.0.0.1`}${t.url}`,{method:t.method,headers:a,body:s,signal:r.signal,duplex:`half`}))}catch{i=new Response(`bad request`,{status:400})}for(let[e,t]of i.headers)e!==`set-cookie`&&n.setHeader(e,t);let a=i.headers.getSetCookie();if(a.length&&n.setHeader(`set-cookie`,a),n.writeHead(i.status),!i.body)return n.end();let o=j.fromWeb(i.body);n.on(`close`,()=>o.destroy()),o.pipe(n)}}export{ke as webUi};
|
package/package.json
CHANGED
package/dist/server-DA-e5nav.mjs
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import{A as e,C as t,D as n,E as r,M as i,N as a,O as o,S as s,T as c,_ as l,a as u,b as d,c as f,d as ee,f as te,g as ne,h as re,i as ie,j as ae,k as p,l as m,m as h,n as g,o as _,p as v,r as oe,s as se,t as y,u as ce,v as le,w as ue,x as de,y as fe}from"./cli.mjs";import{spawn as pe,spawnSync as b}from"node:child_process";import x from"node:fs";import{homedir as S}from"node:os";import{dirname as C,isAbsolute as me,join as w,resolve as he}from"node:path";import{createHash as T,randomBytes as E,timingSafeEqual as D}from"node:crypto";import{createServer as O}from"node:http";import{Readable as k,Transform as A}from"node:stream";function j(e){let t=he(!e||e===`~`?S():n(e)),r=x.readdirSync(t,{withFileTypes:!0}).filter(e=>!e.name.startsWith(`.`)&&(e.isDirectory()||e.isSymbolicLink()&&M(w(t,e.name)))).map(e=>e.name).sort((e,t)=>e.localeCompare(t));return{path:t,parent:C(t)===t?null:C(t),home:S(),dirs:r,detected:v(t)}}const M=e=>{try{return x.statSync(e).isDirectory()}catch{return!1}},N=w(r,`uploads`),P=e=>e.split(/[\\/]+/).filter(e=>e&&e!==`.`&&e!==`..`);async function F(e,t){x.mkdirSync(C(e),{recursive:!0}),x.writeFileSync(e,Buffer.from(await t.arrayBuffer()))}async function I(e){x.mkdirSync(N,{recursive:!0,mode:448});for(let e of x.readdirSync(N)){let t=w(N,e);Date.now()-x.statSync(t).mtimeMs>864e5&&x.rmSync(t,{recursive:!0,force:!0})}let t=x.mkdtempSync(w(N,`u-`)),n=e.getAll(`file`).filter(e=>e instanceof File);if(!n.length)throw Error(`no files in the upload`);let r=e.getAll(`path`).map(String);if(n.length===1&&/\.(zip|tar|tgz|tar\.gz)$/i.test(n[0].name)){let e=w(t,`.upload-`+P(n[0].name).pop());await F(e,n[0]);let r=w(t,`files`);x.mkdirSync(r);let i=/\.zip$/i.test(e);if(L(e,i))throw Error(`${n[0].name} contains symlinks or hard links: upload the folder instead`);let a=i?b(`unzip`,[`-q`,e,`-d`,r]):b(`tar`,[`-xf`,e,`-C`,r]);if(a.error||a.status!==0){let t=i?b(`tar`,[`-xf`,e,`-C`,r]):a;if(t.error||t.status!==0)throw Error(`could not unpack ${n[0].name}`)}return x.rmSync(e),R(r),{path:z(r),files:ge(r)}}let i=w(t,`files`);for(let[e,t]of n.entries()){let n=P(r[e]||t.name);n.length&&await F(w(i,...n),t)}return{path:z(i),files:n.length}}function L(e,t){let n=(e,t)=>{let n=b(e,t,{encoding:`utf8`,maxBuffer:256*1024**2});return n.error||n.status!==0?void 0:n.stdout},r=t?n(`zipinfo`,[e])??n(`tar`,[`-tvf`,e]):n(`tar`,[`-tvf`,e]);if(r===void 0)throw Error(`could not read ${e.split(`/`).pop()}`);return r.split(`
|
|
2
|
-
`).some(e=>/^[lh]/.test(e)||/ link to /.test(e))}function R(e){for(let t of x.readdirSync(e,{withFileTypes:!0,recursive:!0}))t.isSymbolicLink()&&x.rmSync(w(t.parentPath,t.name),{force:!0})}function z(e){for(;;){let t=x.readdirSync(e).filter(e=>e!==`__MACOSX`&&e!==`.DS_Store`);if(t.length!==1||!M(w(e,t[0])))return e;e=w(e,t[0])}}function ge(e){return x.readdirSync(e,{withFileTypes:!0,recursive:!0}).filter(e=>e.isFile()).length}function _e(e){if(e!=null&&e!==``){if(typeof e!=`string`||!me(e)||!M(e))throw Error(`cwd must be an existing folder`);return e}}const B=()=>[process.execPath,...process.execArgv,process.argv[1]],V=5e3,H=1e4,U=e=>e.length>H?e.slice(0,H)+`…`:e;function ve(e,t,n={}){let r={...process.env,NO_COLOR:`1`,LANDFALL_REPORT:`1`,...n.env};n.password&&Object.assign(r,f(n.password));let i=pe(B()[0],[...B().slice(1),...e],{stdio:[n.stdin===void 0?`ignore`:`pipe`,`pipe`,`pipe`],env:r,cwd:n.cwd,detached:!0});i.stdin?.on(`error`,()=>{}).end(n.stdin);let a={argv:e,lines:[],dropped:0,report:{},stop(){if(a.code===void 0){a.stopped=!0;try{process.kill(-i.pid,`SIGTERM`)}catch{}}}},o,s=()=>o??=setTimeout(()=>(o=void 0,t()),50),c=()=>{let e=``;return{push(t){let n=(e+t).split(`
|
|
3
|
-
`);e=n.pop(),e.length>H&&(n.push(e),e=``);for(let e of n)if(e.startsWith(``))try{Object.assign(a.report,JSON.parse(e.slice(1)))}catch{}else a.lines.push(U(e.slice(e.lastIndexOf(`\r`,e.length-2)+1).replace(/\r$/,``)));a.lines.length>V&&(a.dropped+=a.lines.splice(0,a.lines.length-V).length),s()},flush:()=>e&&a.lines.push(U(e))}},l=c(),u=c();return i.stdout.setEncoding(`utf8`).on(`data`,l.push),i.stderr.setEncoding(`utf8`).on(`data`,u.push),i.on(`error`,e=>{a.lines.push(`error: ${e.message}`),a.code=1,t()}),i.on(`close`,e=>{l.flush(),u.flush(),a.code=a.stopped?null:e??1,clearTimeout(o),t()}),a}const ye=x.readFileSync(new URL(`./index.html`,import.meta.url),`utf8`),be=512*1024**2,xe=1024**2,W=/* @__PURE__ */ new Map,Se=/* @__PURE__ */ new Set([`ui`,`web`]);function Ce(){let e=w(C(t),`ui-token`);try{let t=x.readFileSync(e,`utf8`).trim();if(t.length>=32)return t}catch{}let n=E(24).toString(`hex`);return x.mkdirSync(C(e),{recursive:!0,mode:448}),x.writeFileSync(e,n+`
|
|
4
|
-
`,{mode:384}),n}const G=(e,t=200)=>Response.json(e,{status:t,headers:{"cache-control":`no-store`}}),K=(e,t=500)=>G({error:e?.message??String(e)},t);function q(e,t){let n=re(),r=/* @__PURE__ */ new Set,{args:i,env:a,stdin:o}=we(e),s=ve(i,()=>r.forEach(e=>e()),{password:t.password,cwd:t.cwd,stdin:o,env:{...a,LANDFALL_RUN:n}}),c={id:n,argv:e,server:t.server,startedAt:(/* @__PURE__ */ new Date()).toISOString(),undoable:m(e),job:s,listeners:r,opts:t};W.set(n,c);for(let[e,t]of W){if(W.size<=50)break;t.job.code!==void 0&&W.delete(e)}return c}const J=e=>e[0]===`env`||e[0]===`sites`&&e[1]===`env`,Y=/^[A-Za-z_][A-Za-z0-9_]*=/;function we(e){let t=[],n={},r=[];for(let i=0;i<e.length;i++){let a=e[i],o=a.match(/^--((?:[\w-]+-)?password)(?:=(.*))?$/s);o&&(o[2]!==void 0||i+1<e.length)?n[ae(o[1])]=o[2]??e[++i]:J(e)&&Y.test(a)&&!a.includes(`
|
|
5
|
-
`)&&!e.includes(`--env-file`)?r.push(a):t.push(a)}return r.length&&t.push(`--env-file`,`-`),{args:t,env:n,stdin:r.length?r.join(`
|
|
6
|
-
`)+`
|
|
7
|
-
`:void 0}}const Te=e=>e.map((t,n)=>/^(-p|--([\w-]+-)?password)$/.test(e[n-1])?`***`:J(e)&&Y.test(t)?t.replace(/=.*/s,`=***`):t.replace(/^(--([\w-]+-)?password=).*/,`$1***`)),X=e=>({id:e.id,argv:Te(e.argv),server:e.server,startedAt:e.startedAt,undoable:e.undoable,code:e.job.code,stopped:!!e.job.stopped,report:e.job.report});function Z(e,t,n){let r=new TextEncoder,i=t,a=()=>{},o=new ReadableStream({start(t){let o=e=>t.enqueue(r.encode(e)),s=()=>{let{lines:t,dropped:n,code:r}=e.job,s=Math.max(i,n);n+t.length>s&&(o(`data: ${JSON.stringify({from:s,lines:t.slice(s-n),report:e.job.report})}\n\n`),i=n+t.length),r!==void 0&&(o(`event: done\ndata: ${JSON.stringify(X(e))}\n\n`),a())},c=setInterval(()=>o(`: beat
|
|
8
|
-
|
|
9
|
-
`),15e3);a=()=>{clearInterval(c),e.listeners.delete(s);try{t.close()}catch{}},e.listeners.add(s),n.addEventListener(`abort`,a),s()},cancel:()=>a()});return new Response(o,{headers:{"content-type":`text/event-stream`,"cache-control":`no-store`}})}async function Ee(t,n){let[r,i,a]=n,f=t.method;if(r===`state`&&f===`GET`){let e=o();return G({version:y,cwd:process.cwd(),project:p(process.cwd())??null,projects:h(),defaultImage:l,rules:{loginName:c.source,minPassword:8},paths:{root:d,sites:de,traefik:s,journal:le,proxy:fe},servers:ce().map(t=>({...t,target:ee(t.name),saved:e.servers[t.name]??null})),jobs:[...W.values()].reverse().map(X)})}if(r===`servers`&&i){let{server:n}=e(i);if(f===`GET`&&!a)return G(await te(n));if(f===`GET`&&a===`firewall`)return G(await u(n));if(f===`GET`&&a===`config`)return G(await g(i,n));if(f===`GET`&&a===`history`)return G(await _(n));if(f===`GET`&&a===`facts`)return G(await ie(i,n));if(f===`GET`&&a===`updates`)return G(await se(n,new URL(t.url).searchParams.get(`refresh`)===`1`));if(f===`GET`&&a===`dns`){let e=(new URL(t.url).searchParams.get(`domains`)??``).split(`,`).filter(e=>ue.test(e)).slice(0,50);return G(await oe(n,e))}}if(r===`jobs`){if(f===`POST`&&!i){let e=await t.json(),n=e.argv;if(!Array.isArray(n)||!n.length||!n.every(e=>typeof e==`string`)||Se.has(n[0]))return K(`argv must be a list of landfall arguments`,400);if([...W.values()].filter(e=>e.job.code===void 0).length>=20)return K(`20 jobs are already running: wait for one to finish or stop one`,429);let r;try{r=_e(e.cwd)}catch(e){return K(e,400)}return G(X(q(n,{server:e.server,password:e.password||void 0,cwd:r})))}let e=i?W.get(i):void 0;if(i&&!e)return K(`no job ${i}`,404);if(e&&f===`GET`&&a===`events`)return Z(e,Number(new URL(t.url).searchParams.get(`from`))||0,t.signal);if(e&&f===`GET`&&!a)return G({...X(e),lines:e.job.lines,from:e.job.dropped});if(e&&f===`POST`&&a===`force`)return e.job.code===void 0||e.argv.includes(`--force`)?K(`only a finished job without --force`,400):[...W.values()].filter(e=>e.job.code===void 0).length>=20?K(`too many jobs running`,429):G(X(q([...e.argv,`--force`],e.opts)));if(e&&f===`POST`&&a===`stop`)return e.job.stop(),G(X(e))}if(r===`fs`&&f===`GET`)return G(j(new URL(t.url).searchParams.get(`path`)??void 0));if(r===`upload`&&f===`POST`){let e=await I(await t.formData());return G({...e,detected:j(e.path).detected})}return K(`not found`,404)}const Q=(e,t)=>e.length===t.length&&D(Buffer.from(e),Buffer.from(t)),De=e=>(e.headers.get(`cookie`)??``).split(/;\s*/).find(e=>e.startsWith(`landfall=`))?.slice(9)??``,$=e=>T(`sha256`).update(`landfall-ui:${e}`).digest(`hex`);async function Oe(e){ne(!0);let t=Ce(),n=O(Ae(async e=>{let n=new URL(e.url),r=e.headers.get(`host`)??``;if(!/^(127\.0\.0\.1|localhost|\[::1\])(:\d+)?$/.test(r))return new Response(`forbidden host`,{status:403});let i=e.headers.get(`origin`);if(i&&new URL(i).host!==r)return new Response(`forbidden origin`,{status:403});if(n.pathname===`/ping`)return new Response($(t));if(n.pathname===`/login`)return Q(n.searchParams.get(`t`)??``,t)?new Response(null,{status:303,headers:{location:`/`,"set-cookie":`landfall=${t}; HttpOnly; SameSite=Strict; Path=/; Max-Age=31536000`}}):new Response(`wrong or missing token, use the link lf ui printed`,{status:403});if(!Q(De(e),t))return new Response("Open the link `lf ui` printed (it logs this browser in).",{status:401,headers:{"content-type":`text/plain`}});if(n.pathname===`/`)return new Response(ye,{headers:{"content-type":`text/html; charset=utf-8`,"cache-control":`no-store`}});if(n.pathname.startsWith(`/api/`)){let t=e.headers.get(`sec-fetch-site`);if(t&&t!==`same-origin`&&t!==`none`)return new Response(`forbidden`,{status:403});try{return await Ee(e,n.pathname.slice(5).split(`/`).filter(Boolean).map(decodeURIComponent))}catch(e){return K(e)}}return new Response(`not found`,{status:404})}));n.requestTimeout=0;let r=e=>new Promise(t=>{n.once(`error`,t),n.listen(e,`127.0.0.1`,()=>(n.off(`error`,t),t(void 0)))}),o=e=>fetch(`http://127.0.0.1:${e}/ping`).then(async e=>e.ok&&await e.text()===$(t),()=>!1),s=e.port??4777,c=e.port===void 0?[...Array.from({length:20},(e,t)=>s+t),0]:[s],l;for(let n of c){if(!(l=await r(n)))break;if(l.code!==`EADDRINUSE`)throw l;if(await o(n)){let r=`http://127.0.0.1:${n}/login?t=${t}`;return console.log(`lf ui already runs: ${i.cyan(r)}`),e.open&&a(r),0}}if(l)return console.error(`${i.red(`error:`)} port ${s} is in use by another program; pick another with --port`),1;let u=n.address().port;u!==s&&console.log(i.dim(`port ${s} is in use by another program: using ${u}`));let d=`http://127.0.0.1:${u}/login?t=${t}`;return console.log(`${i.bold(`lf ui`)} ${i.dim(y)} on ${i.cyan(d)}`),console.log(i.dim(`Only this machine can reach it, and only with that link. Ctrl-C stops it (and any running commands).`)),e.open&&a(d),new Promise(e=>{let t=()=>{for(let e of W.values())e.job.stop();n.closeAllConnections(),n.close(),e(0)};process.once(`SIGINT`,t),process.once(`SIGTERM`,t)})}function ke(e,t){let n=0;return e.pipe(new A({transform(e,r,i){n+=e.length,i(n>t?/* @__PURE__ */ Error(`request too large`):null,e)}}))}function Ae(e){return async(t,n)=>{let r=new AbortController;n.on(`close`,()=>r.abort());let i;try{let a=new Headers;for(let[e,n]of Object.entries(t.headers))for(let t of[n??[]].flat())a.append(e,t);let o=t.url?.startsWith(`/api/upload`)?be:xe;if(Number(t.headers[`content-length`])>o){n.writeHead(413).end(`request too large`);return}let s=t.method===`GET`||t.method===`HEAD`?void 0:k.toWeb(ke(t,o));i=await e(new Request(`http://${t.headers.host??`127.0.0.1`}${t.url}`,{method:t.method,headers:a,body:s,signal:r.signal,duplex:`half`}))}catch{i=new Response(`bad request`,{status:400})}for(let[e,t]of i.headers)e!==`set-cookie`&&n.setHeader(e,t);let a=i.headers.getSetCookie();if(a.length&&n.setHeader(`set-cookie`,a),n.writeHead(i.status),!i.body)return n.end();let o=k.fromWeb(i.body);n.on(`close`,()=>o.destroy()),o.pipe(n)}}export{Oe as webUi};
|