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.
- package/apps/server/dist/main.mjs +324 -125
- package/apps/server/dist/main.mjs.map +1 -1
- package/apps/server/src/services/open/open.mjs +50 -0
- package/apps/server/src/services/open/open.ts +71 -0
- package/apps/web/dist/assets/{dist-BFSo4bqV.js → dist-BtfuptBR.js} +1 -1
- package/apps/web/dist/assets/index-7KisGKFg.js +782 -0
- package/apps/web/dist/assets/index-BeAmEPQC.css +2 -0
- package/apps/web/dist/assets/video-player-4TBQR7ei.js +1 -0
- package/apps/web/dist/assets/{video-player-DlVZx3V3.js → video-player-DsF9gAr6.js} +1 -1
- package/apps/web/dist/assets/{video-viewer-DJBDb-Jx.js → video-viewer-DAKoRaNT.js} +1 -1
- package/apps/web/dist/index.html +2 -2
- package/apps/web/dist/sw.js +1 -1
- package/package.json +2 -1
- package/apps/web/dist/assets/index-CUhKVIGk.js +0 -782
- package/apps/web/dist/assets/index-Cop1FRe3.css +0 -2
- package/apps/web/dist/assets/video-player-DDu2cDQE.js +0 -1
|
@@ -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",
|
|
6577
|
-
createReadStream(
|
|
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,
|
|
6779
|
+
const range = byteRange(request.headers.range, size);
|
|
6581
6780
|
if (range === null) {
|
|
6582
6781
|
response.statusCode = 416;
|
|
6583
|
-
response.setHeader("content-range", `bytes */${
|
|
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}/${
|
|
6787
|
+
response.setHeader("content-range", `bytes ${range.start}-${range.end}/${size}`);
|
|
6589
6788
|
response.setHeader("content-length", range.end - range.start + 1);
|
|
6590
|
-
createReadStream(
|
|
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);
|