@kvbox/server 0.5.15 → 0.5.17

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.
@@ -2685,7 +2685,7 @@ function versionOf(srcFileUrl, injected) {
2685
2685
  }
2686
2686
 
2687
2687
  // src/version.ts
2688
- var VERSION = versionOf(import.meta.url, true ? "0.5.15" : void 0);
2688
+ var VERSION = versionOf(import.meta.url, true ? "0.5.17" : void 0);
2689
2689
 
2690
2690
  // src/caller.ts
2691
2691
  import { createHash as createHash2, timingSafeEqual } from "node:crypto";
@@ -2695,6 +2695,9 @@ import { createHmac } from "node:crypto";
2695
2695
  var PROXY_ERROR_HEADER = "x-kvbox-proxy-error";
2696
2696
  var PROXIED_HEADER = "x-kvbox-proxied";
2697
2697
  var VISITOR_PATH_HEADER = "x-kvbox-path";
2698
+ var WAKE_HEADER = "x-kvbox-wake";
2699
+ var WAKE_WAIT_MS = 3e4;
2700
+ var WOKE_HEADER = "x-kvbox-woke";
2698
2701
  var VISITOR_AUTH_HEADER = "x-kvbox-authorization";
2699
2702
  var NOT_FORWARDED = /* @__PURE__ */ new Set(["expect"]);
2700
2703
  var HOP_BY_HOP = /* @__PURE__ */ new Set(["connection", "keep-alive", "proxy-authenticate", "proxy-authorization", "proxy-connection", "te", "trailer", "transfer-encoding", "upgrade"]);
