offsite 0.0.29 → 0.0.31

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.
@@ -1,16 +1,16 @@
1
1
  import { createHash, randomBytes, randomUUID } from "node:crypto";
2
- import { chmodSync, closeSync, createReadStream, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, watch, writeFileSync } from "node:fs";
3
- import { createServer } from "node:http";
2
+ import { chmodSync, closeSync, createReadStream, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, watch, writeFileSync } from "node:fs";
3
+ import http, { createServer } from "node:http";
4
4
  import path from "node:path";
5
5
  import { consola } from "consola";
6
6
  import { WebSocketServer } from "ws";
7
+ import { z } from "zod";
7
8
  import os from "node:os";
8
9
  import { deleteSession, forkSession, getSessionMessages, getSubagentMessages, listSessions, query, renameSession } from "@anthropic-ai/claude-agent-sdk";
9
10
  import { execFile, spawn } from "node:child_process";
10
11
  import readline from "node:readline";
11
- import { lstat, mkdir, open, readFile, readdir, readlink, stat, writeFile } from "node:fs/promises";
12
- import { z } from "zod";
13
12
  import { Command } from "@commander-js/extra-typings";
13
+ import { lstat, mkdir, open, readFile, readdir, readlink, stat, writeFile } from "node:fs/promises";
14
14
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
15
15
  import { OpenCode } from "@opencode/client";
16
16
  import { ensure, headers } from "@opencode/client/service";
@@ -23,7 +23,29 @@ import { migrate } from "drizzle-orm/node-sqlite/migrator";
23
23
  import { integer, snakeCase, text } from "drizzle-orm/sqlite-core";
24
24
  import { promisify } from "node:util";
25
25
  import webPush from "web-push";
26
+ import { createProxyServer } from "http-proxy-3";
26
27
  import { spawn as spawn$1 } from "@lydell/node-pty";
28
+ //#region ../core/src/media.ts
29
+ const mediaTypes = /* @__PURE__ */ new Map([
30
+ [".png", "image/png"],
31
+ [".jpg", "image/jpeg"],
32
+ [".jpeg", "image/jpeg"],
33
+ [".gif", "image/gif"],
34
+ [".webp", "image/webp"],
35
+ [".svg", "image/svg+xml"],
36
+ [".mp4", "video/mp4"],
37
+ [".m4v", "video/mp4"],
38
+ [".webm", "video/webm"],
39
+ [".mov", "video/quicktime"],
40
+ [".mp3", "audio/mpeg"],
41
+ [".wav", "audio/wav"],
42
+ [".ogg", "audio/ogg"],
43
+ [".opus", "audio/ogg"],
44
+ [".m4a", "audio/mp4"],
45
+ [".aac", "audio/aac"],
46
+ [".flac", "audio/flac"]
47
+ ]);
48
+ //#endregion
27
49
  //#region src/constants.ts
