offsite 0.0.30 → 0.0.32

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({
@@ -3731,9 +3795,11 @@ function claudeMode(value) {
3731
3795
  return mode;
3732
3796
  }
3733
3797
  function claudeOptions(accountId, cwd) {
3798
+ if (!statSync(cwd, { throwIfNoEntry: false })?.isDirectory()) throw new Error(`Project folder ${cwd} does not exist`);
3734
3799
  const configDir = storedAccount("claude", accountId).configDir ?? claudeBaseDir();
3735
3800
  const env = {
3736
3801
  ...process.env,
3802
+ ...openEnv(),
3737
3803
  CLAUDE_CODE_ENTRYPOINT: "offsite",
3738
3804
  CLAUDE_CONFIG_DIR: configDir
3739
3805
  };
@@ -4890,32 +4956,12 @@ var Run = class {
4890
4956
  }
4891
4957
  };
4892
4958
  //#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
4959
  //#region src/services/files.ts
4915
4960
  const FILE_LIMIT = 2e4;
4916
4961
  const OUTPUT_LIMIT$1 = 262144;
4917
4962
  const READ_LIMIT = 1048576;
4918
4963
  const MEDIA_LINK_LIFETIME = 36e5;
4964
+ const browseToken = randomBytes(32).toString("base64url");
4919
4965
  const mediaLinks = /* @__PURE__ */ new Map();
4920
4966
  const skipped = /* @__PURE__ */ new Set([
4921
4967
  "node_modules",
@@ -5046,6 +5092,62 @@ function resolveProjectFile(cwd, file) {
5046
5092
  return full;
5047
5093
  }
5048
5094
  //#endregion
5095
+ //#region src/services/proxy.ts
5096
+ const proxies = /* @__PURE__ */ new Map();
5097
+ function proxyPort(origin) {
5098
+ const target = new URL(origin);
5099
+ if (target.protocol !== "http:" && target.protocol !== "https:") throw new Error(`${origin} is not an http or https address`);
5100
+ const existing = proxies.get(target.origin);
5101
+ if (existing !== void 0) return existing;
5102
+ const started = startProxy(target);
5103
+ proxies.set(target.origin, started);
5104
+ started.catch(() => proxies.delete(target.origin));
5105
+ return started;
5106
+ }
5107
+ function startProxy(target) {
5108
+ const proxy = createProxyServer({
5109
+ target: target.origin,
5110
+ changeOrigin: true,
5111
+ ws: true,
5112
+ secure: false,
5113
+ autoRewrite: true,
5114
+ protocolRewrite: "http",
5115
+ cookieDomainRewrite: ""
5116
+ });
5117
+ proxy.on("proxyReq", (outgoing, request) => rewriteOrigin(target, outgoing, request));
5118
+ proxy.on("proxyReqWs", (outgoing, request) => rewriteOrigin(target, outgoing, request));
5119
+ proxy.on("proxyRes", (incoming) => {
5120
+ delete incoming.headers["x-frame-options"];
5121
+ delete incoming.headers["content-security-policy"];
5122
+ 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, ""));
5123
+ });
5124
+ proxy.on("error", (error, _request, response) => {
5125
+ if (!(response instanceof http.ServerResponse)) {
5126
+ response.destroy();
5127
+ return;
5128
+ }
5129
+ if (!response.headersSent) response.writeHead(502, { "content-type": "text/plain; charset=utf-8" });
5130
+ response.end(`Could not reach ${target.origin}: ${error.message}`);
5131
+ });
5132
+ const server = http.createServer((request, response) => proxy.web(request, response));
5133
+ server.on("upgrade", (request, socket, head) => proxy.ws(request, socket, head));
5134
+ return new Promise((resolve, reject) => {
5135
+ server.once("error", reject);
5136
+ server.listen(0, args.host, () => {
5137
+ const address = server.address();
5138
+ if (address === null || typeof address === "string") {
5139
+ reject(/* @__PURE__ */ new Error(`the proxy for ${target.origin} has no port`));
5140
+ return;
5141
+ }
5142
+ resolve(address.port);
5143
+ });
5144
+ });
5145
+ }
5146
+ function rewriteOrigin(target, outgoing, request) {
5147
+ if (request.headers.origin !== void 0) outgoing.setHeader("origin", target.origin);
5148
+ if (request.headers.referer !== void 0) outgoing.setHeader("referer", request.headers.referer.replace(`http://${request.headers.host}`, target.origin));
5149
+ }
5150
+ //#endregion
5049
5151
  //#region src/services/runs.ts
5050
5152
  const locks = /* @__PURE__ */ new Map();
5051
5153
  function stopAll() {
@@ -6262,6 +6364,8 @@ const handlers = {
6262
6364
  "files.media": (params) => linkProjectMedia(projectPath(params.projectId), params.path),
6263
6365
  "files.download": (params) => linkProjectDownload(projectPath(params.projectId), params.path),
6264
6366
  "files.upload": (params) => saveUpload(params.name, Buffer.from(params.data, "base64")),
6367
+ "files.browse": () => browseToken,
6368
+ "browser.proxy": (params) => proxyPort(params.origin),
6265
6369
  "tasks.output": (params) => taskOutput(params.sessionId, params.path),
6266
6370
  "git.status": (params) => gitRepos(projectPath(params.projectId)),
6267
6371
  "git.diff": (params) => projectDiff(projectPath(params.projectId)),
@@ -6408,6 +6512,8 @@ function hash(secret) {
6408
6512
  //#region src/main.ts
6409
6513
  const UNPAIR_CHECK = 5e3;
6410
6514
  const TICKET_LIFETIME = 6e4;
6515
+ const SNIFF_LENGTH = 8e3;
6516
+ const openBody = z.object({ path: z.string() });
6411
6517
  const contentTypes = {
6412
6518
  ".html": "text/html; charset=utf-8",
6413
6519
  ".js": "text/javascript; charset=utf-8",
@@ -6419,6 +6525,19 @@ const contentTypes = {
6419
6525
  ".webmanifest": "application/manifest+json",
6420
6526
  ".woff2": "font/woff2"
6421
6527
  };
6528
+ const browseTypes = new Map([
6529
+ ...Object.entries(contentTypes),
6530
+ ...mediaTypes,
6531
+ [".htm", "text/html; charset=utf-8"],
6532
+ [".mjs", "text/javascript; charset=utf-8"],
6533
+ [".json", "application/json; charset=utf-8"],
6534
+ [".pdf", "application/pdf"],
6535
+ [".xml", "application/xml"],
6536
+ [".wasm", "application/wasm"],
6537
+ [".woff", "font/woff"],
6538
+ [".ttf", "font/ttf"],
6539
+ [".otf", "font/otf"]
6540
+ ]);
6422
6541
  const port = Number(args.port);
6423
6542
  if (!Number.isInteger(port) || port < 1 || port > 65535) {
6424
6543
  console.error(`invalid port ${args.port}`);
@@ -6437,6 +6556,7 @@ if (args.command === "unpair") {
6437
6556
  }
6438
6557
  lockHome();
6439
6558
  process.env.CLAUDE_CONFIG_DIR = claudeBaseDir();
6559
+ installOpen(args.host, port);
6440
6560
  watchSessions();
6441
6561
  refreshAll();
6442
6562
  if (findClaude() === null) {
@@ -6452,6 +6572,14 @@ const server = createServer((request, response) => {
6452
6572
  serveMedia(url.searchParams.get("token"), request, response);
6453
6573
  return;
6454
6574
  }
6575
+ if (url.pathname.startsWith("/api/browse/")) {
6576
+ serveBrowse(url.pathname, request, response);
6577
+ return;
6578
+ }
6579
+ if (url.pathname === `/api/open/${openSecret}`) {
6580
+ answerOpen(request, response);
6581
+ return;
6582
+ }
6455
6583
  if (url.pathname.startsWith("/api/")) {
6456
6584
  answerApi(url.pathname, request, response);
6457
6585
  return;
@@ -6566,28 +6694,100 @@ function serveMedia(token, request, response) {
6566
6694
  response.end("not found");
6567
6695
  return;
6568
6696
  }
6697
+ response.setHeader("access-control-allow-origin", "*");
6569
6698
  response.setHeader("content-type", link.mediaType);
6570
6699
  if (link.download !== null) response.setHeader("content-disposition", `attachment; filename*=UTF-8''${encodeURIComponent(link.download)}`);
6571
- response.setHeader("accept-ranges", "bytes");
6572
6700
  response.setHeader("cache-control", "private, max-age=3600");
6573
6701
  response.setHeader("content-security-policy", "sandbox");
6574
6702
  response.setHeader("x-content-type-options", "nosniff");
6703
+ streamFile(link.file, stats.size, request, response);
6704
+ }
6705
+ function serveBrowse(pathname, request, response) {
6706
+ if (request.method !== "GET") {
6707
+ response.statusCode = 405;
6708
+ response.end("use GET");
6709
+ return;
6710
+ }
6711
+ const rest = pathname.slice(12);
6712
+ const slash = rest.indexOf("/");
6713
+ if (slash === -1 || rest.slice(0, slash) !== browseToken) {
6714
+ response.statusCode = 404;
6715
+ response.end("This browser link expired");
6716
+ return;
6717
+ }
6718
+ let file;
6719
+ try {
6720
+ file = decodeURIComponent(rest.slice(process.platform === "win32" ? slash + 1 : slash));
6721
+ } catch {
6722
+ response.statusCode = 400;
6723
+ response.end("bad path");
6724
+ return;
6725
+ }
6726
+ const stats = statSync(file, { throwIfNoEntry: false });
6727
+ if (stats === void 0 || !stats.isFile()) {
6728
+ response.statusCode = 404;
6729
+ response.end(`${file} is not a file`);
6730
+ return;
6731
+ }
6732
+ response.setHeader("content-type", browseType(file));
6733
+ response.setHeader("cache-control", "no-cache");
6734
+ streamFile(file, stats.size, request, response);
6735
+ }
6736
+ function browseType(file) {
6737
+ const known = browseTypes.get(path.extname(file).toLowerCase());
6738
+ if (known !== void 0) return known;
6739
+ const head = Buffer.alloc(SNIFF_LENGTH);
6740
+ const handle = openSync(file, "r");
6741
+ const read = readSync(handle, head, 0, SNIFF_LENGTH, 0);
6742
+ closeSync(handle);
6743
+ return head.subarray(0, read).includes(0) ? "application/octet-stream" : "text/plain; charset=utf-8";
6744
+ }
6745
+ async function answerOpen(request, response) {
6746
+ if (request.method !== "POST") {
6747
+ response.statusCode = 405;
6748
+ response.end("use POST");
6749
+ return;
6750
+ }
6751
+ let body = "";
6752
+ for await (const chunk of request) body += chunk;
6753
+ let parsed;
6754
+ try {
6755
+ parsed = openBody.parse(JSON.parse(body));
6756
+ } catch {
6757
+ response.statusCode = 400;
6758
+ response.end("expected { path }");
6759
+ return;
6760
+ }
6761
+ if (clients.size === 0) {
6762
+ response.statusCode = 409;
6763
+ response.end("no browser is connected to OffSite");
6764
+ return;
6765
+ }
6766
+ broadcast({
6767
+ type: "open",
6768
+ path: parsed.path
6769
+ });
6770
+ response.statusCode = 204;
6771
+ response.end();
6772
+ }
6773
+ function streamFile(file, size, request, response) {
6774
+ response.setHeader("accept-ranges", "bytes");
6575
6775
  if (request.headers.range === void 0) {
6576
- response.setHeader("content-length", stats.size);
6577
- createReadStream(link.file).on("error", () => response.destroy()).pipe(response);
6776
+ response.setHeader("content-length", size);
6777
+ createReadStream(file).on("error", () => response.destroy()).pipe(response);
6578
6778
  return;
6579
6779
  }
6580
- const range = byteRange(request.headers.range, stats.size);
6780
+ const range = byteRange(request.headers.range, size);
6581
6781
  if (range === null) {
6582
6782
  response.statusCode = 416;
6583
- response.setHeader("content-range", `bytes */${stats.size}`);
6783
+ response.setHeader("content-range", `bytes */${size}`);
6584
6784
  response.end();
6585
6785
  return;
6586
6786
  }
6587
6787
  response.statusCode = 206;
6588
- response.setHeader("content-range", `bytes ${range.start}-${range.end}/${stats.size}`);
6788
+ response.setHeader("content-range", `bytes ${range.start}-${range.end}/${size}`);
6589
6789
  response.setHeader("content-length", range.end - range.start + 1);
6590
- createReadStream(link.file, range).on("error", () => response.destroy()).pipe(response);
6790
+ createReadStream(file, range).on("error", () => response.destroy()).pipe(response);
6591
6791
  }
6592
6792
  function byteRange(header, size) {
6593
6793
  const match = /^bytes=(?:(\d+)-(\d*)|-(\d+))$/.exec(header);