offsite 0.0.61 → 0.0.62

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.
@@ -2,7 +2,7 @@ import { createHash, randomBytes, randomUUID } from "node:crypto";
2
2
  import { appendFileSync, chmodSync, closeSync, createReadStream, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, watch, writeFileSync } from "node:fs";
3
3
  import http, { createServer } from "node:http";
4
4
  import path from "node:path";
5
- import { createGzip } from "node:zlib";
5
+ import zlib, { createGzip } from "node:zlib";
6
6
  import { consola } from "consola";
7
7
  import { WebSocketServer } from "ws";
8
8
  import { z } from "zod";
@@ -25,9 +25,17 @@ import { integer, real, snakeCase, text } from "drizzle-orm/sqlite-core";
25
25
  import sharp from "sharp";
26
26
  import { promisify } from "node:util";
27
27
  import webPush from "web-push";
28
- import { createProxyServer } from "http-proxy-3";
29
- import { OUTGOING_PASSES } from "http-proxy-3/dist/lib/http-proxy/passes/web-outgoing.js";
28
+ import dns from "node:dns";
29
+ import { BlockList, isIP } from "node:net";
30
+ import { CookieJar, getPublicSuffix } from "tough-cookie";
30
31
  import { spawn as spawn$1 } from "@lydell/node-pty";
32
+ import { once } from "node:events";
33
+ import https from "node:https";
34
+ import { buffer } from "node:stream/consumers";
35
+ import { pipeline } from "node:stream/promises";
36
+ import { defaultTreeAdapter, html, parse, serialize } from "parse5";
37
+ import { MagicString } from "magic-string";
38
+ import { parseSync } from "oxc-parser";
31
39
  // ../core/src/media.ts
32
40
  const mediaTypes = /* @__PURE__ */ new Map([
33
41
  [".png", "image/png"],
@@ -2771,6 +2779,7 @@ const pushSubscriptions = snakeCase.table("push_subscriptions", {
2771
2779
  });
2772
2780
  // src/services/files.ts
2773
2781
  const FILE_LIMIT = 2e4;
2782
+ const SNIFF_LENGTH = 8e3;
2774
2783
  const OUTPUT_LIMIT$1 = 262144;
2775
2784
  const READ_LIMIT$1 = 1048576;
2776
2785
  const MEDIA_LINK_LIFETIME = 36e5;
@@ -2882,6 +2891,18 @@ function linkProjectDownload(cwd, file) {
2882
2891
  if (!statSync(full).isFile()) throw new Error(`${file} is not a file`);
2883
2892
  return addLink(full, "application/octet-stream");
2884
2893
  }
2894
+ function setBrowseHeaders(response, contentType) {
2895
+ response.setHeader("content-type", contentType);
2896
+ response.setHeader("cache-control", "no-cache");
2897
+ if (/^(text\/html|image\/svg|application\/xml)/.test(contentType)) response.setHeader("content-security-policy", "sandbox allow-scripts allow-forms allow-popups allow-modals allow-downloads");
2898
+ }
2899
+ function sniffType(file) {
2900
+ const head = Buffer.alloc(SNIFF_LENGTH);
2901
+ const handle = openSync(file, "r");
2902
+ const read = readSync(handle, head, 0, SNIFF_LENGTH, 0);
2903
+ closeSync(handle);
2904
+ return head.subarray(0, read).includes(0) ? "application/octet-stream" : "text/plain; charset=utf-8";
2905
+ }
2885
2906
  function addLink(file, mediaType) {
2886
2907
  for (const [token, link] of mediaLinks) if (link.expiresAt <= Date.now()) mediaLinks.delete(token);
2887
2908
  const token = randomBytes(32).toString("base64url");
@@ -5553,6 +5574,114 @@ var Run = class {
5553
5574
  });
5554
5575
  }
5555
5576
  };
5577
+ // src/lib/proxy/sites.ts
5578
+ const sites = /* @__PURE__ */ new Map();
5579
+ const siteNames = /* @__PURE__ */ new Map();
5580
+ const privateAddresses = new BlockList();
5581
+ for (const range of [
5582
+ ["0.0.0.0", 8],
5583
+ ["10.0.0.0", 8],
5584
+ ["100.64.0.0", 10],
5585
+ ["127.0.0.0", 8],
5586
+ ["169.254.0.0", 16],
5587
+ ["172.16.0.0", 12],
5588
+ ["192.0.0.0", 24],
5589
+ ["192.168.0.0", 16],
5590
+ ["198.18.0.0", 15],
5591
+ ["224.0.0.0", 4],
5592
+ ["240.0.0.0", 4]
5593
+ ]) privateAddresses.addSubnet(range[0], range[1], "ipv4");
5594
+ for (const range of [
5595
+ ["::", 128],
5596
+ ["::1", 128],
5597
+ ["fc00::", 7],
5598
+ ["fe80::", 10],
5599
+ ["ff00::", 8]
5600
+ ]) privateAddresses.addSubnet(range[0], range[1], "ipv6");
5601
+ function proxiedPath(key, target) {
5602
+ return `/api/proxy/${key}/${target.protocol === "https:" ? "https" : "http"}/${target.host}${target.pathname}${target.search}${target.hash}`;
5603
+ }
5604
+ function linkRewriter(key, base) {
5605
+ return (value) => {
5606
+ const text = value.trim();
5607
+ if (text === "" || text.startsWith("#")) return value;
5608
+ if (text.startsWith(`/api/proxy/${key}/`)) return text;
5609
+ const target = URL.parse(text, base);
5610
+ if (target === null || target.protocol !== "http:" && target.protocol !== "https:") return value;
5611
+ return proxiedPath(key, target);
5612
+ };
5613
+ }
5614
+ function findSite(key) {
5615
+ return sites.get(key) ?? null;
5616
+ }
5617
+ async function openSite(address, fromPage) {
5618
+ const target = new URL(address);
5619
+ if (target.protocol !== "http:" && target.protocol !== "https:") throw new Error(`${address} is not an http or https address`);
5620
+ const local = await isLocalHost(hostOf(target));
5621
+ if (local && fromPage) throw new Error("A website cannot open an address on this machine");
5622
+ const name = local ? `local ${target.hostname}` : `public ${getPublicSuffix(target.hostname) ?? target.hostname}`;
5623
+ const existing = siteNames.get(name);
5624
+ if (existing !== void 0) return proxiedPath(existing.key, target);
5625
+ const site = {
5626
+ key: randomBytes(16).toString("hex"),
5627
+ local,
5628
+ jar: new CookieJar(void 0, {
5629
+ allowSpecialUseDomain: true,
5630
+ rejectPublicSuffixes: !local
5631
+ }),
5632
+ storage: /* @__PURE__ */ new Map()
5633
+ };
5634
+ sites.set(site.key, site);
5635
+ siteNames.set(name, site);
5636
+ return proxiedPath(site.key, target);
5637
+ }
5638
+ function hostOf(target) {
5639
+ return target.hostname.replace(/^\[|\]$/g, "");
5640
+ }
5641
+ function checkAddress(site, address, port, ownPort) {
5642
+ if (!isPrivate(address)) return;
5643
+ if (!site.local) throw new Error("Websites cannot reach addresses on this machine");
5644
+ if (port === ownPort) throw new Error("Pages cannot reach Offsite itself");
5645
+ }
5646
+ function guardedLookup(site, port, ownPort) {
5647
+ return (hostname, options, callback) => {
5648
+ dns.lookup(hostname, {
5649
+ ...options,
5650
+ all: true
5651
+ }, (error, addresses) => {
5652
+ if (error !== null) {
5653
+ callback(error, "", 0);
5654
+ return;
5655
+ }
5656
+ try {
5657
+ for (const item of addresses) checkAddress(site, item.address, port, ownPort);
5658
+ } catch (caught) {
5659
+ callback(caught, "", 0);
5660
+ return;
5661
+ }
5662
+ if (options.all === true) {
5663
+ callback(null, addresses);
5664
+ return;
5665
+ }
5666
+ const first = addresses[0];
5667
+ if (first === void 0) {
5668
+ callback(/* @__PURE__ */ new Error(`${hostname} has no address`), "", 0);
5669
+ return;
5670
+ }
5671
+ callback(null, first.address, first.family);
5672
+ });
5673
+ };
5674
+ }
5675
+ function isPrivate(address) {
5676
+ const family = isIP(address);
5677
+ if (family === 4) return privateAddresses.check(address, "ipv4");
5678
+ if (family === 6) return privateAddresses.check(address, "ipv6");
5679
+ throw new Error(`${address} is not an IP address`);
5680
+ }
5681
+ async function isLocalHost(hostname) {
5682
+ if (isIP(hostname) !== 0) return isPrivate(hostname);
5683
+ return (await dns.promises.lookup(hostname, { all: true })).some((item) => isPrivate(item.address));
5684
+ }
5556
5685
  // src/services/message-search.ts
5557
5686
  const READ_LIMIT = 30;
5558
5687
  const CODEX_READ_LIMIT = 10;
@@ -5687,140 +5816,6 @@ function bestMatch(query, sessionId, messages) {
5687
5816
  }
5688
5817
  return best;
5689
5818
  }