@@ -2799,7 +2802,7 @@ function rewriteHead(head, drop, add = {}) {
2799
2802
  var HEADERS_TIMEOUT_MS = 55e3;
2800
2803
  var MAX_HANDSHAKE_BYTES = 16 * 1024;
2801
2804
  var CONNECT_TIMEOUT_MS = 5e3;
2802
- var DROPPED = /* @__PURE__ */ new Set([...HOP_BY_HOP, ...NOT_FORWARDED, "host", "authorization", OWNER_HEADER, VISITOR_AUTH_HEADER, VISITOR_PATH_HEADER]);
2805
+ var DROPPED = /* @__PURE__ */ new Set([...HOP_BY_HOP, ...NOT_FORWARDED, "host", "authorization", OWNER_HEADER, VISITOR_AUTH_HEADER, VISITOR_PATH_HEADER, WAKE_HEADER]);
2803
2806
  var ProxyFailure = class extends Error {
2804
2807
  constructor(status, code, message) {
2805
2808
  super(message);
@@ -2809,24 +2812,64 @@ var ProxyFailure = class extends Error {
2809
2812
  status;
2810
2813
  code;
2811
2814
  };
2815
+ function sharedWaker(bring = defaultBring, capMs = WAKE_WAIT_MS) {
2816
+ const inFlight = /* @__PURE__ */ new Map();
2817
+ return async (ref, status) => {
2818
+ const key = `${ref.owner}/${ref.name}`;
2819
+ let wake = inFlight.get(key);
2820
+ if (!wake) {
2821
+ wake = bring(ref, status).finally(() => inFlight.delete(key));
2822
+ inFlight.set(key, wake);
2823
+ wake.catch(() => {
2824
+ });
2825
+ }
2826
+ let timer;
2827
+ const cap = new Promise((_, reject) => {
2828
+ timer = setTimeout(() => reject(new ProxyFailure(503, "waking", `box ${ref.name} is waking up`)), capMs);
2829
+ });
2830
+ try {
2831
+ await Promise.race([wake, cap]);
2832
+ } finally {
2833
+ clearTimeout(timer);
2834
+ }
2835
+ };
2836
+ }
2837
+ var defaultBring = async (ref, status) => {
2838
+ await (status === "hibernated" ? wakeMachine(ref) : resumeMachine(ref));
2839
+ };
2840
+ var wakeBox = sharedWaker();
2841
+ var notRunning = (status) => status === "hibernated" || status === "standby" ? status : "stopped";
2812
2842
  function visitorPath(value) {
2813
2843
  const path = Array.isArray(value) ? value[0] : value;
2814
2844
  return path !== void 0 && path !== null && path.startsWith("/") && !/[\s]/.test(path) ? path : "/";
2815
2845
  }
2816
- async function targetOf(url, owner, path) {
2846
+ async function targetOf(url, owner, path, wake) {
2817
2847
  const m = BOX_HTTP_PATH_RE.exec(url.pathname);
2818
2848
  const port = Number(m?.[2]);
2819
2849
  if (!m || port < 1 || port > 65535) throw new ProxyFailure(404, "not-found", `no route: ${url.pathname}`);
2820
2850
  const name = decodeURIComponent(m[1]);
2851
+ const ref = parseRef(name, owner);
2821
2852
  let box;
2822
2853
  try {
2823
- box = await getMachine(parseRef(name, owner));
2854
+ box = await getMachine(ref);
2824
2855
  } catch (err) {
2825
2856
  if (err instanceof NotFoundError) throw new ProxyFailure(404, "not-found", err.message);
2826
2857
  throw err;
2827
2858
  }
2828
- if (box.status !== "running") throw new ProxyFailure(409, "not-running", `box ${name} is ${box.status}`);
2829
- return { ip: box.ip, port, path };
2859
+ let woke = false;
2860
+ if (box.status !== "running" && wake && (box.status === "hibernated" || box.status === "standby")) {
2861
+ try {
2862
+ await wake(ref, box.status);
2863
+ woke = true;
2864
+ } catch (err) {
2865
+ if (err instanceof ProxyFailure) throw err;
2866
+ box = await getMachine(ref);
2867
+ if (box.status !== "running") throw new ProxyFailure(502, "wake-failed", `box ${name} did not wake: ${err.message}`);
2868
+ }
2869
+ if (woke) box = await getMachine(ref);
2870
+ }
2871
+ if (box.status !== "running") throw new ProxyFailure(409, notRunning(box.status), `box ${name} is ${box.status}`);
2872
+ return { ip: box.ip, port, path, woke };
2830
2873
  }
2831
2874
  function boxHeaders(incoming, target, upgrade = false) {
2832
2875
  const out = [];
@@ -2841,16 +2884,17 @@ function boxHeaders(incoming, target, upgrade = false) {
2841
2884
  out.push(["host", host]);
2842
2885
  return out;
2843
2886
  }
2844
- var failure = (err) => new Response(JSON.stringify({ error: err.message }), { status: err.status, headers: { "content-type": "application/json", [PROXY_ERROR_HEADER]: err.code } });
2845
- function boxHttpRoutes() {
2887
+ var failure = (err, woke = false) => new Response(JSON.stringify({ error: err.message }), { status: err.status, headers: { "content-type": "application/json", [PROXY_ERROR_HEADER]: err.code, ...woke ? { [WOKE_HEADER]: "1" } : {} } });
2888
+ var wants = (value) => value === "1";
2889
+ function boxHttpRoutes(wake = wakeBox) {
2846
2890
  const r = new Hono();
2847
- r.all("/:name/http/:port", forward);
2891
+ r.all("/:name/http/:port", (c) => forward(c, wake));
2848
2892
  return r;
2849
2893
  }
2850
- async function forward(c) {
2894
+ async function forward(c, wake) {
2851
2895
  let target;
2852
2896
  try {
2853
- target = await targetOf(new URL(c.req.url), c.get("owner"), visitorPath(c.req.header(VISITOR_PATH_HEADER)));
2897
+ target = await targetOf(new URL(c.req.url), c.get("owner"), visitorPath(c.req.header(VISITOR_PATH_HEADER)), wants(c.req.header(WAKE_HEADER)) ? wake : void 0);
2854
2898
  } catch (err) {
2855
2899
  if (err instanceof ProxyFailure) return failure(err);
2856
2900
  throw err;
@@ -2870,8 +2914,8 @@ async function forward(c) {
2870
2914
  });
2871
2915
  } catch (err) {
2872
2916
  const e = err;
2873
- if (e.code === "UND_ERR_HEADERS_TIMEOUT") return failure(new ProxyFailure(504, "timeout", `box took over ${HEADERS_TIMEOUT_MS / 1e3} s to answer on port ${target.port}`));
2874
- return failure(new ProxyFailure(502, "unreachable", `box answered nothing on port ${target.port} (${e.code ?? e.message})`));
2917
+ if (e.code === "UND_ERR_HEADERS_TIMEOUT") return failure(new ProxyFailure(504, "timeout", `box took over ${HEADERS_TIMEOUT_MS / 1e3} s to answer on port ${target.port}`), target.woke);
2918
+ return failure(new ProxyFailure(502, "unreachable", `box answered nothing on port ${target.port} (${e.code ?? e.message})`), target.woke);
2875
2919
  }
2876
2920
  const headers = new Headers();
2877
2921
  for (const [name, value] of Object.entries(res.headers)) {
@@ -2879,17 +2923,19 @@ async function forward(c) {
2879
2923
  for (const v of Array.isArray(value) ? value : [value]) headers.append(name, v);
2880
2924
  }
2881
2925
  headers.set(PROXIED_HEADER, "1");
2926
+ if (target.woke) headers.set(WOKE_HEADER, "1");
2882
2927
  const empty = res.statusCode === 204 || res.statusCode === 304 || raw.method === "HEAD";
2883
2928
  if (empty) res.body.resume();
2884
2929
  return new Response(empty ? null : Readable.toWeb(res.body), { status: res.statusCode, headers });
2885
2930
  }
2886
- function refuse(socket, status, text, code = "refused") {
2931
+ function refuse(socket, status, text, code = "refused", woke = false) {
2887
2932
  const body = JSON.stringify({ error: text });
2888
2933
  socket.end(`HTTP/1.1 ${status} ${status === 401 ? "Unauthorized" : "Error"}\r
2889
2934
  content-type: application/json\r
2890
2935
  content-length: ${Buffer.byteLength(body)}\r
2891
2936
  ${PROXY_ERROR_HEADER}: ${code}\r
2892
- connection: close\r
2937
+ ${woke ? `${WOKE_HEADER}: 1\r
2938
+ ` : ""}connection: close\r
2893
2939
  \r
2894
2940
  ${body}`);
2895
2941
  }
@@ -2898,7 +2944,7 @@ function headerPairs(raw) {
2898
2944
  for (let i = 0; i + 1 < raw.length; i += 2) out.push([raw[i], raw[i + 1]]);
2899
2945
  return out;
2900
2946
  }
2901
- function boxUpgradeHandler(cp, headersTimeoutMs = HEADERS_TIMEOUT_MS) {
2947
+ function boxUpgradeHandler(cp, headersTimeoutMs = HEADERS_TIMEOUT_MS, wake = wakeBox) {
2902
2948
  const full = secretCheck(cp);
2903
2949
  const edge = edgeCheck(cp);
2904
2950
  const authorized = (auth) => full(auth) || edge(auth);
@@ -2911,7 +2957,7 @@ function boxUpgradeHandler(cp, headersTimeoutMs = HEADERS_TIMEOUT_MS) {
2911
2957
  if (typeof user !== "string" || !LOCAL_OWNER_RE.test(user)) return refuse(socket, 400, "X-Kvbox-Owner header required");
2912
2958
  let target;
2913
2959
  try {
2914
- target = await targetOf(new URL(req.url ?? "/", "https://host"), `${user}@${cp.name}`, visitorPath(headers[VISITOR_PATH_HEADER]));
2960
+ target = await targetOf(new URL(req.url ?? "/", "https://host"), `${user}@${cp.name}`, visitorPath(headers[VISITOR_PATH_HEADER]), wants(headers[WAKE_HEADER]) ? wake : void 0);
2915
2961
  } catch (err) {
2916
2962
  if (err instanceof ProxyFailure) return refuse(socket, err.status, err.message, err.code);
2917
2963
  return refuse(socket, 500, err.message);
@@ -2922,7 +2968,7 @@ function boxUpgradeHandler(cp, headersTimeoutMs = HEADERS_TIMEOUT_MS) {
2922
2968
  let piped = false;
2923
2969
  box.once("timeout", () => box.destroy(new Error("timed out")));
2924
2970
  box.once("error", (err) => {
2925
- if (!connected && !socket.destroyed) refuse(socket, 502, `box answered nothing on port ${target.port} (${err.code ?? err.message})`, "unreachable");
2971
+ if (!connected && !socket.destroyed) refuse(socket, 502, `box answered nothing on port ${target.port} (${err.code ?? err.message})`, "unreachable", target.woke);
2926
2972
  else if (piped) socket.destroy();
2927
2973
  });
2928
2974
  socket.once("close", () => box.destroy());
@@ -2941,7 +2987,7 @@ ${lines.join("")}\r
2941
2987
  readHead(box, { maxBytes: MAX_HANDSHAKE_BYTES, timeoutMs: headersTimeoutMs }).then(
2942
2988
  (answer) => {
2943
2989
  if (socket.destroyed) return void box.destroy();
2944
- socket.write(rewriteHead(answer.head, (name) => name.startsWith("x-kvbox-"), { [PROXIED_HEADER]: "1" }));
2990
+ socket.write(rewriteHead(answer.head, (name) => name.startsWith("x-kvbox-"), { [PROXIED_HEADER]: "1", ...target.woke ? { [WOKE_HEADER]: "1" } : {} }));
2945
2991
  if (answer.rest.length > 0) socket.write(answer.rest);
2946
2992
  piped = true;
2947
2993
  box.pipe(socket);
@@ -2950,8 +2996,8 @@ ${lines.join("")}\r
2950
2996
  (err) => {
2951
2997
  box.destroy();
2952
2998
  if (socket.destroyed) return;
2953
- if (err instanceof HeadTimeoutError) refuse(socket, 504, `box took over ${headersTimeoutMs / 1e3} s to answer on port ${target.port}`, "timeout");
2954
- else refuse(socket, 502, `box gave no answer on port ${target.port} (${err.message})`, "unreachable");
2999
+ if (err instanceof HeadTimeoutError) refuse(socket, 504, `box took over ${headersTimeoutMs / 1e3} s to answer on port ${target.port}`, "timeout", target.woke);
3000
+ else refuse(socket, 502, `box gave no answer on port ${target.port} (${err.message})`, "unreachable", target.woke);
2955
3001
  }
2956
3002
  );
2957
3003
  });