28
50
  const deprecatedModels = {
29
51
  claude: [
@@ -59,6 +81,140 @@ function broadcast(event) {
59
81
  for (const client of clients) client.send(event);
60
82
  }
61
83
  //#endregion
84
+ //#region src/args.ts
85
+ let command = "web";
86
+ let name = null;
87
+ const program = new Command("offsite").description("Starts the server and the web app").option("--host <host>", "Address to listen on", "127.0.0.1").option("--port <port>", "Port to listen on", "47841").option("--dir <dir>", "Folder for settings and accounts", path.join(os.homedir(), ".offsite")).option("--no-auth", "Accept connections without pairing").option("--no-pair", "Don't create a pairing code or print pairing links").option("--pwa", "Serve the web app as an installable app that works offline").configureHelp({ showGlobalOptions: true });
88
+ program.command("web", { isDefault: true }).description("Starts the server and the web app, same as offsite");
89
+ program.command("serve").description("Starts only the server, without the web app").action(() => {
90
+ command = "serve";
91
+ });
92
+ program.command("pair").description("Creates a pairing code for another browser and prints its link").option("--name <name>", "Name to recognize this browser by").action((options) => {
93
+ command = "pair";
94
+ name = options.name ?? null;
95
+ });
96
+ program.command("unpair").description("Picks paired browsers to unpair").action(() => {
97
+ command = "unpair";
98
+ });
99
+ program.parse();
100
+ const options = program.opts();
101
+ if (!options.auth && !options.pair) program.error("error: --no-pair can't be used with --no-auth");
102
+ const args = {
103
+ ...options,
104
+ dir: path.resolve(options.dir),
105
+ command,
106
+ name
107
+ };
108
+ const serverDir = path.join(import.meta.dirname, "..");
109
+ //#endregion
110
+ //#region src/storage/storage.ts
111
+ const home = args.dir;
112
+ const lockFile = path.join(home, "server.pid");
113
+ mkdirSync(home, { recursive: true });
114
+ function lockHome() {
115
+ lock();
116
+ process.on("exit", () => unlinkSync(lockFile));
117
+ }
118
+ function lock() {
119
+ try {
120
+ writeFileSync(lockFile, `${process.pid}\n`, { flag: "wx" });
121
+ return;
122
+ } catch (error) {
123
+ if (!hasCode(error, "EEXIST")) throw error;
124
+ }
125
+ let content;
126
+ let modified;
127
+ try {
128
+ content = readFileSync(lockFile, "utf8");
129
+ modified = statSync(lockFile).mtimeMs;
130
+ } catch (error) {
131
+ if (hasCode(error, "ENOENT")) return lock();
132
+ throw error;
133
+ }
134
+ const pid = Number(content);
135
+ const validPid = Number.isInteger(pid) && pid > 0;
136
+ const writtenThisBoot = modified > Date.now() - os.uptime() * 1e3;
137
+ if (validPid && writtenThisBoot && pid !== process.pid && isRunning(pid)) {
138
+ consola.error(`offsite is already running for ${home} (pid ${pid})`);
139
+ process.exit(1);
140
+ }
141
+ unlinkSync(lockFile);
142
+ lock();
143
+ }
144
+ function isRunning(pid) {
145
+ try {
146
+ process.kill(pid, 0);
147
+ return true;
148
+ } catch (error) {
149
+ if (hasCode(error, "ESRCH")) return false;
150
+ if (hasCode(error, "EPERM")) return true;
151
+ throw error;
152
+ }
153
+ }
154
+ function hasCode(error, code) {
155
+ return error instanceof Error && "code" in error && error.code === code;
156
+ }
157
+ function readJson(name) {
158
+ try {
159
+ return JSON.parse(readFileSync(path.join(home, name), "utf8"));
160
+ } catch (error) {
161
+ if (error.code === "ENOENT") return null;
162
+ throw error;
163
+ }
164
+ }
165
+ function writeJson(name, value) {
166
+ let file = path.join(home, name);
167
+ mkdirSync(home, { recursive: true });
168
+ try {
169
+ file = realpathSync(file);
170
+ } catch (error) {
171
+ if (!hasCode(error, "ENOENT")) throw error;
172
+ }
173
+ writeFileSync(`${file}.tmp`, `${JSON.stringify(value, null, 2)}\n`, { mode: 384 });
174
+ renameSync(`${file}.tmp`, file);
175
+ }
176
+ //#endregion
177
+ //#region src/services/open/open.ts
178
+ const openSecret = randomBytes(32).toString("base64url");
179
+ let env = {};
180
+ function openEnv() {
181
+ return env;
182
+ }
183
+ function installOpen(host, port) {
184
+ if (process.platform === "win32") return;
185
+ mkdirSync(path.join(home, "bin"), { recursive: true });
186
+ const folder = realpathSync(path.join(home, "bin"));
187
+ const script = path.join(folder, "open.mjs");
188
+ writeFileSync(script, readFileSync(path.join(serverDir, "src/services/open/open.mjs")));
189
+ for (const name of [
190
+ "open",
191
+ "xdg-open",
192
+ "wslview"
193
+ ]) {
194
+ const wrapper = path.join(folder, name);
195
+ writeFileSync(wrapper, `#!/bin/sh\nexec ${quote(process.execPath)} ${quote(script)} ${name} "$@"\n`);
196
+ chmodSync(wrapper, 493);
197
+ }
198
+ const startup = path.join(folder, "env.sh");
199
+ writeFileSync(startup, `export PATH=${quote(folder)}:"$PATH"\n`);
200
+ env = {
201
+ PATH: process.env.PATH === void 0 ? folder : `${folder}${path.delimiter}${process.env.PATH}`,
202
+ BROWSER: path.join(folder, process.platform === "darwin" ? "open" : "xdg-open"),
203
+ OFFSITE_OPEN_URL: `http://${loopback(host)}:${port}/api/open/${openSecret}`,
204
+ CLAUDE_ENV_FILE: startup,
205
+ BASH_ENV: startup
206
+ };
207
+ }
208
+ function loopback(host) {
209
+ if (host === "0.0.0.0") return "127.0.0.1";
210
+ if (host === "::") return "[::1]";
211
+ if (host.includes(":")) return `[${host}]`;
212
+ return host;
213
+ }
214
+ function quote(value) {
215
+ return `'${value.replaceAll("'", "'\\''")}'`;
216
+ }
217
+ //#endregion
62
218
  //#region src/lib/codex/app-server.ts
63
219
  const IDLE_LIMIT$3 = 18e5;
64
220
  const START_TIMEOUT = 15e3;
@@ -102,6 +258,7 @@ var AppServer = class {
102
258
  this.child = spawn(executable, ["app-server"], {
103
259
  env: {
104
260
  ...process.env,
261
+ ...openEnv(),
105
262
  CODEX_HOME: home
106
263
  },
107
264
  stdio: [
@@ -713,99 +870,6 @@ function record$1(value) {
713
870
  return isRecord(value) ? value : { value };
714
871
  }
715
872
  //#endregion
716
- //#region src/args.ts
717
- let command = "web";
718
- let name = null;
719
- const program = new Command("offsite").description("Starts the server and the web app").option("--host <host>", "Address to listen on", "127.0.0.1").option("--port <port>", "Port to listen on", "47841").option("--dir <dir>", "Folder for settings and accounts", path.join(os.homedir(), ".offsite")).option("--no-auth", "Accept connections without pairing").option("--no-pair", "Don't create a pairing code or print pairing links").option("--pwa", "Serve the web app as an installable app that works offline").configureHelp({ showGlobalOptions: true });
720
- program.command("web", { isDefault: true }).description("Starts the server and the web app, same as offsite");
721
- program.command("serve").description("Starts only the server, without the web app").action(() => {
722
- command = "serve";
723
- });
724
- program.command("pair").description("Creates a pairing code for another browser and prints its link").option("--name <name>", "Name to recognize this browser by").action((options) => {
725
- command = "pair";
726
- name = options.name ?? null;
727
- });
728
- program.command("unpair").description("Picks paired browsers to unpair").action(() => {
729
- command = "unpair";
730
- });
731
- program.parse();
732
- const options = program.opts();
733
- if (!options.auth && !options.pair) program.error("error: --no-pair can't be used with --no-auth");
734
- const args = {
735
- ...options,
736
- dir: path.resolve(options.dir),
737
- command,
738
- name
739
- };
740
- const serverDir = path.join(import.meta.dirname, "..");
741
- //#endregion
742
- //#region src/storage/storage.ts
743
- const home = args.dir;
744
- const lockFile = path.join(home, "server.pid");
745
- mkdirSync(home, { recursive: true });
746
- function lockHome() {
747
- lock();
748
- process.on("exit", () => unlinkSync(lockFile));
749
- }
750
- function lock() {
751
- try {
752
- writeFileSync(lockFile, `${process.pid}\n`, { flag: "wx" });
753
- return;
754
- } catch (error) {
755
- if (!hasCode(error, "EEXIST")) throw error;
756
- }
757
- let content;
758
- let modified;
759
- try {
760
- content = readFileSync(lockFile, "utf8");
761
- modified = statSync(lockFile).mtimeMs;
762
- } catch (error) {
763
- if (hasCode(error, "ENOENT")) return lock();
764
- throw error;
765
- }
766
- const pid = Number(content);
767
- const validPid = Number.isInteger(pid) && pid > 0;
768
- const writtenThisBoot = modified > Date.now() - os.uptime() * 1e3;
769
- if (validPid && writtenThisBoot && pid !== process.pid && isRunning(pid)) {
770
- consola.error(`offsite is already running for ${home} (pid ${pid})`);
771
- process.exit(1);
772
- }
773
- unlinkSync(lockFile);
774
- lock();
775
- }
776
- function isRunning(pid) {
777
- try {
778
- process.kill(pid, 0);
779
- return true;
780
- } catch (error) {
781
- if (hasCode(error, "ESRCH")) return false;
782
- if (hasCode(error, "EPERM")) return true;
783
- throw error;
784
- }
785
- }
786
- function hasCode(error, code) {
787
- return error instanceof Error && "code" in error && error.code === code;
788
- }
789
- function readJson(name) {
790
- try {
791
- return JSON.parse(readFileSync(path.join(home, name), "utf8"));
792
- } catch (error) {
793
- if (error.code === "ENOENT") return null;
794
- throw error;
795
- }
796
- }
797
- function writeJson(name, value) {
798
- let file = path.join(home, name);
799
- mkdirSync(home, { recursive: true });
800
- try {
801
- file = realpathSync(file);
802
- } catch (error) {
803
- if (!hasCode(error, "ENOENT")) throw error;
804
- }
805
- writeFileSync(`${file}.tmp`, `${JSON.stringify(value, null, 2)}\n`, { mode: 384 });
806
- renameSync(`${file}.tmp`, file);
807
- }
808
- //#endregion
809
873
  //#region src/storage/config.ts
810
874
  const configFile = path.join(home, "config.json");
811
875
  const modelSetting = z.strictObject({
@@ -3734,6 +3798,7 @@ function claudeOptions(accountId, cwd) {
3734
3798
  const configDir = storedAccount("claude", accountId).configDir ?? claudeBaseDir();
3735
3799
  const env = {
3736
3800
  ...process.env,
3801
+ ...openEnv(),
3737
3802
  CLAUDE_CODE_ENTRYPOINT: "offsite",
3738
3803
  CLAUDE_CONFIG_DIR: configDir
3739
3804
  };
@@ -4890,32 +4955,12 @@ var Run = class {
4890
4955
  }
4891
4956
  };
4892
4957
  //#endregion
4893
- //#region ../core/src/media.ts
4894
- const mediaTypes = /* @__PURE__ */ new Map([
4895
- [".png", "image/png"],
4896
- [".jpg", "image/jpeg"],
4897
- [".jpeg", "image/jpeg"],
4898
- [".gif", "image/gif"],
4899
- [".webp", "image/webp"],
4900
- [".svg", "image/svg+xml"],
4901
- [".mp4", "video/mp4"],
4902
- [".m4v", "video/mp4"],
4903
- [".webm", "video/webm"],
4904
- [".mov", "video/quicktime"],
4905
- [".mp3", "audio/mpeg"],
4906
- [".wav", "audio/wav"],
4907
- [".ogg", "audio/ogg"],
4908
- [".opus", "audio/ogg"],
4909
- [".m4a", "audio/mp4"],
4910
- [".aac", "audio/aac"],
4911
- [".flac", "audio/flac"]
4912
- ]);
4913
- //#endregion
4914
4958
  //#region src/services/files.ts
4915
4959
  const FILE_LIMIT = 2e4;
4916
4960
  const OUTPUT_LIMIT$1 = 262144;
4917
4961
  const READ_LIMIT = 1048576;
4918
4962
  const MEDIA_LINK_LIFETIME = 36e5;
4963
+ const browseToken = randomBytes(32).toString("base64url");
4919
4964
  const mediaLinks = /* @__PURE__ */ new Map();
4920
4965
  const skipped = /* @__PURE__ */ new Set([
4921
4966
  "node_modules",
@@ -5046,6 +5091,62 @@ function resolveProjectFile(cwd, file) {
5046
5091
  return full;
5047
5092
  }
5048
5093
  //#endregion
5094
+ //#region src/services/proxy.ts
5095
+ const proxies = /* @__PURE__ */ new Map();
5096
+ function proxyPort(origin) {
5097
+ const target = new URL(origin);
5098
+ if (target.protocol !== "http:" && target.protocol !== "https:") throw new Error(`${origin} is not an http or https address`);
5099
+ const existing = proxies.get(target.origin);
5100
+ if (existing !== void 0) return existing;
5101
+ const started = startProxy(target);
5102
+ proxies.set(target.origin, started);
5103
+ started.catch(() => proxies.delete(target.origin));
5104
+ return started;
5105
+ }
5106
+ function startProxy(target) {
5107
+ const proxy = createProxyServer({
5108
+ target: target.origin,
5109
+ changeOrigin: true,
5110
+ ws: true,
5111
+ secure: false,
5112
+ autoRewrite: true,
5113
+ protocolRewrite: "http",
5114
+ cookieDomainRewrite: ""
5115
+ });
5116
+ proxy.on("proxyReq", (outgoing, request) => rewriteOrigin(target, outgoing, request));
5117
+ proxy.on("proxyReqWs", (outgoing, request) => rewriteOrigin(target, outgoing, request));
5118
+ proxy.on("proxyRes", (incoming) => {
5119
+ delete incoming.headers["x-frame-options"];
5120
+ delete incoming.headers["content-security-policy"];
5121
+ 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, ""));
5122
+ });
5123
+ proxy.on("error", (error, _request, response) => {
5124
+ if (!(response instanceof http.ServerResponse)) {
5125
+ response.destroy();
5126
+ return;
5127
+ }
5128
+ if (!response.headersSent) response.writeHead(502, { "content-type": "text/plain; charset=utf-8" });
5129
+ response.end(`Could not reach ${target.origin}: ${error.message}`);
5130
+ });
5131
+ const server = http.createServer((request, response) => proxy.web(request, response));
5132
+ server.on("upgrade", (request, socket, head) => proxy.ws(request, socket, head));
5133
+ return new Promise((resolve, reject) => {
5134
+ server.once("error", reject);
5135
+ server.listen(0, args.host, () => {
5136
+ const address = server.address();
5137
+ if (address === null || typeof address === "string") {
5138
+ reject(/* @__PURE__ */ new Error(`the proxy for ${target.origin} has no port`));
5139
+ return;
5140
+ }
5141
+ resolve(address.port);
5142
+ });
5143
+ });
5144
+ }
5145
+ function rewriteOrigin(target, outgoing, request) {
5146
+ if (request.headers.origin !== void 0) outgoing.setHeader("origin", target.origin);
5147
+ if (request.headers.referer !== void 0) outgoing.setHeader("referer", request.headers.referer.replace(`http://${request.headers.host}`, target.origin));
5148
+ }
5149
+ //#endregion
5049
5150
  //#region src/services/runs.ts
5050
5151
  const locks = /* @__PURE__ */ new Map();
5051
5152
  function stopAll() {
@@ -6262,6 +6363,8 @@ const handlers = {
6262
6363
  "files.media": (params) => linkProjectMedia(projectPath(params.projectId), params.path),
6263
6364
  "files.download": (params) => linkProjectDownload(projectPath(params.projectId), params.path),
6264
6365
  "files.upload": (params) => saveUpload(params.name, Buffer.from(params.data, "base64")),
6366
+ "files.browse": () => browseToken,
6367
+ "browser.proxy": (params) => proxyPort(params.origin),
6265
6368
  "tasks.output": (params) => taskOutput(params.sessionId, params.path),
6266
6369
  "git.status": (params) => gitRepos(projectPath(params.projectId)),
6267
6370
  "git.diff": (params) => projectDiff(projectPath(params.projectId)),
@@ -6408,6 +6511,8 @@ function hash(secret) {
6408
6511
  //#region src/main.ts
6409
6512
  const UNPAIR_CHECK = 5e3;
6410
6513
  const TICKET_LIFETIME = 6e4;
6514
+ const SNIFF_LENGTH = 8e3;
6515
+ const openBody = z.object({ path: z.string() });
6411
6516
  const contentTypes = {
6412
6517
  ".html": "text/html; charset=utf-8",
6413
6518
  ".js": "text/javascript; charset=utf-8",
@@ -6419,6 +6524,19 @@ const contentTypes = {
6419
6524
  ".webmanifest": "application/manifest+json",
6420
6525
  ".woff2": "font/woff2"
6421
6526
  };
6527
+ const browseTypes = new Map([
6528
+ ...Object.entries(contentTypes),
6529
+ ...mediaTypes,
6530
+ [".htm", "text/html; charset=utf-8"],
6531
+ [".mjs", "text/javascript; charset=utf-8"],
6532
+ [".json", "application/json; charset=utf-8"],
6533
+ [".pdf", "application/pdf"],
6534
+ [".xml", "application/xml"],
6535
+ [".wasm", "application/wasm"],
6536
+ [".woff", "font/woff"],
6537
+ [".ttf", "font/ttf"],
6538
+ [".otf", "font/otf"]
6539
+ ]);
6422
6540
  const port = Number(args.port);
6423
6541
  if (!Number.isInteger(port) || port < 1 || port > 65535) {
6424
6542
  console.error(`invalid port ${args.port}`);
@@ -6437,6 +6555,7 @@ if (args.command === "unpair") {
6437
6555
  }
6438
6556
  lockHome();
6439
6557
  process.env.CLAUDE_CONFIG_DIR = claudeBaseDir();
6558
+ installOpen(args.host, port);
6440
6559
  watchSessions();
6441
6560
  refreshAll();
6442
6561
  if (findClaude() === null) {
@@ -6452,6 +6571,14 @@ const server = createServer((request, response) => {
6452
6571
  serveMedia(url.searchParams.get("token"), request, response);
6453
6572
  return;
6454
6573
  }
6574
+ if (url.pathname.startsWith("/api/browse/")) {
6575
+ serveBrowse(url.pathname, request, response);
6576
+ return;
6577
+ }
6578
+ if (url.pathname === `/api/open/${openSecret}`) {
6579
+ answerOpen(request, response);
6580
+ return;
6581
+ }
6455
6582
  if (url.pathname.startsWith("/api/")) {
6456
6583
  answerApi(url.pathname, request, response);
6457
6584
  return;
@@ -6566,28 +6693,100 @@ function serveMedia(token, request, response) {
6566
6693
  response.end("not found");
6567
6694
  return;
6568
6695
  }
6696
+ response.setHeader("access-control-allow-origin", "*");
6569
6697
  response.setHeader("content-type", link.mediaType);
6570
6698
  if (link.download !== null) response.setHeader("content-disposition", `attachment; filename*=UTF-8''${encodeURIComponent(link.download)}`);
6571
- response.setHeader("accept-ranges", "bytes");
6572
6699
  response.setHeader("cache-control", "private, max-age=3600");
6573
6700
  response.setHeader("content-security-policy", "sandbox");
6574
6701
  response.setHeader("x-content-type-options", "nosniff");
6702
+ streamFile(link.file, stats.size, request, response);
6703
+ }
6704
+ function serveBrowse(pathname, request, response) {
6705
+ if (request.method !== "GET") {
6706
+ response.statusCode = 405;
6707
+ response.end("use GET");
6708
+ return;
6709
+ }
6710
+ const rest = pathname.slice(12);
6711
+ const slash = rest.indexOf("/");
6712
+ if (slash === -1 || rest.slice(0, slash) !== browseToken) {
6713
+ response.statusCode = 404;
6714
+ response.end("This browser link expired");
6715
+ return;
6716
+ }
6717
+ let file;
6718
+ try {
6719
+ file = decodeURIComponent(rest.slice(process.platform === "win32" ? slash + 1 : slash));
6720
+ } catch {
6721
+ response.statusCode = 400;
6722
+ response.end("bad path");
6723
+ return;
6724
+ }
6725
+ const stats = statSync(file, { throwIfNoEntry: false });
6726
+ if (stats === void 0 || !stats.isFile()) {
6727
+ response.statusCode = 404;
6728
+ response.end(`${file} is not a file`);
6729
+ return;
6730
+ }
6731
+ response.setHeader("content-type", browseType(file));
6732
+ response.setHeader("cache-control", "no-cache");
6733
+ streamFile(file, stats.size, request, response);
6734
+ }
6735
+ function browseType(file) {
6736
+ const known = browseTypes.get(path.extname(file).toLowerCase());
6737
+ if (known !== void 0) return known;
6738
+ const head = Buffer.alloc(SNIFF_LENGTH);
6739
+ const handle = openSync(file, "r");
6740
+ const read = readSync(handle, head, 0, SNIFF_LENGTH, 0);
6741
+ closeSync(handle);
6742
+ return head.subarray(0, read).includes(0) ? "application/octet-stream" : "text/plain; charset=utf-8";
6743
+ }
6744
+ async function answerOpen(request, response) {
6745
+ if (request.method !== "POST") {
6746
+ response.statusCode = 405;
6747
+ response.end("use POST");
6748
+ return;
6749
+ }
6750
+ let body = "";
6751
+ for await (const chunk of request) body += chunk;
6752
+ let parsed;
6753
+ try {
6754
+ parsed = openBody.parse(JSON.parse(body));
6755
+ } catch {
6756
+ response.statusCode = 400;
6757
+ response.end("expected { path }");
6758
+ return;
6759
+ }
6760
+ if (clients.size === 0) {
6761
+ response.statusCode = 409;
6762
+ response.end("no browser is connected to OffSite");
6763
+ return;
6764
+ }
6765
+ broadcast({
6766
+ type: "open",
6767
+ path: parsed.path
6768
+ });
6769
+ response.statusCode = 204;
6770
+ response.end();
6771
+ }
6772
+ function streamFile(file, size, request, response) {
6773
+ response.setHeader("accept-ranges", "bytes");
6575
6774
  if (request.headers.range === void 0) {
6576
- response.setHeader("content-length", stats.size);
6577
- createReadStream(link.file).on("error", () => response.destroy()).pipe(response);
6775
+ response.setHeader("content-length", size);
6776
+ createReadStream(file).on("error", () => response.destroy()).pipe(response);
6578
6777
  return;
6579
6778
  }
6580
- const range = byteRange(request.headers.range, stats.size);
6779
+ const range = byteRange(request.headers.range, size);
6581
6780
  if (range === null) {
6582
6781
  response.statusCode = 416;
6583
- response.setHeader("content-range", `bytes */${stats.size}`);
6782
+ response.setHeader("content-range", `bytes */${size}`);
6584
6783
  response.end();
6585
6784
  return;
6586
6785
  }
6587
6786
  response.statusCode = 206;
6588
- response.setHeader("content-range", `bytes ${range.start}-${range.end}/${stats.size}`);
6787
+ response.setHeader("content-range", `bytes ${range.start}-${range.end}/${size}`);
6589
6788
  response.setHeader("content-length", range.end - range.start + 1);
6590
- createReadStream(link.file, range).on("error", () => response.destroy()).pipe(response);
6789
+ createReadStream(file, range).on("error", () => response.destroy()).pipe(response);
6591
6790
  }
6592
6791
  function byteRange(header, size) {
6593
6792
  const match = /^bytes=(?:(\d+)-(\d*)|-(\d+))$/.exec(header);