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.
- package/apps/server/dist/main.mjs +325 -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-DAHGeLcz.js → dist-Dce5gUb9.js} +1 -1
- package/apps/web/dist/assets/index-BNP3kNP8.css +2 -0
- package/apps/web/dist/assets/index-D1dHWaOx.js +782 -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-Cnet_ssC.css +0 -2
- package/apps/web/dist/assets/index-Dhe_JHH1.js +0 -782
- 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({
|
|
@@ -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",
|
|
6577
|
-
createReadStream(
|
|
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,
|
|
6780
|
+
const range = byteRange(request.headers.range, size);
|
|
6581
6781
|
if (range === null) {
|
|
6582
6782
|
response.statusCode = 416;
|
|
6583
|
-
response.setHeader("content-range", `bytes */${
|
|
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}/${
|
|
6788
|
+
response.setHeader("content-range", `bytes ${range.start}-${range.end}/${size}`);
|
|
6589
6789
|
response.setHeader("content-length", range.end - range.start + 1);
|
|
6590
|
-
createReadStream(
|
|
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);
|