5690
- // src/services/proxy.ts
5691
- const proxies = /* @__PURE__ */ new Map();
5692
- const pageScriptPath = "/__offsite/browser.js";
5693
- function pageScript(origin) {
5694
- return `(() => {
5695
- if (window.parent === window || window.navigation === undefined) {
5696
- return
5697
- }
5698
- const post = (message) =>
5699
- window.parent.postMessage({ offsite: 'browser', ...message }, '*')
5700
- const report = () =>
5701
- post({
5702
- event: 'location',
5703
- url: ${JSON.stringify(origin)} + location.pathname + location.search + location.hash,
5704
- proxied: location.href,
5705
- back: navigation.canGoBack,
5706
- forward: navigation.canGoForward,
5707
- })
5708
- addEventListener('pageshow', report)
5709
- navigation.addEventListener('currententrychange', report)
5710
- navigation.addEventListener('navigate', (event) => {
5711
- if (!event.destination.sameDocument && event.downloadRequest === null) {
5712
- post({ event: 'navigate' })
5713
- }
5714
- })
5715
- addEventListener('message', (event) => {
5716
- if (event.source !== window.parent || event.data?.offsite !== 'browser') {
5717
- return
5718
- }
5719
- if (event.data.command === 'back') {
5720
- navigation.back()
5721
- }
5722
- if (event.data.command === 'forward') {
5723
- navigation.forward()
5724
- }
5725
- if (event.data.command === 'reload') {
5726
- location.reload()
5727
- }
5728
- })
5729
- })()
5730
- `;
5731
- }
5732
- function proxyPort(origin) {
5733
- const target = new URL(origin);
5734
- if (target.protocol !== "http:" && target.protocol !== "https:") throw new Error(`${origin} is not an http or https address`);
5735
- const existing = proxies.get(target.origin);
5736
- if (existing !== void 0) return existing;
5737
- const started = startProxy(target);
5738
- proxies.set(target.origin, started);
5739
- started.catch(() => proxies.delete(target.origin));
5740
- return started;
5741
- }
5742
- function startProxy(target) {
5743
- const options = {
5744
- target,
5745
- changeOrigin: true,
5746
- ws: true,
5747
- secure: false,
5748
- autoRewrite: true,
5749
- protocolRewrite: "http",
5750
- cookieDomainRewrite: "",
5751
- selfHandleResponse: true
5752
- };
5753
- const proxy = createProxyServer(options);
5754
- proxy.on("proxyReq", (outgoing, request) => {
5755
- rewriteOrigin(target, outgoing, request);
5756
- if (request.headers.accept?.startsWith("text/html") === true) outgoing.removeHeader("accept-encoding");
5757
- });
5758
- proxy.on("proxyReqWs", (outgoing, request) => rewriteOrigin(target, outgoing, request));
5759
- proxy.on("proxyRes", async (incoming, request, response) => {
5760
- try {
5761
- await rewriteRedirect(target, incoming, request);
5762
- } catch (error) {
5763
- response.writeHead(502, { "content-type": "text/plain; charset=utf-8" });
5764
- response.end(`Could not follow the redirect from ${target.origin}: ${error instanceof Error ? error.message : String(error)}`);
5765
- return;
5766
- }
5767
- delete incoming.headers["x-frame-options"];
5768
- delete incoming.headers["content-security-policy"];
5769
- if (incoming.headers["set-cookie"] !== void 0) incoming.headers["set-cookie"] = incoming.headers["set-cookie"].map((cookie) => cookie.replace(/;\s*secure/i, "").replace(/;\s*samesite=none/i, ""));
5770
- const page = request.method === "GET" && request.headers.accept?.startsWith("text/html") === true && incoming.statusCode !== 204 && incoming.statusCode !== 304 && /^text\/html/i.test(incoming.headers["content-type"] ?? "") && incoming.headers["content-encoding"] === void 0;
5771
- if (page) delete incoming.headers["content-length"];
5772
- for (const pass of Object.values(OUTGOING_PASSES)) pass(request, response, incoming, options);
5773
- if (!page) {
5774
- incoming.pipe(response);
5775
- return;
5776
- }
5777
- const chunks = [];
5778
- incoming.on("data", (chunk) => chunks.push(chunk));
5779
- incoming.on("end", () => response.end(Buffer.from(Buffer.concat(chunks).toString("latin1").replace(/^(\xEF\xBB\xBF)?\s*(<!doctype[^>]*>)?/i, `$&<script src="${pageScriptPath}"><\/script>`), "latin1")));
5780
- });
5781
- proxy.on("error", (error, _request, response) => {
5782
- if (!(response instanceof http.ServerResponse)) {
5783
- response.destroy();
5784
- return;
5785
- }
5786
- if (!response.headersSent) response.writeHead(502, { "content-type": "text/plain; charset=utf-8" });
5787
- response.end(`Could not reach ${target.origin}: ${error.message}`);
5788
- });
5789
- const server = http.createServer((request, response) => {
5790
- if (request.url === pageScriptPath) {
5791
- response.writeHead(200, { "content-type": "text/javascript; charset=utf-8" });
5792
- response.end(pageScript(target.origin));
5793
- return;
5794
- }
5795
- proxy.web(request, response);
5796
- });
5797
- server.on("upgrade", (request, socket, head) => proxy.ws(request, socket, head));
5798
- return new Promise((resolve, reject) => {
5799
- server.once("error", reject);
5800
- server.listen(0, args.host, () => {
5801
- const address = server.address();
5802
- if (address === null || typeof address === "string") {
5803
- reject(/* @__PURE__ */ new Error(`the proxy for ${target.origin} has no port`));
5804
- return;
5805
- }
5806
- resolve(address.port);
5807
- });
5808
- });
5809
- }
5810
- async function rewriteRedirect(target, incoming, request) {
5811
- const location = incoming.headers.location;
5812
- if (location === void 0 || !/^30[1278]$/.test(String(incoming.statusCode))) return;
5813
- const next = new URL(location, target);
5814
- if (next.origin === target.origin || next.protocol !== "http:" && next.protocol !== "https:") return;
5815
- if (request.headers.host === void 0) throw new Error("the redirected request has no host");
5816
- const proxied = new URL(`http://${request.headers.host}`);
5817
- proxied.port = String(await proxyPort(next.origin));
5818
- incoming.headers.location = `${proxied.origin}${next.pathname}${next.search}${next.hash}`;
5819
- }
5820
- function rewriteOrigin(target, outgoing, request) {
5821
- if (request.headers.origin !== void 0) outgoing.setHeader("origin", target.origin);
5822
- if (request.headers.referer !== void 0) outgoing.setHeader("referer", request.headers.referer.replace(`http://${request.headers.host}`, target.origin));
5823
- }
5824
5819
  // src/services/runs.ts
5825
5820
  const locks = /* @__PURE__ */ new Map();
5826
5821
  function stopAll() {
@@ -6961,7 +6956,7 @@ function attach(client) {
6961
6956
  layout: layout(),
6962
6957
  instance
6963
6958
  });
6964
- for (const tab of tabs) forward(connection, tab.id, true, Buffer.concat(terminals.get(tab.id)?.history ?? []));
6959
+ for (const tab of tabs) forward$1(connection, tab.id, true, Buffer.concat(terminals.get(tab.id)?.history ?? []));
6965
6960
  }
6966
6961
  function resize(connection, message) {
6967
6962
  const terminal = terminals.get(message.id);
@@ -7097,7 +7092,7 @@ function output(id, terminal, data) {
7097
7092
  terminal.historySize -= terminal.history[0].length;
7098
7093
  terminal.history.shift();
7099
7094
  }
7100
- for (const connection of connections.values()) forward(connection, id, false, data);
7095
+ for (const connection of connections.values()) forward$1(connection, id, false, data);
7101
7096
  scan(id, terminal, data);
7102
7097
  if ([...connections.values()].some((connection) => connection.client.socket.bufferedAmount > OUTPUT_LIMIT)) {
7103
7098
  terminal.pty.pause();
@@ -7202,7 +7197,7 @@ function publish() {
7202
7197
  tabs
7203
7198
  });
7204
7199
  }
7205
- function forward(connection, id, replay, data) {
7200
+ function forward$1(connection, id, replay, data) {
7206
7201
  connection.client.socket.send(encodeFrame(replay ? 3 : 2, id, data));
7207
7202
  }
7208
7203
  function send(connection, event) {
@@ -7433,7 +7428,7 @@ const handlers = {
7433
7428
  "files.download": (params) => linkProjectDownload(projectPath(params.projectId), params.path),
7434
7429
  "files.upload": (params) => saveUpload(params.name, Buffer.from(params.data, "base64")),
7435
7430
  "files.browse": () => browseToken,
7436
- "browser.proxy": (params) => proxyPort(params.origin),
7431
+ "browser.open": (params) => openSite(params.url, params.fromPage),
7437
7432
  "tasks.output": (params) => taskOutput(params.sessionId, params.path),
7438
7433
  "git.status": (params) => gitRepos(projectPath(params.projectId)),
7439
7434
  "git.diff": (params) => projectDiff(projectPath(params.projectId)),
@@ -7537,6 +7532,1155 @@ async function answer(client, text) {
7537
7532
  });
7538
7533
  }
7539
7534
  }
