@dutodev/cli 1.8.1 → 1.9.0

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.
@@ -644,6 +644,8 @@ function reasonOf(c) {
644
644
  if (c.source === "headers") return c.kind === "remove" ? 'headers: ""' : "headers:";
645
645
  if (c.source === "host") return "host:";
646
646
  if (c.source === "visitor") return "from the visitor; set trust_forwarded: true to keep it";
647
+ if (c.source === "isolation" && c.name === "set-cookie") return "Domain=duto.page reaches every user's tunnel; kept on this address";
648
+ if (c.source === "isolation") return "from another user's tunnel; only SameSite=None cookies go through";
647
649
  return "Duto credential";
648
650
  }
649
651
  function valueOf(c) {
@@ -16,7 +16,7 @@ import {
16
16
  tunnelColor,
17
17
  updateLine,
18
18
  validateName
19
- } from "./chunk-QFM4MUED.js";
19
+ } from "./chunk-QMQWGCIE.js";
20
20
 
21
21
  // src/ui/fullscreen.tsx
22
22
  import { render } from "ink";
package/dist/index.js CHANGED
@@ -28,7 +28,7 @@ import {
28
28
  updateLine,
29
29
  validateName,
30
30
  warn
31
- } from "./chunk-QFM4MUED.js";
31
+ } from "./chunk-QMQWGCIE.js";
32
32
 
33
33
  // src/index.ts
34
34
  import { hostname as hostname2 } from "os";
@@ -36,7 +36,7 @@ import { join as join7, relative } from "path";
36
36
  import cac from "cac";
37
37
 
38
38
  // src/version.ts
39
- var CLI_VERSION = "1.8.1";
39
+ var CLI_VERSION = "1.9.0";
40
40
 
41
41
  // src/api.ts
42
42
  var DutoApiError = class extends Error {
@@ -561,7 +561,7 @@ async function browserLogin(o) {
561
561
  const start = await api.startLogin({
562
562
  deviceId: o.deviceId,
563
563
  deviceName: hostname(),
564
- cliVersion: "1.8.1",
564
+ cliVersion: "1.9.0",
565
565
  nodeVersion: process.version,
566
566
  os: process.platform,
567
567
  arch: process.arch
@@ -2436,6 +2436,89 @@ function deliver(h, route) {
2436
2436
  return changes.length ? { headers: out, changes } : { headers: h, changes };
2437
2437
  }
2438
2438
 
2439
+ // src/session/siteIsolation.ts
2440
+ function firstLabel(host) {
2441
+ return host.split(".")[0] ?? "";
2442
+ }
2443
+ function parentDomain(host) {
2444
+ return host.slice(host.indexOf(".") + 1).toLowerCase();
2445
+ }
2446
+ function tunnelOwner(host) {
2447
+ const label = firstLabel(host.toLowerCase());
2448
+ return label.slice(label.lastIndexOf("-") + 1);
2449
+ }
2450
+ function originHost(value) {
2451
+ if (!value) return null;
2452
+ try {
2453
+ const url = new URL(value);
2454
+ return { origin: url.origin, host: url.hostname.toLowerCase() };
2455
+ } catch {
2456
+ return null;
2457
+ }
2458
+ }
2459
+ function isSiblingTunnel(host, publicHost) {
2460
+ const parent = parentDomain(publicHost);
2461
+ if (!host.endsWith(`.${parent}`)) return false;
2462
+ return !host.slice(0, -parent.length - 1).includes(".");
2463
+ }
2464
+ function crossUserOrigin(h, publicHost) {
2465
+ const site = h["sec-fetch-site"];
2466
+ if (site !== void 0 && site !== "same-site") return null;
2467
+ const from = originHost(h.origin) ?? originHost(h.referer);
2468
+ if (!from || !isSiblingTunnel(from.host, publicHost)) return null;
2469
+ return tunnelOwner(from.host) === tunnelOwner(publicHost) ? null : from.origin;
2470
+ }
2471
+ function scopeSetCookies(lines, publicHost) {
2472
+ const parent = parentDomain(publicHost);
2473
+ const changes = [];
2474
+ const scoped = lines.map((line) => {
2475
+ const parts = line.split(";");
2476
+ const kept = parts.filter((part, i) => {
2477
+ if (i === 0) return true;
2478
+ const [key = "", value = ""] = part.split("=");
2479
+ const shared = key.trim().toLowerCase() === "domain" && value.trim().replace(/^\./, "").toLowerCase() === parent;
2480
+ if (shared) changes.push({ kind: "remove", name: "set-cookie", from: `${parseSetCookie(line).name}: ${part.trim()}`, source: "isolation" });
2481
+ return !shared;
2482
+ });
2483
+ return kept.length === parts.length ? line : kept.join(";");
2484
+ });
2485
+ return { lines: scoped, changes };
2486
+ }
2487
+ var SameSiteMemory = class {
2488
+ byName = /* @__PURE__ */ new Map();
2489
+ learn(setCookies) {
2490
+ for (const line of setCookies) {
2491
+ const c = parseSetCookie(line);
2492
+ const value = typeof c.attrs.samesite === "string" ? c.attrs.samesite.toLowerCase() : "lax";
2493
+ this.byName.set(c.name, value === "none" || value === "strict" ? value : "lax");
2494
+ }
2495
+ }
2496
+ sentCrossSite(name, navigation) {
2497
+ const sameSite = this.byName.get(name) ?? "lax";
2498
+ return sameSite === "none" || navigation && sameSite === "lax";
2499
+ }
2500
+ };
2501
+ function isNavigation(h, method) {
2502
+ if (method !== "GET" && method !== "HEAD") return false;
2503
+ const mode = h["sec-fetch-mode"];
2504
+ return mode === "navigate" || mode === void 0 && h.origin === void 0;
2505
+ }
2506
+ function isolateCookies(h, publicHost, memory, method = "GET") {
2507
+ const cookie = h.cookie;
2508
+ const origin = cookie ? crossUserOrigin(h, publicHost) : null;
2509
+ if (!cookie || !origin) return { headers: h, changes: [] };
2510
+ const navigation = isNavigation(h, method);
2511
+ const pairs = cookie.split(";").map((p) => p.trim()).filter(Boolean);
2512
+ const nameOf = (pair2) => pair2.split("=")[0].trim();
2513
+ const kept = pairs.filter((p) => memory.sentCrossSite(nameOf(p), navigation));
2514
+ const removed = pairs.filter((p) => !kept.includes(p)).map(nameOf);
2515
+ if (removed.length === 0) return { headers: h, changes: [] };
2516
+ const headers = { ...h };
2517
+ if (kept.length) headers.cookie = kept.join("; ");
2518
+ else delete headers.cookie;
2519
+ return { headers, changes: [{ kind: "remove", name: "cookie", from: `${removed.join(", ")} (from ${origin})`, source: "isolation" }] };
2520
+ }
2521
+
2439
2522
  // src/session/errorPage.ts
2440
2523
  var esc = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
2441
2524
  function wantsHtml(accept) {
@@ -2731,6 +2814,26 @@ function waitForService(res, port, checkMs, pingMs) {
2731
2814
  async function startProxy(o) {
2732
2815
  const max = o.maxBody ?? MAX_BODY;
2733
2816
  let nextId = 1;
2817
+ const sameSite = /* @__PURE__ */ new Map();
2818
+ function memoryOf(route) {
2819
+ let memory = sameSite.get(route.tunnel);
2820
+ if (!memory) sameSite.set(route.tunnel, memory = new SameSiteMemory());
2821
+ return memory;
2822
+ }
2823
+ function deliverFor(req, route) {
2824
+ const delivered = deliver(req.headers, route);
2825
+ const isolated = isolateCookies(delivered.headers, hostOf(req), memoryOf(route), req.method ?? "GET");
2826
+ return isolated.changes.length ? { headers: isolated.headers, changes: [...delivered.changes, ...isolated.changes] } : delivered;
2827
+ }
2828
+ function scopeResponseCookies(id, req, route, up) {
2829
+ const lines = up.headers["set-cookie"];
2830
+ if (!lines?.length) return;
2831
+ memoryOf(route).learn(lines);
2832
+ const scoped = scopeSetCookies(lines, hostOf(req));
2833
+ if (!scoped.changes.length) return;
2834
+ up.headers["set-cookie"] = scoped.lines;
2835
+ o.store.update(id, { headerChanges: [...o.store.get(id)?.headerChanges ?? [], ...scoped.changes] });
2836
+ }
2734
2837
  const routeWaitMs = o.routeWaitMs ?? DEFAULT_ROUTE_WAIT_MS;
2735
2838
  async function routeFor(req) {
2736
2839
  const host = hostOf(req);
@@ -2860,7 +2963,7 @@ async function startProxy(o) {
2860
2963
  }
2861
2964
  const preflight = !!route.auth && isPreflight(req.method, req.headers);
2862
2965
  const access = route.auth && !preflight ? route.auth.access(req.headers) : void 0;
2863
- const { headers, changes } = deliver(req.headers, route);
2966
+ const { headers, changes } = deliverFor(req, route);
2864
2967
  if (access?.kind === "denied") {
2865
2968
  const html = wantsHtml(req.headers.accept);
2866
2969
  const { id: id2, started: started2 } = record(req, route, headers, changes, false, html ? void 0 : access, DENIED_NO_REPLAY);
@@ -2894,6 +2997,7 @@ async function startProxy(o) {
2894
2997
  o.store.update(id, { redirect: { from, to } });
2895
2998
  }
2896
2999
  }
3000
+ scopeResponseCookies(id, req, route, up);
2897
3001
  res.writeHead(up.statusCode ?? 502, up.statusMessage, up.headers);
2898
3002
  trackResponse(id, started, up, reqCap);
2899
3003
  up.pipe(res);
@@ -2937,7 +3041,7 @@ async function startProxy(o) {
2937
3041
  const route = await routeFor(req);
2938
3042
  if (!route) return void client.end("HTTP/1.1 404 Not Found\r\nConnection: close\r\n\r\n");
2939
3043
  const access = route.auth?.access(req.headers);
2940
- const { headers, changes } = deliver(req.headers, route);
3044
+ const { headers, changes } = deliverFor(req, route);
2941
3045
  const { id, started } = record(req, route, headers, changes, true, access, access?.kind === "denied" ? DENIED_NO_REPLAY : void 0);
2942
3046
  if (access?.kind === "denied") {
2943
3047
  client.end(WS_DENIED);
@@ -3584,7 +3688,7 @@ async function runSession(specs, source, flags, configPath) {
3584
3688
  const warnings = credentialWarnings(specs);
3585
3689
  const watcher = configPath ? watchConfig({ path: configPath, onChange: (next) => void session.apply(next).catch((err) => session.configFailed(err instanceof Error ? err.message : String(err))), onError: (error) => session.configFailed(error) }) : null;
3586
3690
  if (wantsUi(flags)) {
3587
- const { runFullscreen } = await import("./fullscreen-Q3RGW6NJ.js");
3691
+ const { runFullscreen } = await import("./fullscreen-GM4ZV6FW.js");
3588
3692
  const code = await runFullscreen(session, specs, CLI_VERSION, inspector, inspectorError, update, warnings);
3589
3693
  watcher?.close();
3590
3694
  await inspector?.close().catch(() => void 0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dutodev/cli",
3
- "version": "1.8.1",
3
+ "version": "1.9.0",
4
4
  "description": "Expose a local port at https://<name>-<user>.duto.page with one command.",
5
5
  "license": "MIT",
6
6
  "type": "module",