7535
+ // src/lib/proxy/css.ts
7536
+ function rewriteCss(source, rewrite) {
7537
+ return source.replace(/url\(\s*(['"]?)(.*?)\1\s*\)/gi, (_match, quote, value) => `url(${quote}${rewrite(value)}${quote})`).replace(/@import\s+(['"])(.*?)\1/gi, (_match, quote, value) => `@import ${quote}${rewrite(value)}${quote}`);
7538
+ }
7539
+ // src/lib/proxy/script.ts
7540
+ const CACHE_LIMIT = 500;
7541
+ const MARKERS = /\b(?:location|top|parent|import)\b/;
7542
+ const WINDOWS = /* @__PURE__ */ new Set([
7543
+ "window",
7544
+ "self",
7545
+ "globalThis",
7546
+ "frames"
7547
+ ]);
7548
+ const FUNCTIONS = /* @__PURE__ */ new Set([
7549
+ "FunctionDeclaration",
7550
+ "FunctionExpression",
7551
+ "ArrowFunctionExpression"
7552
+ ]);
7553
+ const PATTERNS = /* @__PURE__ */ new Set([
7554
+ "ObjectPattern",
7555
+ "ArrayPattern",
7556
+ "RestElement",
7557
+ "AssignmentPattern",
7558
+ "Property"
7559
+ ]);
7560
+ const NAMES = /* @__PURE__ */ new Set([
7561
+ "ImportSpecifier",
7562
+ "ImportDefaultSpecifier",
7563
+ "ImportNamespaceSpecifier",
7564
+ "ExportSpecifier",
7565
+ "LabeledStatement",
7566
+ "BreakStatement",
7567
+ "ContinueStatement"
7568
+ ]);
7569
+ const KEYS = /* @__PURE__ */ new Set([
7570
+ "Property",
7571
+ "MethodDefinition",
7572
+ "PropertyDefinition"
7573
+ ]);
7574
+ const cache = /* @__PURE__ */ new Map();
7575
+ function rewriteScript(source, rewrite, scope) {
7576
+ if (!MARKERS.test(source)) return source;
7577
+ const id = createHash("sha1").update(scope).update(source).digest("base64");
7578
+ const known = cache.get(id);
7579
+ if (known !== void 0) return known;
7580
+ const rewritten = rewriteSource(source, rewrite);
7581
+ if (cache.size >= CACHE_LIMIT) cache.clear();
7582
+ cache.set(id, rewritten);
7583
+ return rewritten;
7584
+ }
7585
+ function rewriteHandler(source, rewrite) {
7586
+ const rewritten = rewriteSource(`function handler(event){${source}
7587
+ }`, rewrite);
7588
+ return rewritten.slice(24, rewritten.length - 2);
7589
+ }
7590
+ function rewriteSource(source, rewrite) {
7591
+ const parsed = parseSync("page.js", source, {
7592
+ lang: "js",
7593
+ sourceType: "unambiguous"
7594
+ });
7595
+ if (parsed.errors.some((error) => error.severity === "Error")) return source;
7596
+ const output = new MagicString(source);
7597
+ walk(parsed.program, null, false, {
7598
+ source,
7599
+ output,
7600
+ rewrite
7601
+ });
7602
+ return output.toString();
7603
+ }
7604
+ function walk(node, parent, binding, context) {
7605
+ visit(node, parent, binding, context);
7606
+ for (const field of Object.keys(node)) {
7607
+ const value = node[field];
7608
+ const position = {
7609
+ node,
7610
+ field,
7611
+ parent
7612
+ };
7613
+ if (Array.isArray(value)) {
7614
+ for (const item of value) if (isNode(item)) walk(item, position, inBinding(node, field, binding), context);
7615
+ } else if (isNode(value)) walk(value, position, inBinding(node, field, binding), context);
7616
+ }
7617
+ }
7618
+ function visit(node, parent, binding, context) {
7619
+ const output = context.output;
7620
+ if ((node.type === "ImportDeclaration" || node.type === "ExportNamedDeclaration" || node.type === "ExportAllDeclaration") && isNode(node.source)) {
7621
+ const specifier = String(node.source.value);
7622
+ if (specifier.startsWith("/") || /^[a-z][a-z\d+.-]*:/i.test(specifier)) output.overwrite(node.source.start, node.source.end, JSON.stringify(context.rewrite(specifier)));
7623
+ return;
7624
+ }
7625
+ if (node.type === "ImportExpression" && isNode(node.source)) {
7626
+ wrap(output, node.source, "__oi");
7627
+ return;
7628
+ }
7629
+ if (node.type === "AssignmentExpression") {
7630
+ if (node.operator === "=" && isNode(node.left) && isNode(node.right) && isLocationTarget(node.left)) {
7631
+ const current = context.source.slice(node.left.start, node.left.end);
7632
+ output.appendRight(node.right.start, `__ou(${current},`);
7633
+ output.appendLeft(node.right.end, ")");
7634
+ }
7635
+ return;
7636
+ }
7637
+ if (node.type === "Identifier") {
7638
+ visitIdentifier(node, parent, binding, output);
7639
+ return;
7640
+ }
7641
+ if (node.type === "MemberExpression" && !binding) visitMember(node, parent, output);
7642
+ }
7643
+ function visitIdentifier(node, parent, binding, output) {
7644
+ const name = String(node.name);
7645
+ if (name !== "location" && name !== "top" && name !== "parent") return;
7646
+ const wrapper = name === "location" ? "__ol" : "__ow";
7647
+ if (parent?.node.type === "Property" && parent.field === "value" && parent.node.shorthand === true && !binding) {
7648
+ output.appendRight(node.start, `${name}:${wrapper}(`);
7649
+ output.appendLeft(node.end, ")");
7650
+ return;
7651
+ }
7652
+ if (binding || !isRead(node, parent)) return;
7653
+ wrap(output, node, wrapper);
7654
+ }
7655
+ function visitMember(node, parent, output) {
7656
+ if (!isNode(node.property) || !isNode(node.object)) return;
7657
+ if (node.computed === true) return;
7658
+ const name = String(node.property.name);
7659
+ if (name === "url" && node.object.type === "MetaProperty" && isRead(node, parent)) {
7660
+ wrap(output, node, "__om");
7661
+ return;
7662
+ }
7663
+ if (name === "location" && isRead(node, parent) && !insideChain(parent)) {
7664
+ wrap(output, node, "__ol");
7665
+ return;
7666
+ }
7667
+ if ((name === "top" || name === "parent") && node.object.type === "Identifier" && WINDOWS.has(String(node.object.name)) && isRead(node, parent) && !insideChain(parent)) wrap(output, node, "__ow");
7668
+ }
7669
+ function isRead(node, parent) {
7670
+ if (parent === null) return true;
7671
+ const owner = parent.node;
7672
+ const field = parent.field;
7673
+ if (owner.type === "MemberExpression" && field === "property") return owner.computed === true;
7674
+ if (KEYS.has(owner.type) && field === "key") return owner.computed === true;
7675
+ if (NAMES.has(owner.type)) return false;
7676
+ if (owner.type === "CallExpression" && field === "callee") return false;
7677
+ if (owner.type === "UpdateExpression") return false;
7678
+ if (owner.type === "AssignmentExpression" && field === "left") return false;
7679
+ if (owner.type === "UnaryExpression") return owner.operator !== "delete" && owner.operator !== "typeof";
7680
+ return true;
7681
+ }
7682
+ function inBinding(node, field, binding) {
7683
+ if (node.type === "VariableDeclarator") return field === "id";
7684
+ if (FUNCTIONS.has(node.type)) return field === "params" || field === "id";
7685
+ if (node.type === "ClassExpression" || node.type === "ClassDeclaration") return field === "id";
7686
+ if (node.type === "CatchClause") return field === "param";
7687
+ if (node.type === "ForInStatement" || node.type === "ForOfStatement") return field === "left";
7688
+ if (node.type === "AssignmentExpression") return field === "left";
7689
+ if (node.type === "AssignmentPattern" && field === "right") return false;
7690
+ if (node.type === "Property" && field === "key") return false;
7691
+ return binding && PATTERNS.has(node.type);
7692
+ }
7693
+ function isLocationTarget(node) {
7694
+ if (node.type === "Identifier") return node.name === "location";
7695
+ return node.type === "MemberExpression" && node.computed !== true && isNode(node.property) && node.property.name === "location";
7696
+ }
7697
+ function insideChain(parent) {
7698
+ return parent !== null && parent.node.type === "MemberExpression" && parent.field === "object" && hasChain(parent);
7699
+ }
7700
+ function hasChain(parent) {
7701
+ if (parent === null) return false;
7702
+ if (parent.node.type === "ChainExpression") return true;
7703
+ return hasChain(parent.parent);
7704
+ }
7705
+ function wrap(output, node, name) {
7706
+ output.appendRight(node.start, `${name}(`);
7707
+ output.appendLeft(node.end, ")");
7708
+ }
7709
+ function isNode(value) {
7710
+ return typeof value === "object" && value !== null && "type" in value && typeof value.type === "string";
7711
+ }
7712
+ // src/lib/proxy/html.ts
7713
+ const LINKS = /* @__PURE__ */ new Set([
7714
+ "href",
7715
+ "src",
7716
+ "action",
7717
+ "formaction",
7718
+ "poster",
7719
+ "data",
7720
+ "background"
7721
+ ]);
7722
+ const LINK_TAGS = /* @__PURE__ */ new Set([
7723
+ "a",
7724
+ "area",
7725
+ "audio",
7726
+ "base",
7727
+ "body",
7728
+ "button",
7729
+ "embed",
7730
+ "form",
7731
+ "iframe",
7732
+ "img",
7733
+ "input",
7734
+ "link",
7735
+ "object",
7736
+ "script",
7737
+ "source",
7738
+ "td",
7739
+ "track",
7740
+ "video"
7741
+ ]);
7742
+ const REMOVED = /* @__PURE__ */ new Set([
7743
+ "integrity",
7744
+ "crossorigin",
7745
+ "nonce",
7746
+ "ping"
7747
+ ]);
7748
+ const NAVIGATING = /* @__PURE__ */ new Set([
7749
+ "a",
7750
+ "area",
7751
+ "base",
7752
+ "form"
7753
+ ]);
7754
+ const SCRIPT_TYPES = /* @__PURE__ */ new Set([
7755
+ "",
7756
+ "module",
7757
+ "text/javascript",
7758
+ "application/javascript",
7759
+ "text/ecmascript",
7760
+ "application/ecmascript"
7761
+ ]);
7762
+ const META_REMOVED = /* @__PURE__ */ new Set([
7763
+ "content-security-policy",
7764
+ "content-security-policy-report-only",
7765
+ "x-frame-options"
7766
+ ]);
7767
+ function rewriteHtml(source, key, base, script) {
7768
+ const document = parse(source);
7769
+ rewriteChildren(document, {
7770
+ key,
7771
+ base,
7772
+ rewrite: linkRewriter(key, base)
7773
+ });
7774
+ const head = findHead(document);
7775
+ const runtime = defaultTreeAdapter.createElement("script", html.NS.HTML, []);
7776
+ defaultTreeAdapter.insertText(runtime, script);
7777
+ const first = head.childNodes[0];
7778
+ if (first === void 0) defaultTreeAdapter.appendChild(head, runtime);
7779
+ else defaultTreeAdapter.insertBefore(head, runtime, first);
7780
+ return serialize(document);
7781
+ }
7782
+ function findHead(node) {
7783
+ for (const child of node.childNodes) if ("tagName" in child) {
7784
+ if (child.tagName === "head") return child;
7785
+ if (child.tagName === "html") return findHead(child);
7786
+ }
7787
+ throw new Error("the page has no head");
7788
+ }
7789
+ function rewriteChildren(node, page) {
7790
+ const remove = [];
7791
+ for (const child of node.childNodes) {
7792
+ if (!("tagName" in child)) continue;
7793
+ if (rewriteElement(child, page)) {
7794
+ remove.push(child);
7795
+ continue;
7796
+ }
7797
+ rewriteChildren(child, page);
7798
+ if ("content" in child) rewriteChildren(child.content, page);
7799
+ }
7800
+ for (const child of remove) defaultTreeAdapter.detachNode(child);
7801
+ }
7802
+ function rewriteElement(element, page) {
7803
+ const tag = element.tagName;
7804
+ if (tag === "meta") {
7805
+ const equiv = attribute(element, "http-equiv")?.value.toLowerCase();
7806
+ if (equiv !== void 0 && META_REMOVED.has(equiv)) return true;
7807
+ if (attribute(element, "name")?.value.toLowerCase() === "referrer") return true;
7808
+ if (equiv === "refresh") rewriteRefresh(element, page);
7809
+ }
7810
+ if (tag === "base") {
7811
+ const href = attribute(element, "href")?.value;
7812
+ const next = href === void 0 ? null : URL.parse(href, page.base);
7813
+ if (next !== null) {
7814
+ page.base = next;
7815
+ page.rewrite = linkRewriter(page.key, next);
7816
+ }
7817
+ }
7818
+ element.attrs = element.attrs.filter((item) => !REMOVED.has(item.name));
7819
+ if (NAVIGATING.has(tag)) element.attrs = element.attrs.filter((item) => item.name !== "target" || !/^_(top|parent|blank)$/i.test(item.value.trim()));
7820
+ for (const item of element.attrs) if (LINKS.has(item.name) && LINK_TAGS.has(tag)) item.value = page.rewrite(item.value);
7821
+ else if (item.name === "srcset" || item.name === "imagesrcset") item.value = rewriteSrcset(item.value, page.rewrite);
7822
+ else if (item.name === "style") item.value = rewriteCss(item.value, page.rewrite);
7823
+ else if (item.name.startsWith("on")) item.value = rewriteHandler(item.value, page.rewrite);
7824
+ if (tag === "style") rewriteText(element, (text) => rewriteCss(text, page.rewrite));
7825
+ if (tag === "script") rewriteInlineScript(element, page);
7826
+ return false;
7827
+ }
7828
+ function rewriteInlineScript(element, page) {
7829
+ const type = (attribute(element, "type")?.value ?? "").trim().toLowerCase();
7830
+ if (type === "importmap") {
7831
+ rewriteText(element, (text) => JSON.stringify(rewriteMapLinks(JSON.parse(text), page)));
7832
+ return;
7833
+ }
7834
+ if (SCRIPT_TYPES.has(type)) rewriteText(element, (text) => rewriteScript(text, page.rewrite, `${page.key} ${page.base.origin}`));
7835
+ }
7836
+ function rewriteMapLinks(value, page) {
7837
+ if (typeof value === "string") return page.rewrite(value);
7838
+ if (typeof value !== "object" || value === null) return value;
7839
+ return Object.fromEntries(Object.entries(value).map((entry) => [entry[0], rewriteMapLinks(entry[1], page)]));
7840
+ }
7841
+ function rewriteText(element, rewrite) {
7842
+ for (const child of element.childNodes) if ("value" in child) child.value = rewrite(child.value);
7843
+ }
7844
+ function rewriteRefresh(element, page) {
7845
+ const content = attribute(element, "content");
7846
+ if (content === void 0) return;
7847
+ content.value = content.value.replace(/(url\s*=\s*['"]?)([^'"]+)/i, (_match, label, url) => `${label}${page.rewrite(url)}`);
7848
+ }
7849
+ function rewriteSrcset(value, rewrite) {
7850
+ return value.split(/,\s+/).map((candidate) => {
7851
+ const parts = candidate.trim().split(/\s+/);
7852
+ const url = parts[0];
7853
+ if (url === void 0 || url === "") return candidate;
7854
+ return [rewrite(url), ...parts.slice(1)].join(" ");
7855
+ }).join(", ");
7856
+ }
7857
+ function attribute(element, name) {
7858
+ return element.attrs.find((item) => item.name === name);
7859
+ }
7860
+ // src/lib/proxy/page-script.ts
7861
+ function pageScript(config) {
7862
+ const json = JSON.stringify(config).replaceAll("<", "\\u003c");
7863
+ return `(${PAGE_SCRIPT})(${json})`;
7864
+ }
7865
+ const PAGE_SCRIPT = String.raw`function (config) {
7866
+ if (self.__ol) return
7867
+ const inWindow = typeof document !== 'undefined'
7868
+ const realLocation = self.location
7869
+ const origin = config.origin ?? realLocation.origin
7870
+ const prefix = '/api/proxy/' + config.key + '/'
7871
+ const realFetch = self.fetch.bind(self)
7872
+ const native = { get: inWindow && Element.prototype.getAttribute, set: inWindow && Element.prototype.setAttribute }
7873
+ const realPost = inWindow && window.postMessage.bind(window)
7874
+ const panel = inWindow && window.parent !== window && window.parent === window.top ? window.parent : null
7875
+ const skipped = /^\s*(data|blob|javascript|about|mailto|tel|sms|file):/i
7876
+ const schemes = { 'http:': 'http', 'https:': 'https', 'ws:': 'http', 'wss:': 'https' }
7877
+
7878
+ function hide(name, value) {
7879
+ Object.defineProperty(self, name, { value, enumerable: false, configurable: true, writable: true })
7880
+ }
7881
+ function decode(value) {
7882
+ const text = String(value)
7883
+ const index = text.indexOf(prefix)
7884
+ if (index === -1 || (index !== 0 && !text.startsWith(origin))) return null
7885
+ const rest = text.slice(index + prefix.length)
7886
+ const slash = rest.indexOf('/')
7887
+ const scheme = rest.slice(0, slash)
7888
+ if (scheme !== 'http' && scheme !== 'https') return null
7889
+ return scheme + '://' + rest.slice(slash + 1)
7890
+ }
7891
+ function original(value) {
7892
+ return decode(value) ?? value
7893
+ }
7894
+ function pageUrl() {
7895
+ return decode(realLocation.href) ?? config.url
7896
+ }
7897
+ function baseUrl() {
7898
+ const base = inWindow && document.querySelector('base[href]')
7899
+ return base ? (decode(native.get.call(base, 'href')) ?? pageUrl()) : pageUrl()
7900
+ }
7901
+ function encode(value, base) {
7902
+ const text = String(value)
7903
+ if (text === '' || text.startsWith('#') || skipped.test(text) || decode(text) !== null) return text
7904
+ const url = URL.parse(text, base ?? baseUrl())
7905
+ if (url === null || schemes[url.protocol] === undefined) return text
7906
+ return prefix + schemes[url.protocol] + '/' + url.host + url.pathname + url.search + url.hash
7907
+ }
7908
+ function absolute(value) {
7909
+ const encoded = encode(value)
7910
+ return encoded.startsWith('/') ? origin + encoded : encoded
7911
+ }
7912
+ function encodeSet(value) {
7913
+ return String(value).split(/,\s+/).map(function (item) {
7914
+ const parts = item.trim().split(/\s+/)
7915
+ parts[0] = encode(parts[0])
7916
+ return parts.join(' ')
7917
+ }).join(', ')
7918
+ }
7919
+ function socketUrl(value) {
7920
+ return (origin.startsWith('https') ? 'wss://' : 'ws://') + origin.replace(/^https?:\/\//, '') + encode(value)
7921
+ }
7922
+ function postPanel(message) {
7923
+ if (panel) panel.postMessage(Object.assign({ offsite: 'browser' }, message), '*')
7924
+ }
7925
+ function navigate(value, replace) {
7926
+ const next = URL.parse(String(value), baseUrl())
7927
+ if (next === null) return
7928
+ const web = next.protocol === 'http:' || next.protocol === 'https:'
7929
+ if (panel && web && next.host !== new URL(pageUrl()).host) {
7930
+ postPanel({ event: 'open', url: next.href })
7931
+ return
7932
+ }
7933
+ if (replace) realLocation.replace(encode(next.href))
7934
+ else realLocation.assign(encode(next.href))
7935
+ }
7936
+
7937
+ const fakeLocation = {}
7938
+ for (const part of ['href', 'protocol', 'host', 'hostname', 'port', 'pathname', 'search', 'hash', 'origin']) {
7939
+ Object.defineProperty(fakeLocation, part, {
7940
+ enumerable: true,
7941
+ get() {
7942
+ return new URL(pageUrl())[part]
7943
+ },
7944
+ set(value) {
7945
+ if (part === 'origin') return
7946
+ if (part === 'hash') {
7947
+ realLocation.hash = value
7948
+ return
7949
+ }
7950
+ if (part === 'href') {
7951
+ navigate(value)
7952
+ return
7953
+ }
7954
+ const next = new URL(pageUrl())
7955
+ next[part] = value
7956
+ navigate(next.href)
7957
+ },
7958
+ })
7959
+ }
7960
+ fakeLocation.assign = function (value) { navigate(value) }
7961
+ fakeLocation.replace = function (value) { navigate(value, true) }
7962
+ fakeLocation.reload = function () { realLocation.reload() }
7963
+ fakeLocation.toString = function () { return pageUrl() }
7964
+ fakeLocation.valueOf = function () { return fakeLocation }
7965
+ fakeLocation[Symbol.toPrimitive] = function () { return pageUrl() }
7966
+ fakeLocation.ancestorOrigins = []
7967
+
7968
+ function pageTop() {
7969
+ if (!inWindow || window.parent === window) return self
7970
+ let current = window
7971
+ while (current.parent !== window.top) current = current.parent
7972
+ return current
7973
+ }
7974
+ hide('__ol', function (value) { return value === realLocation ? fakeLocation : value })
7975
+ hide('__ow', function (value) {
7976
+ if (!inWindow) return value
7977
+ if (value === window.top) return pageTop()
7978
+ return value
7979
+ })
7980
+ hide('__ou', function (current, value) {
7981
+ if (current === realLocation || (inWindow && current === window.top.location)) return encode(value)
7982
+ return value
7983
+ })
7984
+ hide('__oi', function (value) {
7985
+ const text = String(value)
7986
+ return /^(\/|https?:)/.test(text) ? absolute(text) : text
7987
+ })
7988
+ hide('__om', original)
7989
+
7990
+ function patch(owner, name, wrap) {
7991
+ const descriptor = Object.getOwnPropertyDescriptor(owner, name)
7992
+ if (descriptor === undefined) return
7993
+ Object.defineProperty(owner, name, Object.assign({}, descriptor, wrap(descriptor)))
7994
+ }
7995
+ function getter(owner, name, read) {
7996
+ patch(owner, name, function (descriptor) {
7997
+ return { get() { return read(descriptor.get.call(this), this) } }
7998
+ })
7999
+ }
8000
+
8001
+ self.fetch = function (input, init) {
8002
+ const options = Object.assign({}, init, { credentials: 'omit' })
8003
+ if (options.mode === 'same-origin') options.mode = 'cors'
8004
+ if (input instanceof RealRequest) return realFetch(input, options)
8005
+ return realFetch(absolute(input), options)
8006
+ }
8007
+ const RealRequest = self.Request
8008
+ self.Request = class Request extends RealRequest {
8009
+ constructor(input, init) {
8010
+ super(input instanceof RealRequest ? input : absolute(input), init)
8011
+ }
8012
+ }
8013
+ getter(RealRequest.prototype, 'url', original)
8014
+ getter(Response.prototype, 'url', original)
8015
+ const realOpen = XMLHttpRequest.prototype.open
8016
+ XMLHttpRequest.prototype.open = function (method, url) {
8017
+ const rest = Array.prototype.slice.call(arguments, 2)
8018
+ return realOpen.call(this, method, encode(url), ...rest)
8019
+ }
8020
+ patch(XMLHttpRequest.prototype, 'withCredentials', function () {
8021
+ return { get() { return false }, set() {} }
8022
+ })
8023
+ getter(XMLHttpRequest.prototype, 'responseURL', original)
8024
+ self.WebSocket = new Proxy(self.WebSocket, {
8025
+ construct(target, args) {
8026
+ args[0] = socketUrl(args[0])
8027
+ return Reflect.construct(target, args)
8028
+ },
8029
+ })
8030
+ if (self.EventSource) {
8031
+ self.EventSource = new Proxy(self.EventSource, {
8032
+ construct(target, args) {
8033
+ return new target(encode(args[0]))
8034
+ },
8035
+ })
8036
+ }
8037
+ function workerUrl(value, options) {
8038
+ const text = String(value)
8039
+ if (skipped.test(text)) return text
8040
+ const target = URL.parse(text, baseUrl())
8041
+ if (target === null) return text
8042
+ const runtime = origin + prefix + 'runtime.js?url=' + encodeURIComponent(target.href) + '&origin=' + encodeURIComponent(origin)
8043
+ const script = absolute(target.href)
8044
+ const code = options && options.type === 'module'
8045
+ ? 'import ' + JSON.stringify(runtime) + ';import ' + JSON.stringify(script) + ';'
8046
+ : 'importScripts(' + JSON.stringify(runtime) + ');importScripts(' + JSON.stringify(script) + ');'
8047
+ return URL.createObjectURL(new Blob([code], { type: 'text/javascript' }))
8048
+ }
8049
+ for (const name of ['Worker', 'SharedWorker']) {
8050
+ if (self[name]) {
8051
+ self[name] = new Proxy(self[name], {
8052
+ construct(target, args) {
8053
+ args[0] = workerUrl(args[0], args[1])
8054
+ return Reflect.construct(target, args)
8055
+ },
8056
+ })
8057
+ }
8058
+ }
8059
+ if (!inWindow) {
8060
+ if (self.importScripts) {
8061
+ const realImport = self.importScripts.bind(self)
8062
+ self.importScripts = function () {
8063
+ realImport(...Array.from(arguments, function (item) { return absolute(item) }))
8064
+ }
8065
+ }
8066
+ return
8067
+ }
8068
+
8069
+ delete Navigator.prototype.serviceWorker
8070
+ Object.defineProperty(window, 'indexedDB', { value: undefined, configurable: true })
8071
+ Object.defineProperty(window, 'caches', {
8072
+ configurable: true,
8073
+ value: {
8074
+ keys() { return Promise.resolve([]) },
8075
+ has() { return Promise.resolve(false) },
8076
+ delete() { return Promise.resolve(false) },
8077
+ match() { return Promise.resolve(undefined) },
8078
+ open() { return Promise.reject(new DOMException('Cache storage is off', 'SecurityError')) },
8079
+ },
8080
+ })
8081
+ Object.defineProperty(Navigator.prototype, 'locks', {
8082
+ configurable: true,
8083
+ value: {
8084
+ request(name, options, callback) {
8085
+ const run = callback ?? options
8086
+ return Promise.resolve().then(function () { return run({ name, mode: 'exclusive' }) })
8087
+ },
8088
+ query() { return Promise.resolve({ held: [], pending: [] }) },
8089
+ },
8090
+ })
8091
+ const blankFrames = new Proxy(window, {
8092
+ get(target, name) {
8093
+ if (name === 'document') throw new DOMException('Helper frames are blocked', 'SecurityError')
8094
+ const value = Reflect.get(target, name)
8095
+ return typeof value === 'function' && value.prototype === undefined ? value.bind(target) : value
8096
+ },
8097
+ })
8098
+ getter(HTMLIFrameElement.prototype, 'contentWindow', function (value, frame) {
8099
+ const blank = native.get.call(frame, 'src') === null && native.get.call(frame, 'srcdoc') === null
8100
+ return blank && value !== null ? blankFrames : value
8101
+ })
8102
+ const realBeacon = navigator.sendBeacon.bind(navigator)
8103
+ navigator.sendBeacon = function (url, data) { return realBeacon(encode(url), data) }
8104
+
8105
+ const properties = [
8106
+ [HTMLAnchorElement, 'href'], [HTMLAreaElement, 'href'], [HTMLLinkElement, 'href'], [HTMLBaseElement, 'href'],
8107
+ [HTMLImageElement, 'src'], [HTMLScriptElement, 'src'], [HTMLIFrameElement, 'src'], [HTMLMediaElement, 'src'],
8108
+ [HTMLSourceElement, 'src'], [HTMLEmbedElement, 'src'], [HTMLInputElement, 'src'], [HTMLTrackElement, 'src'],
8109
+ [HTMLFormElement, 'action'], [HTMLButtonElement, 'formAction'], [HTMLInputElement, 'formAction'],
8110
+ [HTMLObjectElement, 'data'], [HTMLVideoElement, 'poster'],
8111
+ ]
8112
+ for (const item of properties) {
8113
+ patch(item[0].prototype, item[1], function (descriptor) {
8114
+ return {
8115
+ get() { return original(descriptor.get.call(this)) },
8116
+ set(value) { descriptor.set.call(this, encode(value)) },
8117
+ }
8118
+ })
8119
+ }
8120
+ const RealAudio = window.Audio
8121
+ window.Audio = function Audio(source) {
8122
+ return source === undefined ? new RealAudio() : new RealAudio(encode(source))
8123
+ }
8124
+ window.Audio.prototype = RealAudio.prototype
8125
+ for (const owner of [HTMLImageElement, HTMLSourceElement]) {
8126
+ patch(owner.prototype, 'srcset', function (descriptor) {
8127
+ return {
8128
+ get() { return descriptor.get.call(this) },
8129
+ set(value) { descriptor.set.call(this, encodeSet(value)) },
8130
+ }
8131
+ })
8132
+ }
8133
+ for (const owner of [HTMLScriptElement, HTMLLinkElement]) {
8134
+ patch(owner.prototype, 'integrity', function () {
8135
+ return { get() { return '' }, set() {} }
8136
+ })
8137
+ }
8138
+ for (const part of ['protocol', 'host', 'hostname', 'port', 'pathname', 'search', 'hash', 'origin']) {
8139
+ for (const owner of [HTMLAnchorElement, HTMLAreaElement]) {
8140
+ getter(owner.prototype, part, function (value, element) {
8141
+ const url = URL.parse(element.href)
8142
+ return url === null ? value : url[part]
8143
+ })
8144
+ }
8145
+ }
8146
+
8147
+ const linkTags = new Set(['A', 'AREA', 'AUDIO', 'BASE', 'BUTTON', 'EMBED', 'FORM', 'IFRAME', 'IMG', 'INPUT', 'LINK', 'OBJECT', 'SCRIPT', 'SOURCE', 'TRACK', 'VIDEO'])
8148
+ const linkNames = new Set(['href', 'src', 'action', 'formaction', 'poster', 'data'])
8149
+ Element.prototype.setAttribute = function (name, value) {
8150
+ const lower = String(name).toLowerCase()
8151
+ if (lower === 'integrity' || lower === 'crossorigin') return
8152
+ if (linkNames.has(lower) && linkTags.has(this.tagName)) {
8153
+ native.set.call(this, name, encode(value))
8154
+ return
8155
+ }
8156
+ if (lower === 'srcset') {
8157
+ native.set.call(this, name, encodeSet(value))
8158
+ return
8159
+ }
8160
+ native.set.call(this, name, value)
8161
+ }
8162
+ Element.prototype.getAttribute = function (name) {
8163
+ const value = native.get.call(this, name)
8164
+ if (value === null) return null
8165
+ const url = decode(value)
8166
+ if (url === null) return value
8167
+ const parsed = URL.parse(url)
8168
+ if (parsed === null || parsed.origin !== new URL(pageUrl()).origin) return url
8169
+ return parsed.pathname + parsed.search + parsed.hash
8170
+ }
8171
+ function encodeSelector(selector) {
8172
+ return String(selector).replace(/\[(src|href)(\^?=)(["']?)([^"'\]]+)\3\]/g, function (match, name, operator, quote, value) {
8173
+ if (!/^(\/|https?:\/\/)/.test(value)) return match
8174
+ return '[' + name + operator + '"' + encode(value) + '"]'
8175
+ })
8176
+ }
8177
+ for (const owner of [Document, DocumentFragment, Element]) {
8178
+ for (const name of ['querySelector', 'querySelectorAll']) {
8179
+ const realQuery = owner.prototype[name]
8180
+ owner.prototype[name] = function (selector) {
8181
+ return realQuery.call(this, encodeSelector(selector))
8182
+ }
8183
+ }
8184
+ }
8185
+ for (const name of ['matches', 'closest']) {
8186
+ const realMatch = Element.prototype[name]
8187
+ Element.prototype[name] = function (selector) {
8188
+ return realMatch.call(this, encodeSelector(selector))
8189
+ }
8190
+ }
8191
+ function encodeCss(text) {
8192
+ return String(text).replace(/url\(\s*(['"]?)(.*?)\1\s*\)/g, function (match, quote, url) {
8193
+ return 'url(' + quote + encode(url) + quote + ')'
8194
+ })
8195
+ }
8196
+ for (const name of ['insertRule', 'replace', 'replaceSync']) {
8197
+ const realRule = CSSStyleSheet.prototype[name]
8198
+ CSSStyleSheet.prototype[name] = function (text, index) {
8199
+ return realRule.call(this, encodeCss(text), index)
8200
+ }
8201
+ }
8202
+ function fixElement(element) {
8203
+ const style = native.get.call(element, 'style')
8204
+ if (style !== null) {
8205
+ const next = encodeCss(style)
8206
+ if (next !== style) native.set.call(element, 'style', next)
8207
+ }
8208
+ if (element.tagName === 'STYLE') {
8209
+ const text = element.textContent
8210
+ const next = encodeCss(text)
8211
+ if (next !== text) element.textContent = next
8212
+ return
8213
+ }
8214
+ if (!linkTags.has(element.tagName)) return
8215
+ for (const name of linkNames) {
8216
+ const value = native.get.call(element, name)
8217
+ if (value !== null) {
8218
+ const next = encode(value)
8219
+ if (next !== value) native.set.call(element, name, next)
8220
+ }
8221
+ }
8222
+ const set = native.get.call(element, 'srcset')
8223
+ if (set !== null && !set.includes(prefix)) native.set.call(element, 'srcset', encodeSet(set))
8224
+ }
8225
+ new MutationObserver(function (records) {
8226
+ for (const record of records) {
8227
+ if (record.type === 'attributes' || record.target.nodeName === 'STYLE') fixElement(record.target)
8228
+ for (const node of record.addedNodes) {
8229
+ if (node.nodeType !== 1) continue
8230
+ fixElement(node)
8231
+ for (const child of node.querySelectorAll('[src],[href],[action],[srcset],[poster],[data],[style],style')) fixElement(child)
8232
+ }
8233
+ }
8234
+ }).observe(document, { childList: true, subtree: true, attributes: true, attributeFilter: ['style'] })
8235
+
8236
+ getter(Document.prototype, 'URL', pageUrl)
8237
+ getter(Document.prototype, 'documentURI', pageUrl)
8238
+ getter(Node.prototype, 'baseURI', baseUrl)
8239
+ getter(Document.prototype, 'referrer', original)
8240
+ patch(Document.prototype, 'domain', function () {
8241
+ return { get() { return new URL(pageUrl()).hostname }, set() {} }
8242
+ })
8243
+ Object.defineProperty(window, 'origin', { get() { return new URL(pageUrl()).origin }, configurable: true })
8244
+ getter(MessageEvent.prototype, 'origin', function (value) {
8245
+ return value === 'null' ? new URL(pageUrl()).origin : value
8246
+ })
8247
+ window.postMessage = function (message, target, transfer) {
8248
+ if (typeof target === 'object' && target !== null) return realPost(message, Object.assign({}, target, { targetOrigin: '*' }))
8249
+ return realPost(message, '*', transfer)
8250
+ }
8251
+
8252
+ const cookies = new Map()
8253
+ for (const item of config.cookies.split('; ')) {
8254
+ const index = item.indexOf('=')
8255
+ if (index > 0) cookies.set(item.slice(0, index), item.slice(index + 1))
8256
+ }
8257
+ patch(Document.prototype, 'cookie', function () {
8258
+ return {
8259
+ get() { return Array.from(cookies, function (item) { return item[0] + '=' + item[1] }).join('; ') },
8260
+ set(value) {
8261
+ const text = String(value)
8262
+ const pair = text.split(';')[0]
8263
+ const index = pair.indexOf('=')
8264
+ const name = pair.slice(0, index).trim()
8265
+ const age = /;\s*max-age=(-?\d+)/i.exec(text)
8266
+ const expires = /;\s*expires=([^;]+)/i.exec(text)
8267
+ const gone = (age && Number(age[1]) <= 0) || (expires && Date.parse(expires[1]) <= Date.now())
8268
+ if (gone) cookies.delete(name)
8269
+ else cookies.set(name, pair.slice(index + 1).trim())
8270
+ realFetch(prefix + 'cookie', { method: 'POST', body: JSON.stringify({ url: pageUrl(), cookie: text }) })
8271
+ },
8272
+ }
8273
+ })
8274
+
8275
+ function storageArea(initial, save) {
8276
+ const data = new Map(Object.entries(initial))
8277
+ const area = {
8278
+ getItem(name) { return data.has(String(name)) ? data.get(String(name)) : null },
8279
+ setItem(name, value) { data.set(String(name), String(value)); save(data) },
8280
+ removeItem(name) { data.delete(String(name)); save(data) },
8281
+ clear() { data.clear(); save(data) },
8282
+ key(index) { return Array.from(data.keys())[index] ?? null },
8283
+ get length() { return data.size },
8284
+ }
8285
+ return new Proxy(area, {
8286
+ get(target, name) {
8287
+ if (name in target) return Reflect.get(target, name)
8288
+ return typeof name === 'string' ? data.get(name) : undefined
8289
+ },
8290
+ set(target, name, value) { target.setItem(name, value); return true },
8291
+ deleteProperty(target, name) { target.removeItem(name); return true },
8292
+ has(target, name) { return name in target || data.has(name) },
8293
+ ownKeys() { return Array.from(data.keys()) },
8294
+ getOwnPropertyDescriptor(target, name) {
8295
+ return data.has(name) ? { value: data.get(name), enumerable: true, configurable: true, writable: true } : undefined
8296
+ },
8297
+ })
8298
+ }
8299
+ let saveTimer = null
8300
+ let pending = null
8301
+ function sendStorage(keepalive) {
8302
+ if (pending === null) return
8303
+ const body = JSON.stringify({ origin: new URL(pageUrl()).origin, data: pending })
8304
+ pending = null
8305
+ realFetch(prefix + 'storage', { method: 'POST', body, keepalive: keepalive && body.length < 60000 })
8306
+ }
8307
+ const local = storageArea(config.storage, function (data) {
8308
+ pending = Object.fromEntries(data)
8309
+ clearTimeout(saveTimer)
8310
+ saveTimer = setTimeout(function () { sendStorage(false) }, 50)
8311
+ })
8312
+ const session = storageArea({}, function () {})
8313
+ Object.defineProperty(window, 'localStorage', { get() { return local }, configurable: true })
8314
+ Object.defineProperty(window, 'sessionStorage', { get() { return session }, configurable: true })
8315
+ addEventListener('pagehide', function () { sendStorage(true) })
8316
+
8317
+ function report(replaced) {
8318
+ postPanel({ event: 'location', url: pageUrl(), replaced: replaced === true })
8319
+ }
8320
+ for (const name of ['pushState', 'replaceState']) {
8321
+ const realHistory = History.prototype[name]
8322
+ History.prototype[name] = function (state, title, url) {
8323
+ const result = realHistory.call(this, state, title, url === undefined || url === null ? url : encode(url))
8324
+ report(name === 'replaceState')
8325
+ return result
8326
+ }
8327
+ }
8328
+ const realWindowOpen = window.open
8329
+ window.open = function (url, target, features) {
8330
+ if (url === undefined || url === null || url === '') return realWindowOpen.call(window, url, target, features)
8331
+ if (features) return realWindowOpen.call(window, encode(url), target, features)
8332
+ navigate(url)
8333
+ return window
8334
+ }
8335
+ if (panel === null) return
8336
+ report()
8337
+ addEventListener('popstate', report)
8338
+ addEventListener('hashchange', report)
8339
+ addEventListener('pageshow', report)
8340
+ addEventListener('pagehide', function () { postPanel({ event: 'loading' }) })
8341
+ addEventListener('click', function (event) {
8342
+ if (event.defaultPrevented || event.button !== 0 || event.ctrlKey || event.metaKey || event.shiftKey) return
8343
+ const anchor = event.target instanceof Element ? event.target.closest('a[href]') : null
8344
+ if (anchor === null || anchor.hasAttribute('download')) return
8345
+ const next = URL.parse(anchor.href)
8346
+ if (next === null || !/^https?:$/.test(next.protocol)) return
8347
+ if (next.host === new URL(pageUrl()).host) return
8348
+ event.preventDefault()
8349
+ navigate(next.href)
8350
+ })
8351
+ addEventListener('message', function (event) {
8352
+ if (event.source !== panel || !event.data || event.data.offsite !== 'browser') return
8353
+ event.stopImmediatePropagation()
8354
+ if (event.data.command === 'back') history.back()
8355
+ if (event.data.command === 'forward') history.forward()
8356
+ if (event.data.command === 'reload') realLocation.reload()
8357
+ })
8358
+ }`;
8359
+ // src/lib/proxy/proxy.ts
8360
+ const ROUTE = /^\/api\/proxy\/([^/]+)\/(.*)$/;
8361
+ const TARGET = /^(https?)\/([^/?#]+)(.*)$/;
8362
+ const SANDBOX = "sandbox allow-scripts allow-forms allow-popups allow-modals allow-downloads allow-presentation allow-pointer-lock";
8363
+ const UPSTREAM_TIMEOUT = 6e4;
8364
+ const MAX_BODY = 8388608;
8365
+ const MAX_HEADERS = 262144;
8366
+ const REQUEST_HEADERS_DROPPED = /* @__PURE__ */ new Set([
8367
+ "accept-encoding",
8368
+ "cdn-loop",
8369
+ "connection",
8370
+ "cookie",
8371
+ "forwarded",
8372
+ "host",
8373
+ "keep-alive",
8374
+ "origin",
8375
+ "proxy-authorization",
8376
+ "proxy-connection",
8377
+ "referer",
8378
+ "sec-fetch-site",
8379
+ "sec-fetch-user",
8380
+ "te",
8381
+ "trailer",
8382
+ "transfer-encoding",
8383
+ "true-client-ip",
8384
+ "upgrade",
8385
+ "via",
8386
+ "x-forwarded-for",
8387
+ "x-forwarded-host",
8388
+ "x-forwarded-port",
8389
+ "x-forwarded-proto",
8390
+ "x-real-ip"
8391
+ ]);
8392
+ const RESPONSE_HEADERS_DROPPED = /* @__PURE__ */ new Set([
8393
+ "alt-svc",
8394
+ "clear-site-data",
8395
+ "connection",
8396
+ "content-security-policy",
8397
+ "content-security-policy-report-only",
8398
+ "expect-ct",
8399
+ "feature-policy",
8400
+ "keep-alive",
8401
+ "link",
8402
+ "location",
8403
+ "nel",
8404
+ "origin-agent-cluster",
8405
+ "permissions-policy",
8406
+ "public-key-pins",
8407
+ "referrer-policy",
8408
+ "refresh",
8409
+ "report-to",
8410
+ "reporting-endpoints",
8411
+ "service-worker-allowed",
8412
+ "set-cookie",
8413
+ "strict-transport-security",
8414
+ "transfer-encoding",
8415
+ "x-frame-options"
8416
+ ]);
8417
+ const pageWrite = z.object({
8418
+ url: z.string(),
8419
+ cookie: z.string()
8420
+ });
8421
+ const storageWrite = z.object({
8422
+ origin: z.string(),
8423
+ data: z.record(z.string(), z.string())
8424
+ });
8425
+ function serveProxy(request, response) {
8426
+ answerProxy(request, response).catch((error) => {
8427
+ fail(response, 502, error instanceof Error ? error.message : String(error));
8428
+ });
8429
+ }
8430
+ async function answerProxy(request, response) {
8431
+ const route = ROUTE.exec(request.url ?? "");
8432
+ const site = route === null ? null : findSite(route[1] ?? "");
8433
+ response.setHeader("access-control-allow-origin", request.headers.origin ?? "*");
8434
+ response.setHeader("access-control-allow-credentials", "true");
8435
+ response.setHeader("access-control-expose-headers", "*");
8436
+ if (route === null || site === null) {
8437
+ fail(response, 404, "This page expired. Press reload to open it again.");
8438
+ return;
8439
+ }
8440
+ response.setHeader("content-security-policy", pagePolicy(site, request));
8441
+ response.setHeader("referrer-policy", "unsafe-url");
8442
+ if (request.method === "OPTIONS") {
8443
+ response.setHeader("access-control-allow-methods", request.headers["access-control-request-method"] ?? "*");
8444
+ response.setHeader("access-control-allow-headers", request.headers["access-control-request-headers"] ?? "*");
8445
+ response.statusCode = 204;
8446
+ response.end();
8447
+ return;
8448
+ }
8449
+ const rest = route[2] ?? "";
8450
+ if (rest.startsWith("runtime.js")) {
8451
+ serveRuntime(site, rest, response);
8452
+ return;
8453
+ }
8454
+ if (rest === "cookie" || rest === "storage") {
8455
+ await savePageData(site, rest, request, response);
8456
+ return;
8457
+ }
8458
+ const target = readTarget(rest);
8459
+ if (target === null) {
8460
+ fail(response, 404, "not found");
8461
+ return;
8462
+ }
8463
+ await forward(site, target, request, response);
8464
+ }
8465
+ function proxySocket(request, socket, head) {
8466
+ socket.on("error", () => socket.destroy());
8467
+ try {
8468
+ connectSocket(request, socket, head);
8469
+ } catch {
8470
+ socket.destroy();
8471
+ }
8472
+ }
8473
+ function connectSocket(request, socket, head) {
8474
+ const route = ROUTE.exec(request.url ?? "");
8475
+ const site = route === null ? null : findSite(route[1] ?? "");
8476
+ const target = route === null ? null : readTarget(route[2] ?? "");
8477
+ if (site === null || target === null) {
8478
+ socket.destroy();
8479
+ return;
8480
+ }
8481
+ const headers = upstreamHeaders(site, target, request);
8482
+ headers.connection = "Upgrade";
8483
+ headers.upgrade = request.headers.upgrade;
8484
+ const upstream = openUpstream(site, target, request, headers);
8485
+ upstream.on("upgrade", (answer, remote, remoteHead) => {
8486
+ const lines = [`HTTP/1.1 101 ${answer.statusMessage}`];
8487
+ for (let index = 0; index < answer.rawHeaders.length; index += 2) lines.push(`${answer.rawHeaders[index]}: ${answer.rawHeaders[index + 1]}`);
8488
+ socket.write(`${lines.join("\r\n")}\r\n\r\n`);
8489
+ if (remoteHead.length > 0) socket.write(remoteHead);
8490
+ if (head.length > 0) remote.write(head);
8491
+ remote.on("error", () => socket.destroy());
8492
+ socket.on("close", () => remote.destroy());
8493
+ remote.pipe(socket).pipe(remote);
8494
+ });
8495
+ upstream.on("response", () => socket.destroy());
8496
+ upstream.on("error", () => socket.destroy());
8497
+ upstream.end();
8498
+ }
8499
+ async function forward(site, target, request, response) {
8500
+ const headers = upstreamHeaders(site, target, request);
8501
+ headers["accept-encoding"] = "gzip, deflate, br";
8502
+ const upstream = openUpstream(site, target, request, headers);
8503
+ response.on("close", () => upstream.destroy());
8504
+ const answer = once(upstream, "response");
8505
+ upstream.on("error", () => void 0);
8506
+ request.pipe(upstream);
8507
+ const incoming = (await answer)[0];
8508
+ upstream.setTimeout(0);
8509
+ for (const cookie of incoming.headers["set-cookie"] ?? []) await site.jar.setCookie(cookie, target.href, { ignoreError: true });
8510
+ const kind = rewriteKind(incoming, request);
8511
+ response.statusCode = incoming.statusCode ?? 502;
8512
+ copyHeaders(incoming, response, kind !== null);
8513
+ const location = incoming.headers.location;
8514
+ if (location !== void 0) {
8515
+ const next = URL.parse(location, target);
8516
+ if (next === null) throw new Error(`${target.origin} sent a bad redirect`);
8517
+ response.setHeader("location", proxiedPath(site.key, next));
8518
+ }
8519
+ if (kind === null) {
8520
+ await pipeline(incoming, response);
8521
+ return;
8522
+ }
8523
+ const text = decodeBody(await readBody(incoming), incoming);
8524
+ response.setHeader("content-type", contentType(kind));
8525
+ response.end(await rewriteBody(site, target, kind, text));
8526
+ }
8527
+ function openUpstream(site, target, request, headers) {
8528
+ const secure = target.protocol === "https:";
8529
+ const port = Number(target.port) || (secure ? 443 : 80);
8530
+ const ownPort = request.socket.localPort ?? 0;
8531
+ const hostname = hostOf(target);
8532
+ if (target.host === request.headers.host) throw new Error("Pages cannot reach Offsite itself");
8533
+ if (isIP(hostname) !== 0) checkAddress(site, hostname, port, ownPort);
8534
+ const upstream = (secure ? https : http).request(target, {
8535
+ method: request.method,
8536
+ headers,
8537
+ lookup: guardedLookup(site, port, ownPort),
8538
+ maxHeaderSize: MAX_HEADERS,
8539
+ timeout: UPSTREAM_TIMEOUT,
8540
+ rejectUnauthorized: !site.local
8541
+ });
8542
+ upstream.on("timeout", () => upstream.destroy(/* @__PURE__ */ new Error(`${target.host} took too long to answer`)));
8543
+ return upstream;
8544
+ }
8545
+ function upstreamHeaders(site, target, request) {
8546
+ const headers = {};
8547
+ for (const name of Object.keys(request.headers)) if (!REQUEST_HEADERS_DROPPED.has(name) && !name.startsWith("cf-")) headers[name] = request.headers[name];
8548
+ headers.host = target.host;
8549
+ const cookie = site.jar.getCookieStringSync(target.href);
8550
+ if (cookie !== "") headers.cookie = cookie;
8551
+ const referer = refererOf(site, request);
8552
+ if (referer !== null) headers.referer = referer;
8553
+ else if (request.method !== "GET" && request.method !== "HEAD") headers.referer = `${target.origin}/`;
8554
+ if (request.headers.origin !== void 0) headers.origin = referer === null ? target.origin : new URL(referer).origin;
8555
+ return headers;
8556
+ }
8557
+ function refererOf(site, request) {
8558
+ const referer = request.headers.referer;
8559
+ const prefix = `/api/proxy/${site.key}/`;
8560
+ if (referer === void 0 || !referer.includes(prefix)) return null;
8561
+ return readTarget(referer.slice(referer.indexOf(prefix) + prefix.length))?.href ?? null;
8562
+ }
8563
+ function readTarget(rest) {
8564
+ const match = TARGET.exec(rest);
8565
+ if (match === null) return null;
8566
+ return URL.parse(`${match[1]}://${match[2]}${match[3]}`);
8567
+ }
8568
+ function pagePolicy(site, request) {
8569
+ const host = request.headers.host;
8570
+ if (host === void 0) throw new Error("the request has no host");
8571
+ const sources = [
8572
+ "http",
8573
+ "https",
8574
+ "ws",
8575
+ "wss"
8576
+ ].map((scheme) => `${scheme}://${host}/api/proxy/${site.key}/`).join(" ");
8577
+ return `${SANDBOX}; default-src ${sources} 'unsafe-inline' 'unsafe-eval' data: blob:; form-action ${sources}; base-uri ${sources}`;
8578
+ }
8579
+ function rewriteKind(incoming, request) {
8580
+ if (request.headers["sec-fetch-dest"] === "empty") return null;
8581
+ const type = incoming.headers["content-type"]?.split(";")[0]?.trim() ?? "";
8582
+ if (type === "text/html" || type === "application/xhtml+xml") return "html";
8583
+ if (type === "text/css") return "css";
8584
+ if (/\b(java|ecma)script\b/.test(type)) return "script";
8585
+ return null;
8586
+ }
8587
+ function contentType(kind) {
8588
+ if (kind === "html") return "text/html; charset=utf-8";
8589
+ if (kind === "css") return "text/css; charset=utf-8";
8590
+ return "text/javascript; charset=utf-8";
8591
+ }
8592
+ async function rewriteBody(site, target, kind, text) {
8593
+ const rewrite = linkRewriter(site.key, target);
8594
+ if (kind === "css") return rewriteCss(text, rewrite);
8595
+ if (kind === "script") return rewriteScript(text, rewrite, `${site.key} ${target.origin}`);
8596
+ const script = pageScript({
8597
+ key: site.key,
8598
+ url: target.href,
8599
+ cookies: site.jar.getCookieStringSync(target.href, { http: false }),
8600
+ storage: site.storage.get(target.origin) ?? {},
8601
+ origin: null
8602
+ });
8603
+ return rewriteHtml(text, site.key, target, script);
8604
+ }
8605
+ function copyHeaders(incoming, response, rewritten) {
8606
+ for (const name of Object.keys(incoming.headers)) {
8607
+ const value = incoming.headers[name];
8608
+ if (value === void 0 || RESPONSE_HEADERS_DROPPED.has(name) || name.startsWith("access-control-") || name.startsWith("cross-origin-")) continue;
8609
+ if (rewritten && (name === "content-length" || name === "content-encoding" || name === "content-md5" || name === "etag")) continue;
8610
+ response.setHeader(name, value);
8611
+ }
8612
+ if (rewritten) {
8613
+ const cache = incoming.headers["cache-control"];
8614
+ response.setHeader("cache-control", cache === void 0 ? "no-transform" : `${cache}, no-transform`);
8615
+ }
8616
+ }
8617
+ async function readBody(incoming) {
8618
+ const encoding = incoming.headers["content-encoding"];
8619
+ if (encoding === void 0 || encoding === "identity") return buffer(incoming);
8620
+ if (encoding === "gzip") return buffer(incoming.pipe(zlib.createGunzip()));
8621
+ if (encoding === "deflate") return buffer(incoming.pipe(zlib.createInflate()));
8622
+ if (encoding === "br") return buffer(incoming.pipe(zlib.createBrotliDecompress()));
8623
+ throw new Error(`the site sent unknown compression: ${encoding}`);
8624
+ }
8625
+ function decodeBody(body, incoming) {
8626
+ const declared = /charset=["']?([\w-]+)/i.exec(incoming.headers["content-type"] ?? "")?.[1] ?? /<meta[^>]+charset=["']?([\w-]+)/i.exec(body.subarray(0, 1024).toString("latin1"))?.[1] ?? "utf-8";
8627
+ try {
8628
+ return new TextDecoder(declared).decode(body);
8629
+ } catch {
8630
+ return new TextDecoder("utf-8").decode(body);
8631
+ }
8632
+ }
8633
+ function serveRuntime(site, rest, response) {
8634
+ const query = new URL(rest, "http://localhost").searchParams;
8635
+ const url = query.get("url");
8636
+ const origin = query.get("origin");
8637
+ if (url === null || origin === null) {
8638
+ fail(response, 400, "the runtime needs a url and an origin");
8639
+ return;
8640
+ }
8641
+ response.setHeader("content-type", "text/javascript; charset=utf-8");
8642
+ response.end(pageScript({
8643
+ key: site.key,
8644
+ url,
8645
+ cookies: "",
8646
+ storage: {},
8647
+ origin
8648
+ }));
8649
+ }
8650
+ async function savePageData(site, kind, request, response) {
8651
+ if (request.method !== "POST") {
8652
+ fail(response, 405, "use POST");
8653
+ return;
8654
+ }
8655
+ const body = await buffer(request);
8656
+ if (body.length > MAX_BODY) {
8657
+ fail(response, 413, "too much page data");
8658
+ return;
8659
+ }
8660
+ const data = JSON.parse(body.toString());
8661
+ if (kind === "cookie") {
8662
+ const write = pageWrite.parse(data);
8663
+ await site.jar.setCookie(write.cookie, write.url, {
8664
+ ignoreError: true,
8665
+ http: false
8666
+ });
8667
+ }
8668
+ if (kind === "storage") {
8669
+ const write = storageWrite.parse(data);
8670
+ site.storage.set(write.origin, write.data);
8671
+ }
8672
+ response.statusCode = 204;
8673
+ response.end();
8674
+ }
8675
+ function fail(response, status, message) {
8676
+ if (response.headersSent) {
8677
+ response.destroy();
8678
+ return;
8679
+ }
8680
+ response.statusCode = status;
8681
+ response.setHeader("content-type", "text/plain; charset=utf-8");
8682
+ response.end(message);
8683
+ }
7540
8684
  // src/storage/auth.ts
7541
8685
  const CODE_LIFETIME = 3e5;
7542
8686
  const DEVICE_LIFETIME = 2592e6;
@@ -7588,7 +8732,6 @@ function hash(secret) {
7588
8732
  // src/main.ts
7589
8733
  const UNPAIR_CHECK = 5e3;
7590
8734
  const TICKET_LIFETIME = 6e4;
7591
- const SNIFF_LENGTH = 8e3;
7592
8735
  const openBody = z.object({ path: z.string() });
7593
8736
  const contentTypes = {
7594
8737
  ".html": "text/html; charset=utf-8",
@@ -7660,6 +8803,10 @@ const server = createServer((request, response) => {
7660
8803
  serveBrowse(url.pathname, request, response);
7661
8804
  return;
7662
8805
  }
8806
+ if (url.pathname.startsWith("/api/proxy/")) {
8807
+ serveProxy(request, response);
8808
+ return;
8809
+ }
7663
8810
  if (url.pathname === `/api/open/${openSecret}`) {
7664
8811
  answerOpen(request, response);
7665
8812
  return;
@@ -7682,6 +8829,10 @@ const sockets = new WebSocketServer({
7682
8829
  const paired = /* @__PURE__ */ new Map();
7683
8830
  server.on("upgrade", (request, socket, head) => {
7684
8831
  const url = new URL(request.url ?? "/", "http://localhost");
8832
+ if (url.pathname.startsWith("/api/proxy/")) {
8833
+ proxySocket(request, socket, head);
8834
+ return;
8835
+ }
7685
8836
  const device = args.auth ? redeemTicket(url.searchParams.get("ticket")) : null;
7686
8837
  if (url.pathname !== "/ws" || args.auth && device === null) {
7687
8838
  socket.destroy();
@@ -7816,19 +8967,9 @@ function serveBrowse(pathname, request, response) {
7816
8967
  response.end(`${file} is not a file`);
7817
8968
  return;
7818
8969
  }
7819
- response.setHeader("content-type", browseType(file));
7820
- response.setHeader("cache-control", "no-cache");
8970
+ setBrowseHeaders(response, browseTypes.get(path.extname(file).toLowerCase()) ?? sniffType(file));
7821
8971
  streamFile(file, stats.size, request, response);
7822
8972
  }
7823
- function browseType(file) {
7824
- const known = browseTypes.get(path.extname(file).toLowerCase());
7825
- if (known !== void 0) return known;
7826
- const head = Buffer.alloc(SNIFF_LENGTH);
7827
- const handle = openSync(file, "r");
7828
- const read = readSync(handle, head, 0, SNIFF_LENGTH, 0);
7829
- closeSync(handle);
7830
- return head.subarray(0, read).includes(0) ? "application/octet-stream" : "text/plain; charset=utf-8";
7831
- }
7832
8973
  async function answerOpen(request, response) {
7833
8974
  if (request.method !== "POST") {
7834
8975
  response.statusCode = 405;