offsite 0.0.62 → 0.0.64
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/client/dist/assets/{XIcon-BF9rDynM.js → XIcon-CTnyaJjt.js} +1 -1
- package/apps/client/dist/assets/{context-menu-ah5Njpdz.js → context-menu-PUpo3KSB.js} +6 -6
- package/apps/client/dist/assets/{diff-panel-BKiJiAiZ.js → diff-panel-D21KovGL.js} +2 -2
- package/apps/client/dist/assets/{dist-Cgj97HqD.js → dist-Dru-KKjx.js} +1 -1
- package/apps/client/dist/assets/{editor-dialog-B7QW5wuQ.js → editor-dialog-D340lWxq.js} +30 -30
- package/apps/client/dist/assets/{image-viewer-DVmKXAyv.js → image-viewer-BnE64jz6.js} +1 -1
- package/apps/client/dist/assets/index-B4cIMymV.js +21 -0
- package/apps/client/dist/assets/{preview-dialog-B-LOxdLg.js → preview-dialog-CheTA7-7.js} +1 -1
- package/apps/client/dist/assets/{video-viewer-BBAwsDzR.js → video-viewer-9KuQg3l7.js} +1 -1
- package/apps/client/dist/index.html +4 -4
- package/apps/client/dist/sw.js +1 -1
- package/apps/server/dist/main.mjs +296 -140
- package/apps/server/dist/main.mjs.map +1 -1
- package/package.json +1 -1
- package/apps/client/dist/assets/index-C6uF49Ez.js +0 -21
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
2
|
-
import { appendFileSync, chmodSync, closeSync, createReadStream, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, watch, writeFileSync } from "node:fs";
|
|
2
|
+
import { accessSync, appendFileSync, chmodSync, closeSync, constants, createReadStream, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, watch, writeFileSync } from "node:fs";
|
|
3
3
|
import http, { createServer } from "node:http";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import zlib, { createGzip } from "node:zlib";
|
|
@@ -31,7 +31,6 @@ import { CookieJar, getPublicSuffix } from "tough-cookie";
|
|
|
31
31
|
import { spawn as spawn$1 } from "@lydell/node-pty";
|
|
32
32
|
import { once } from "node:events";
|
|
33
33
|
import https from "node:https";
|
|
34
|
-
import { buffer } from "node:stream/consumers";
|
|
35
34
|
import { pipeline } from "node:stream/promises";
|
|
36
35
|
import { defaultTreeAdapter, html, parse, serialize } from "parse5";
|
|
37
36
|
import { MagicString } from "magic-string";
|
|
@@ -302,6 +301,7 @@ function findCodex() {
|
|
|
302
301
|
var AppServer = class {
|
|
303
302
|
home;
|
|
304
303
|
threads = /* @__PURE__ */ new Map();
|
|
304
|
+
agents = /* @__PURE__ */ new Map();
|
|
305
305
|
child;
|
|
306
306
|
waiting = /* @__PURE__ */ new Map();
|
|
307
307
|
ready;
|
|
@@ -418,9 +418,16 @@ var AppServer = class {
|
|
|
418
418
|
}
|
|
419
419
|
const params = message.params ?? {};
|
|
420
420
|
const listener = typeof params.threadId === "string" ? this.threads.get(params.threadId) : void 0;
|
|
421
|
+
const root = this.rootOf(params.threadId);
|
|
422
|
+
const owner = root === void 0 ? void 0 : this.threads.get(root);
|
|
423
|
+
if (root !== void 0) this.linkAgents(root, params.item);
|
|
421
424
|
if (message.id !== void 0) {
|
|
422
|
-
if (
|
|
423
|
-
else
|
|
425
|
+
if (owner === void 0) this.replyError(message.id, `offsite does not handle ${message.method}`);
|
|
426
|
+
else owner.request(message.id, message.method, params);
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
if (message.method === "serverRequest/resolved") {
|
|
430
|
+
owner?.notify(message.method, params);
|
|
424
431
|
return;
|
|
425
432
|
}
|
|
426
433
|
if (message.method === "account/rateLimits/updated") {
|
|
@@ -429,6 +436,14 @@ var AppServer = class {
|
|
|
429
436
|
}
|
|
430
437
|
listener?.notify(message.method, params);
|
|
431
438
|
}
|
|
439
|
+
rootOf(threadId) {
|
|
440
|
+
if (typeof threadId !== "string") return;
|
|
441
|
+
return this.threads.has(threadId) ? threadId : this.agents.get(threadId);
|
|
442
|
+
}
|
|
443
|
+
linkAgents(root, item) {
|
|
444
|
+
if (item?.type === "subAgentActivity") this.agents.set(item.agentThreadId, root);
|
|
445
|
+
if (item?.type === "collabAgentToolCall") for (const agent of item.receiverThreadIds) this.agents.set(agent, root);
|
|
446
|
+
}
|
|
432
447
|
armIdleTimer() {
|
|
433
448
|
clearTimeout(this.idleTimer);
|
|
434
449
|
this.idleTimer = setTimeout(() => {
|
|
@@ -1749,7 +1764,13 @@ function fold(text) {
|
|
|
1749
1764
|
return folded.length === 1 ? folded : character;
|
|
1750
1765
|
}).toLowerCase();
|
|
1751
1766
|
}
|
|
1767
|
+
function exactPhrase(query) {
|
|
1768
|
+
const phrase = /^"([^"]+)"$/.exec(query.trim())?.[1].trim();
|
|
1769
|
+
return phrase === void 0 || phrase === "" ? null : phrase;
|
|
1770
|
+
}
|
|
1752
1771
|
function searchWords(query) {
|
|
1772
|
+
const phrase = exactPhrase(query);
|
|
1773
|
+
if (phrase !== null) return [fold(phrase)];
|
|
1753
1774
|
return fold(query).replace(/^["'`]+|["'`]+$/g, "").split(/\s+/).filter(Boolean);
|
|
1754
1775
|
}
|
|
1755
1776
|
function hasSearchableWord(query) {
|
|
@@ -1764,6 +1785,28 @@ function isWordStart(text, index) {
|
|
|
1764
1785
|
function isWordEnd(text, end) {
|
|
1765
1786
|
return end >= text.length || !alphanumeric.test(text[end]) || isWordStart(text, end);
|
|
1766
1787
|
}
|
|
1788
|
+
function mergeRanges(ranges) {
|
|
1789
|
+
const merged = [];
|
|
1790
|
+
for (const range of [...ranges].sort((a, b) => a.start - b.start)) {
|
|
1791
|
+
const last = merged.at(-1);
|
|
1792
|
+
if (last !== void 0 && range.start <= last.end) last.end = Math.max(last.end, range.end);
|
|
1793
|
+
else merged.push({ ...range });
|
|
1794
|
+
}
|
|
1795
|
+
return merged;
|
|
1796
|
+
}
|
|
1797
|
+
function exactRanges(folded, phrase) {
|
|
1798
|
+
const ranges = [];
|
|
1799
|
+
for (let index = folded.indexOf(phrase); index !== -1; index = folded.indexOf(phrase, index + 1)) {
|
|
1800
|
+
const end = index + phrase.length;
|
|
1801
|
+
const joinedBefore = alphanumeric.test(phrase[0]) && alphanumeric.test(folded[index - 1] ?? "");
|
|
1802
|
+
const joinedAfter = alphanumeric.test(phrase[phrase.length - 1]) && alphanumeric.test(folded[end] ?? "");
|
|
1803
|
+
if (!joinedBefore && !joinedAfter) ranges.push({
|
|
1804
|
+
start: index,
|
|
1805
|
+
end
|
|
1806
|
+
});
|
|
1807
|
+
}
|
|
1808
|
+
return mergeRanges(ranges);
|
|
1809
|
+
}
|
|
1767
1810
|
function findWord(text, folded, word) {
|
|
1768
1811
|
let quality = 0;
|
|
1769
1812
|
let at = -1;
|
|
@@ -1783,6 +1826,13 @@ function matchMessage(query, text) {
|
|
|
1783
1826
|
const words = searchWords(query);
|
|
1784
1827
|
if (words.length === 0) return null;
|
|
1785
1828
|
const folded = fold(text);
|
|
1829
|
+
if (exactPhrase(query) !== null) {
|
|
1830
|
+
const range = exactRanges(folded, words[0]).at(0);
|
|
1831
|
+
return range === void 0 ? null : {
|
|
1832
|
+
quality: 1,
|
|
1833
|
+
offset: range.start
|
|
1834
|
+
};
|
|
1835
|
+
}
|
|
1786
1836
|
let quality = 1;
|
|
1787
1837
|
let offset = text.length;
|
|
1788
1838
|
for (const word of words) {
|
|
@@ -2224,7 +2274,10 @@ async function openCodeSessions(paths) {
|
|
|
2224
2274
|
directory,
|
|
2225
2275
|
parentID: null,
|
|
2226
2276
|
limit: PAGE
|
|
2227
|
-
} : {
|
|
2277
|
+
} : {
|
|
2278
|
+
limit: PAGE,
|
|
2279
|
+
cursor
|
|
2280
|
+
});
|
|
2228
2281
|
found.push(...page.data);
|
|
2229
2282
|
cursor = page.data.length === PAGE ? page.cursor.next ?? null : null;
|
|
2230
2283
|
} while (cursor !== null);
|
|
@@ -2361,6 +2414,7 @@ async function allMessages(client, sessionId) {
|
|
|
2361
2414
|
order: "asc"
|
|
2362
2415
|
} : {
|
|
2363
2416
|
sessionID: sessionId,
|
|
2417
|
+
limit: PAGE,
|
|
2364
2418
|
cursor
|
|
2365
2419
|
});
|
|
2366
2420
|
messages.push(...page.data);
|
|
@@ -2777,14 +2831,63 @@ const pushSubscriptions = snakeCase.table("push_subscriptions", {
|
|
|
2777
2831
|
machine: text().notNull(),
|
|
2778
2832
|
server: text().notNull()
|
|
2779
2833
|
});
|
|
2834
|
+
// src/storage/auth.ts
|
|
2835
|
+
const CODE_LIFETIME = 3e5;
|
|
2836
|
+
const DEVICE_LIFETIME = 2592e6;
|
|
2837
|
+
function addCode(name) {
|
|
2838
|
+
const code = randomBytes(16).toString("base64url");
|
|
2839
|
+
db.delete(pairingCodes).where(lte(pairingCodes.expiresAt, Date.now())).run();
|
|
2840
|
+
db.insert(pairingCodes).values({
|
|
2841
|
+
hash: hash(code),
|
|
2842
|
+
name,
|
|
2843
|
+
expiresAt: Date.now() + CODE_LIFETIME
|
|
2844
|
+
}).run();
|
|
2845
|
+
return code;
|
|
2846
|
+
}
|
|
2847
|
+
function redeemCode(code) {
|
|
2848
|
+
const found = db.delete(pairingCodes).where(eq(pairingCodes.hash, hash(code))).returning().get();
|
|
2849
|
+
if (found === void 0 || found.expiresAt <= Date.now()) return null;
|
|
2850
|
+
const token = randomBytes(32).toString("base64url");
|
|
2851
|
+
const now = Date.now();
|
|
2852
|
+
db.insert(devices).values({
|
|
2853
|
+
hash: hash(token),
|
|
2854
|
+
name: found.name,
|
|
2855
|
+
createdAt: now,
|
|
2856
|
+
expiresAt: now + DEVICE_LIFETIME
|
|
2857
|
+
}).run();
|
|
2858
|
+
return token;
|
|
2859
|
+
}
|
|
2860
|
+
function findDevice(token) {
|
|
2861
|
+
const found = db.select().from(devices).where(and(eq(devices.hash, hash(token)), gt(devices.expiresAt, Date.now()))).get();
|
|
2862
|
+
return found === void 0 ? null : found.hash;
|
|
2863
|
+
}
|
|
2864
|
+
function isPaired(device) {
|
|
2865
|
+
return db.select().from(devices).where(and(eq(devices.hash, device), gt(devices.expiresAt, Date.now()))).get() !== void 0;
|
|
2866
|
+
}
|
|
2867
|
+
function hasDevices() {
|
|
2868
|
+
return db.select().from(devices).where(gt(devices.expiresAt, Date.now())).limit(1).get() !== void 0;
|
|
2869
|
+
}
|
|
2870
|
+
function readDevices() {
|
|
2871
|
+
return db.select().from(devices).orderBy(devices.createdAt).all();
|
|
2872
|
+
}
|
|
2873
|
+
function removeDevices(hashes) {
|
|
2874
|
+
db.delete(devices).where(inArray(devices.hash, hashes)).run();
|
|
2875
|
+
}
|
|
2876
|
+
function recordUse(device) {
|
|
2877
|
+
db.update(devices).set({ lastUsedAt: Date.now() }).where(eq(devices.hash, device)).run();
|
|
2878
|
+
}
|
|
2879
|
+
function hash(secret) {
|
|
2880
|
+
return createHash("sha256").update(secret).digest("hex");
|
|
2881
|
+
}
|
|
2780
2882
|
// src/services/files.ts
|
|
2781
2883
|
const FILE_LIMIT = 2e4;
|
|
2782
2884
|
const SNIFF_LENGTH = 8e3;
|
|
2783
2885
|
const OUTPUT_LIMIT$1 = 262144;
|
|
2784
2886
|
const READ_LIMIT$1 = 1048576;
|
|
2785
2887
|
const MEDIA_LINK_LIFETIME = 36e5;
|
|
2786
|
-
const
|
|
2888
|
+
const BROWSE_LINK_LIFETIME = 36e5;
|
|
2787
2889
|
const mediaLinks = /* @__PURE__ */ new Map();
|
|
2890
|
+
const browseLinks = /* @__PURE__ */ new Map();
|
|
2788
2891
|
const skipped = /* @__PURE__ */ new Set([
|
|
2789
2892
|
"node_modules",
|
|
2790
2893
|
".git",
|
|
@@ -2855,10 +2958,12 @@ function readProjectFile(cwd, file) {
|
|
|
2855
2958
|
content: data.toString("utf8")
|
|
2856
2959
|
};
|
|
2857
2960
|
}
|
|
2858
|
-
function writeProjectFile(cwd, file, content) {
|
|
2961
|
+
function writeProjectFile(cwd, file, content, base) {
|
|
2859
2962
|
const full = resolveProjectFile(cwd, file);
|
|
2860
2963
|
if (!statSync(full).isFile()) throw new Error(`${file} is not a file`);
|
|
2964
|
+
if (base !== null && readFileSync(full, "utf8") !== base) return false;
|
|
2861
2965
|
writeFileSync(full, content);
|
|
2966
|
+
return true;
|
|
2862
2967
|
}
|
|
2863
2968
|
function createProjectEntry(cwd, file, folder) {
|
|
2864
2969
|
const full = resolveNewProjectPath(cwd, file);
|
|
@@ -2878,18 +2983,33 @@ function renameProjectEntry(cwd, file, to) {
|
|
|
2878
2983
|
function deleteProjectEntry(cwd, file) {
|
|
2879
2984
|
rmSync(resolveProjectEntry(cwd, file), { recursive: true });
|
|
2880
2985
|
}
|
|
2881
|
-
function linkProjectMedia(cwd, file) {
|
|
2986
|
+
function linkProjectMedia(cwd, file, device) {
|
|
2882
2987
|
const resolved = path.resolve(realpathSync(cwd), file);
|
|
2883
2988
|
if (!existsSync(resolved)) throw new Error(`${file} does not exist`);
|
|
2884
2989
|
const full = realpathSync(resolved);
|
|
2885
2990
|
const mediaType = mediaTypes.get(path.extname(full).toLowerCase());
|
|
2886
2991
|
if (mediaType === void 0) throw new Error(`${file} is not an image, video or audio file`);
|
|
2887
|
-
return addLink(full, mediaType);
|
|
2992
|
+
return addLink(full, mediaType, device);
|
|
2888
2993
|
}
|
|
2889
|
-
function linkProjectDownload(cwd, file) {
|
|
2994
|
+
function linkProjectDownload(cwd, file, device) {
|
|
2890
2995
|
const full = resolveProjectFile(cwd, file);
|
|
2891
2996
|
if (!statSync(full).isFile()) throw new Error(`${file} is not a file`);
|
|
2892
|
-
return addLink(full, "application/octet-stream");
|
|
2997
|
+
return addLink(full, "application/octet-stream", device);
|
|
2998
|
+
}
|
|
2999
|
+
function linkBrowse(device, address) {
|
|
3000
|
+
for (const [token, link] of browseLinks) if (link.expiresAt <= Date.now()) browseLinks.delete(token);
|
|
3001
|
+
const token = randomBytes(32).toString("base64url");
|
|
3002
|
+
browseLinks.set(token, {
|
|
3003
|
+
device,
|
|
3004
|
+
address,
|
|
3005
|
+
expiresAt: Date.now() + BROWSE_LINK_LIFETIME
|
|
3006
|
+
});
|
|
3007
|
+
return token;
|
|
3008
|
+
}
|
|
3009
|
+
function canBrowse(token, address) {
|
|
3010
|
+
const link = browseLinks.get(token);
|
|
3011
|
+
if (link === void 0 || link.expiresAt <= Date.now() || link.address !== address) return false;
|
|
3012
|
+
return link.device === null || isPaired(link.device);
|
|
2893
3013
|
}
|
|
2894
3014
|
function setBrowseHeaders(response, contentType) {
|
|
2895
3015
|
response.setHeader("content-type", contentType);
|
|
@@ -2903,12 +3023,50 @@ function sniffType(file) {
|
|
|
2903
3023
|
closeSync(handle);
|
|
2904
3024
|
return head.subarray(0, read).includes(0) ? "application/octet-stream" : "text/plain; charset=utf-8";
|
|
2905
3025
|
}
|
|
2906
|
-
function
|
|
3026
|
+
function streamFile(file, size, request, response) {
|
|
3027
|
+
response.setHeader("accept-ranges", "bytes");
|
|
3028
|
+
if (request.headers.range === void 0) {
|
|
3029
|
+
response.setHeader("content-length", size);
|
|
3030
|
+
createReadStream(file).on("error", () => response.destroy()).pipe(response);
|
|
3031
|
+
return;
|
|
3032
|
+
}
|
|
3033
|
+
const range = byteRange(request.headers.range, size);
|
|
3034
|
+
if (range === null) {
|
|
3035
|
+
response.statusCode = 416;
|
|
3036
|
+
response.setHeader("content-range", `bytes */${size}`);
|
|
3037
|
+
response.end();
|
|
3038
|
+
return;
|
|
3039
|
+
}
|
|
3040
|
+
response.statusCode = 206;
|
|
3041
|
+
response.setHeader("content-range", `bytes ${range.start}-${range.end}/${size}`);
|
|
3042
|
+
response.setHeader("content-length", range.end - range.start + 1);
|
|
3043
|
+
createReadStream(file, range).on("error", () => response.destroy()).pipe(response);
|
|
3044
|
+
}
|
|
3045
|
+
function byteRange(header, size) {
|
|
3046
|
+
const match = /^bytes=(?:(\d+)-(\d*)|-(\d+))$/.exec(header);
|
|
3047
|
+
if (match === null) return null;
|
|
3048
|
+
if (match[3] !== void 0) {
|
|
3049
|
+
if (size === 0 || Number(match[3]) === 0) return null;
|
|
3050
|
+
return {
|
|
3051
|
+
start: Math.max(size - Number(match[3]), 0),
|
|
3052
|
+
end: size - 1
|
|
3053
|
+
};
|
|
3054
|
+
}
|
|
3055
|
+
const start = Number(match[1]);
|
|
3056
|
+
const end = match[2] === "" ? size - 1 : Math.min(Number(match[2]), size - 1);
|
|
3057
|
+
if (start > end) return null;
|
|
3058
|
+
return {
|
|
3059
|
+
start,
|
|
3060
|
+
end
|
|
3061
|
+
};
|
|
3062
|
+
}
|
|
3063
|
+
function addLink(file, mediaType, device) {
|
|
2907
3064
|
for (const [token, link] of mediaLinks) if (link.expiresAt <= Date.now()) mediaLinks.delete(token);
|
|
2908
3065
|
const token = randomBytes(32).toString("base64url");
|
|
2909
3066
|
mediaLinks.set(token, {
|
|
2910
3067
|
file,
|
|
2911
3068
|
mediaType,
|
|
3069
|
+
device,
|
|
2912
3070
|
expiresAt: Date.now() + MEDIA_LINK_LIFETIME
|
|
2913
3071
|
});
|
|
2914
3072
|
return token;
|
|
@@ -2917,6 +3075,7 @@ function findMediaLink(token) {
|
|
|
2917
3075
|
if (token === null) return null;
|
|
2918
3076
|
const link = mediaLinks.get(token);
|
|
2919
3077
|
if (link === void 0 || link.expiresAt <= Date.now()) return null;
|
|
3078
|
+
if (link.device !== null && !isPaired(link.device)) return null;
|
|
2920
3079
|
return link;
|
|
2921
3080
|
}
|
|
2922
3081
|
function saveUpload(name, data) {
|
|
@@ -3070,7 +3229,12 @@ async function gitRepos(cwd) {
|
|
|
3070
3229
|
}
|
|
3071
3230
|
async function commit(cwd, message, andPush) {
|
|
3072
3231
|
const text = message.trim() === "" ? await commitMessage(cwd, false) : message;
|
|
3073
|
-
await git(cwd, [
|
|
3232
|
+
await git(cwd, [
|
|
3233
|
+
"add",
|
|
3234
|
+
"--all",
|
|
3235
|
+
"--",
|
|
3236
|
+
"."
|
|
3237
|
+
]);
|
|
3074
3238
|
await git(cwd, [
|
|
3075
3239
|
"commit",
|
|
3076
3240
|
"--message",
|
|
@@ -3269,7 +3433,9 @@ async function gitStatus(cwd) {
|
|
|
3269
3433
|
"--no-optional-locks",
|
|
3270
3434
|
"status",
|
|
3271
3435
|
"--porcelain=v2",
|
|
3272
|
-
"--branch"
|
|
3436
|
+
"--branch",
|
|
3437
|
+
"--",
|
|
3438
|
+
"."
|
|
3273
3439
|
]);
|
|
3274
3440
|
const status = {
|
|
3275
3441
|
branch: null,
|
|
@@ -3325,14 +3491,16 @@ async function repoDiff(repo, prefix) {
|
|
|
3325
3491
|
await diffBase(repo),
|
|
3326
3492
|
"--no-color",
|
|
3327
3493
|
"--no-ext-diff",
|
|
3494
|
+
"--relative",
|
|
3328
3495
|
`--src-prefix=a/${prefix}`,
|
|
3329
3496
|
`--dst-prefix=b/${prefix}`
|
|
3330
3497
|
]);
|
|
3331
3498
|
const added = (await git(repo, [
|
|
3332
3499
|
"ls-files",
|
|
3333
3500
|
"--others",
|
|
3334
|
-
"--exclude-standard"
|
|
3335
|
-
|
|
3501
|
+
"--exclude-standard",
|
|
3502
|
+
"-z"
|
|
3503
|
+
])).stdout.split("\0").filter(Boolean).slice(0, UNTRACKED_LIMIT).map((file) => newFilePatch(repo, file, prefix));
|
|
3336
3504
|
return [tracked.stdout, ...added].join("");
|
|
3337
3505
|
}
|
|
3338
3506
|
async function diffBase(repo) {
|
|
@@ -5750,7 +5918,8 @@ async function claudeMessages(sessionId, dir) {
|
|
|
5750
5918
|
});
|
|
5751
5919
|
}
|
|
5752
5920
|
async function claudeCandidates(query) {
|
|
5753
|
-
const
|
|
5921
|
+
const phrase = exactPhrase(query);
|
|
5922
|
+
const words = phrase !== null ? [JSON.stringify(phrase).slice(1, -1)] : searchWords(query).filter((word) => word.length >= 3 && !/["\\]/.test(word));
|
|
5754
5923
|
if (words.length === 0) return null;
|
|
5755
5924
|
try {
|
|
5756
5925
|
const lists = await Promise.all(words.map((word) => sessionsWith(word)));
|
|
@@ -5783,7 +5952,7 @@ async function sessionsWith(word) {
|
|
|
5783
5952
|
})).stdout.split("\n").filter(Boolean).map((file) => path.basename(file, ".jsonl"));
|
|
5784
5953
|
}
|
|
5785
5954
|
async function searchCodex(query, sessions) {
|
|
5786
|
-
const longest = [...searchWords(query)].sort((a, b) => b.length - a.length)[0];
|
|
5955
|
+
const longest = exactPhrase(query) ?? [...searchWords(query)].sort((a, b) => b.length - a.length)[0];
|
|
5787
5956
|
const known = new Map(sessions.map((session) => [session.id, session]));
|
|
5788
5957
|
const found = (await codexSearch(longest)).filter((result) => known.has(result.thread.id)).sort((a, b) => b.thread.updatedAt - a.thread.updatedAt);
|
|
5789
5958
|
const verified = await readAndMatch(query, found.slice(0, CODEX_READ_LIMIT).map((result) => ({ id: result.thread.id })), codexMessages);
|
|
@@ -6053,7 +6222,7 @@ async function showFile(cwd, ref, file) {
|
|
|
6053
6222
|
const output = await git(cwd, [
|
|
6054
6223
|
"cat-file",
|
|
6055
6224
|
"blob",
|
|
6056
|
-
`${ref}
|
|
6225
|
+
`${ref}:./${file}`
|
|
6057
6226
|
]);
|
|
6058
6227
|
if (output.stdout.length > SHOW_LIMIT || output.stdout.includes("\0")) return null;
|
|
6059
6228
|
return output.stdout;
|
|
@@ -6086,6 +6255,7 @@ async function commitFiles(cwd, hash, parent) {
|
|
|
6086
6255
|
"--name-status",
|
|
6087
6256
|
"-M",
|
|
6088
6257
|
"-z",
|
|
6258
|
+
"--relative",
|
|
6089
6259
|
parent ?? (await git(cwd, [
|
|
6090
6260
|
"hash-object",
|
|
6091
6261
|
"-t",
|
|
@@ -6169,7 +6339,7 @@ async function runAction(cwd, action) {
|
|
|
6169
6339
|
await git(cwd, [
|
|
6170
6340
|
"update-index",
|
|
6171
6341
|
"--cacheinfo",
|
|
6172
|
-
`${entry.stdout.split(" ")[0]},${hash},${action.path}`
|
|
6342
|
+
`${entry.stdout.split(" ")[0]},${hash},${await repoPrefix(cwd)}${action.path}`
|
|
6173
6343
|
]);
|
|
6174
6344
|
return;
|
|
6175
6345
|
}
|
|
@@ -6209,7 +6379,12 @@ async function runAction(cwd, action) {
|
|
|
6209
6379
|
return;
|
|
6210
6380
|
}
|
|
6211
6381
|
if (action.kind === "commit") {
|
|
6212
|
-
if (action.all) await git(cwd, [
|
|
6382
|
+
if (action.all) await git(cwd, [
|
|
6383
|
+
"add",
|
|
6384
|
+
"--all",
|
|
6385
|
+
"--",
|
|
6386
|
+
"."
|
|
6387
|
+
]);
|
|
6213
6388
|
await git(cwd, [
|
|
6214
6389
|
"commit",
|
|
6215
6390
|
...action.amend ? ["--amend"] : [],
|
|
@@ -6263,6 +6438,9 @@ async function network(cwd, args) {
|
|
|
6263
6438
|
throw gitError(error);
|
|
6264
6439
|
}
|
|
6265
6440
|
}
|
|
6441
|
+
async function repoPrefix(cwd) {
|
|
6442
|
+
return (await git(cwd, ["rev-parse", "--show-prefix"])).stdout.trim();
|
|
6443
|
+
}
|
|
6266
6444
|
async function repoChanges(cwd) {
|
|
6267
6445
|
const output = await git(cwd, [
|
|
6268
6446
|
"--no-optional-locks",
|
|
@@ -6270,8 +6448,11 @@ async function repoChanges(cwd) {
|
|
|
6270
6448
|
"--porcelain=v2",
|
|
6271
6449
|
"--branch",
|
|
6272
6450
|
"--untracked-files=all",
|
|
6273
|
-
"-z"
|
|
6451
|
+
"-z",
|
|
6452
|
+
"--",
|
|
6453
|
+
"."
|
|
6274
6454
|
]);
|
|
6455
|
+
const prefix = await repoPrefix(cwd);
|
|
6275
6456
|
const repo = {
|
|
6276
6457
|
head: null,
|
|
6277
6458
|
branch: null,
|
|
@@ -6294,19 +6475,19 @@ async function repoChanges(cwd) {
|
|
|
6294
6475
|
repo.behind = -Number(fields[3]);
|
|
6295
6476
|
}
|
|
6296
6477
|
if (fields[0] === "?") repo.changes.push({
|
|
6297
|
-
path: tokens[index].slice(2),
|
|
6478
|
+
path: tokens[index].slice(2 + prefix.length),
|
|
6298
6479
|
from: null,
|
|
6299
6480
|
status: "U"
|
|
6300
6481
|
});
|
|
6301
6482
|
if (fields[0] === "u") repo.merge.push({
|
|
6302
|
-
path: fields.slice(10).join(" "),
|
|
6483
|
+
path: fields.slice(10).join(" ").slice(prefix.length),
|
|
6303
6484
|
from: null,
|
|
6304
6485
|
status: "!"
|
|
6305
6486
|
});
|
|
6306
6487
|
if (fields[0] === "1" || fields[0] === "2") {
|
|
6307
6488
|
const renamed = fields[0] === "2";
|
|
6308
|
-
const file = fields.slice(renamed ? 9 : 8).join(" ");
|
|
6309
|
-
const from = renamed ? tokens[index + 1] : null;
|
|
6489
|
+
const file = fields.slice(renamed ? 9 : 8).join(" ").slice(prefix.length);
|
|
6490
|
+
const from = renamed ? tokens[index + 1].slice(prefix.length) : null;
|
|
6310
6491
|
if (fields[1][0] !== ".") repo.staged.push({
|
|
6311
6492
|
path: file,
|
|
6312
6493
|
from,
|
|
@@ -6898,6 +7079,7 @@ const active = /* @__PURE__ */ new Map();
|
|
|
6898
7079
|
const terminals = /* @__PURE__ */ new Map();
|
|
6899
7080
|
const connections = /* @__PURE__ */ new Map();
|
|
6900
7081
|
const paused = /* @__PURE__ */ new Set();
|
|
7082
|
+
const held = /* @__PURE__ */ new Set();
|
|
6901
7083
|
let nextTab = 0;
|
|
6902
7084
|
let draining = null;
|
|
6903
7085
|
async function terminalRequest(client, message) {
|
|
@@ -6957,6 +7139,8 @@ function attach(client) {
|
|
|
6957
7139
|
instance
|
|
6958
7140
|
});
|
|
6959
7141
|
for (const tab of tabs) forward$1(connection, tab.id, true, Buffer.concat(terminals.get(tab.id)?.history ?? []));
|
|
7142
|
+
for (const pty of held) pty.resume();
|
|
7143
|
+
held.clear();
|
|
6960
7144
|
}
|
|
6961
7145
|
function resize(connection, message) {
|
|
6962
7146
|
const terminal = terminals.get(message.id);
|
|
@@ -7066,9 +7250,14 @@ async function createTab(projectId) {
|
|
|
7066
7250
|
});
|
|
7067
7251
|
active.set(projectId ?? "global", id);
|
|
7068
7252
|
pty.onData((data) => output(id, terminal, Buffer.from(data)));
|
|
7253
|
+
if (connections.size === 0) {
|
|
7254
|
+
pty.pause();
|
|
7255
|
+
held.add(pty);
|
|
7256
|
+
}
|
|
7069
7257
|
pty.onExit(() => {
|
|
7070
7258
|
terminals.delete(id);
|
|
7071
7259
|
paused.delete(pty);
|
|
7260
|
+
held.delete(pty);
|
|
7072
7261
|
if (closeTab(id)) publish();
|
|
7073
7262
|
});
|
|
7074
7263
|
}
|
|
@@ -7408,10 +7597,7 @@ const handlers = {
|
|
|
7408
7597
|
"files.list": (params) => listFiles(projectPath(params.projectId)),
|
|
7409
7598
|
"files.entries": (params) => listEntries(projectPath(params.projectId)),
|
|
7410
7599
|
"files.read": (params) => readProjectFile(projectPath(params.projectId), params.path),
|
|
7411
|
-
"files.write": (params) =>
|
|
7412
|
-
writeProjectFile(projectPath(params.projectId), params.path, params.content);
|
|
7413
|
-
return null;
|
|
7414
|
-
},
|
|
7600
|
+
"files.write": (params) => writeProjectFile(projectPath(params.projectId), params.path, params.content, params.base),
|
|
7415
7601
|
"files.create": (params) => {
|
|
7416
7602
|
createProjectEntry(projectPath(params.projectId), params.path, params.folder);
|
|
7417
7603
|
return null;
|
|
@@ -7424,10 +7610,10 @@ const handlers = {
|
|
|
7424
7610
|
deleteProjectEntry(projectPath(params.projectId), params.path);
|
|
7425
7611
|
return null;
|
|
7426
7612
|
},
|
|
7427
|
-
"files.media": (params) => linkProjectMedia(projectPath(params.projectId), params.path),
|
|
7428
|
-
"files.download": (params) => linkProjectDownload(projectPath(params.projectId), params.path),
|
|
7613
|
+
"files.media": (params, client) => linkProjectMedia(projectPath(params.projectId), params.path, client.device),
|
|
7614
|
+
"files.download": (params, client) => linkProjectDownload(projectPath(params.projectId), params.path, client.device),
|
|
7429
7615
|
"files.upload": (params) => saveUpload(params.name, Buffer.from(params.data, "base64")),
|
|
7430
|
-
"files.browse": () =>
|
|
7616
|
+
"files.browse": (_params, client) => linkBrowse(client.device, client.address),
|
|
7431
7617
|
"browser.open": (params) => openSite(params.url, params.fromPage),
|
|
7432
7618
|
"tasks.output": (params) => taskOutput(params.sessionId, params.path),
|
|
7433
7619
|
"git.status": (params) => gitRepos(projectPath(params.projectId)),
|
|
@@ -7484,9 +7670,11 @@ const handlers = {
|
|
|
7484
7670
|
"terminal.busy": (params) => busyTerminal(params.id),
|
|
7485
7671
|
"terminal.paste": (params) => savePastedImage(params.type, Buffer.from(params.data, "base64"))
|
|
7486
7672
|
};
|
|
7487
|
-
function connectClient(socket) {
|
|
7673
|
+
function connectClient(socket, device, address) {
|
|
7488
7674
|
const client = {
|
|
7489
7675
|
socket,
|
|
7676
|
+
device,
|
|
7677
|
+
address,
|
|
7490
7678
|
heardAt: Date.now(),
|
|
7491
7679
|
opening: null,
|
|
7492
7680
|
sessionId: null,
|
|
@@ -8362,6 +8550,7 @@ const TARGET = /^(https?)\/([^/?#]+)(.*)$/;
|
|
|
8362
8550
|
const SANDBOX = "sandbox allow-scripts allow-forms allow-popups allow-modals allow-downloads allow-presentation allow-pointer-lock";
|
|
8363
8551
|
const UPSTREAM_TIMEOUT = 6e4;
|
|
8364
8552
|
const MAX_BODY = 8388608;
|
|
8553
|
+
const MAX_PAGE = 33554432;
|
|
8365
8554
|
const MAX_HEADERS = 262144;
|
|
8366
8555
|
const REQUEST_HEADERS_DROPPED = /* @__PURE__ */ new Set([
|
|
8367
8556
|
"accept-encoding",
|
|
@@ -8411,6 +8600,7 @@ const RESPONSE_HEADERS_DROPPED = /* @__PURE__ */ new Set([
|
|
|
8411
8600
|
"service-worker-allowed",
|
|
8412
8601
|
"set-cookie",
|
|
8413
8602
|
"strict-transport-security",
|
|
8603
|
+
"trailer",
|
|
8414
8604
|
"transfer-encoding",
|
|
8415
8605
|
"x-frame-options"
|
|
8416
8606
|
]);
|
|
@@ -8506,10 +8696,17 @@ async function forward(site, target, request, response) {
|
|
|
8506
8696
|
request.pipe(upstream);
|
|
8507
8697
|
const incoming = (await answer)[0];
|
|
8508
8698
|
upstream.setTimeout(0);
|
|
8699
|
+
const status = incoming.statusCode;
|
|
8700
|
+
if (status === void 0 || status < 200 || status > 999) throw new Error(`${target.origin} sent an invalid status`);
|
|
8509
8701
|
for (const cookie of incoming.headers["set-cookie"] ?? []) await site.jar.setCookie(cookie, target.href, { ignoreError: true });
|
|
8510
8702
|
const kind = rewriteKind(incoming, request);
|
|
8511
|
-
response.statusCode =
|
|
8703
|
+
response.statusCode = status;
|
|
8512
8704
|
copyHeaders(incoming, response, kind !== null);
|
|
8705
|
+
if (kind === "html") {
|
|
8706
|
+
response.removeHeader("last-modified");
|
|
8707
|
+
response.removeHeader("expires");
|
|
8708
|
+
response.setHeader("cache-control", "no-store, no-transform");
|
|
8709
|
+
}
|
|
8513
8710
|
const location = incoming.headers.location;
|
|
8514
8711
|
if (location !== void 0) {
|
|
8515
8712
|
const next = URL.parse(location, target);
|
|
@@ -8517,6 +8714,7 @@ async function forward(site, target, request, response) {
|
|
|
8517
8714
|
response.setHeader("location", proxiedPath(site.key, next));
|
|
8518
8715
|
}
|
|
8519
8716
|
if (kind === null) {
|
|
8717
|
+
response.flushHeaders();
|
|
8520
8718
|
await pipeline(incoming, response);
|
|
8521
8719
|
return;
|
|
8522
8720
|
}
|
|
@@ -8616,12 +8814,26 @@ function copyHeaders(incoming, response, rewritten) {
|
|
|
8616
8814
|
}
|
|
8617
8815
|
async function readBody(incoming) {
|
|
8618
8816
|
const encoding = incoming.headers["content-encoding"];
|
|
8619
|
-
if (encoding === void 0 || encoding === "identity") return
|
|
8620
|
-
|
|
8621
|
-
|
|
8622
|
-
|
|
8817
|
+
if (encoding === void 0 || encoding === "identity") return readLimited(incoming, MAX_PAGE);
|
|
8818
|
+
const decoder = createDecoder(encoding);
|
|
8819
|
+
return (await Promise.all([readLimited(decoder, MAX_PAGE), pipeline(incoming, decoder)]))[0];
|
|
8820
|
+
}
|
|
8821
|
+
function createDecoder(encoding) {
|
|
8822
|
+
if (encoding === "gzip") return zlib.createGunzip();
|
|
8823
|
+
if (encoding === "deflate") return zlib.createInflate();
|
|
8824
|
+
if (encoding === "br") return zlib.createBrotliDecompress();
|
|
8623
8825
|
throw new Error(`the site sent unknown compression: ${encoding}`);
|
|
8624
8826
|
}
|
|
8827
|
+
async function readLimited(source, limit) {
|
|
8828
|
+
const chunks = [];
|
|
8829
|
+
let size = 0;
|
|
8830
|
+
for await (const chunk of source) {
|
|
8831
|
+
size += chunk.length;
|
|
8832
|
+
if (size > limit) throw new Error("This page is too large");
|
|
8833
|
+
chunks.push(chunk);
|
|
8834
|
+
}
|
|
8835
|
+
return Buffer.concat(chunks);
|
|
8836
|
+
}
|
|
8625
8837
|
function decodeBody(body, incoming) {
|
|
8626
8838
|
const declared = /charset=["']?([\w-]+)/i.exec(incoming.headers["content-type"] ?? "")?.[1] ?? /<meta[^>]+charset=["']?([\w-]+)/i.exec(body.subarray(0, 1024).toString("latin1"))?.[1] ?? "utf-8";
|
|
8627
8839
|
try {
|
|
@@ -8652,11 +8864,7 @@ async function savePageData(site, kind, request, response) {
|
|
|
8652
8864
|
fail(response, 405, "use POST");
|
|
8653
8865
|
return;
|
|
8654
8866
|
}
|
|
8655
|
-
const body = await
|
|
8656
|
-
if (body.length > MAX_BODY) {
|
|
8657
|
-
fail(response, 413, "too much page data");
|
|
8658
|
-
return;
|
|
8659
|
-
}
|
|
8867
|
+
const body = await readLimited(request, MAX_BODY);
|
|
8660
8868
|
const data = JSON.parse(body.toString());
|
|
8661
8869
|
if (kind === "cookie") {
|
|
8662
8870
|
const write = pageWrite.parse(data);
|
|
@@ -8681,54 +8889,6 @@ function fail(response, status, message) {
|
|
|
8681
8889
|
response.setHeader("content-type", "text/plain; charset=utf-8");
|
|
8682
8890
|
response.end(message);
|
|
8683
8891
|
}
|
|
8684
|
-
// src/storage/auth.ts
|
|
8685
|
-
const CODE_LIFETIME = 3e5;
|
|
8686
|
-
const DEVICE_LIFETIME = 2592e6;
|
|
8687
|
-
function addCode(name) {
|
|
8688
|
-
const code = randomBytes(16).toString("base64url");
|
|
8689
|
-
db.delete(pairingCodes).where(lte(pairingCodes.expiresAt, Date.now())).run();
|
|
8690
|
-
db.insert(pairingCodes).values({
|
|
8691
|
-
hash: hash(code),
|
|
8692
|
-
name,
|
|
8693
|
-
expiresAt: Date.now() + CODE_LIFETIME
|
|
8694
|
-
}).run();
|
|
8695
|
-
return code;
|
|
8696
|
-
}
|
|
8697
|
-
function redeemCode(code) {
|
|
8698
|
-
const found = db.delete(pairingCodes).where(eq(pairingCodes.hash, hash(code))).returning().get();
|
|
8699
|
-
if (found === void 0 || found.expiresAt <= Date.now()) return null;
|
|
8700
|
-
const token = randomBytes(32).toString("base64url");
|
|
8701
|
-
const now = Date.now();
|
|
8702
|
-
db.insert(devices).values({
|
|
8703
|
-
hash: hash(token),
|
|
8704
|
-
name: found.name,
|
|
8705
|
-
createdAt: now,
|
|
8706
|
-
expiresAt: now + DEVICE_LIFETIME
|
|
8707
|
-
}).run();
|
|
8708
|
-
return token;
|
|
8709
|
-
}
|
|
8710
|
-
function findDevice(token) {
|
|
8711
|
-
const found = db.select().from(devices).where(and(eq(devices.hash, hash(token)), gt(devices.expiresAt, Date.now()))).get();
|
|
8712
|
-
return found === void 0 ? null : found.hash;
|
|
8713
|
-
}
|
|
8714
|
-
function isPaired(device) {
|
|
8715
|
-
return db.select().from(devices).where(and(eq(devices.hash, device), gt(devices.expiresAt, Date.now()))).get() !== void 0;
|
|
8716
|
-
}
|
|
8717
|
-
function hasDevices() {
|
|
8718
|
-
return db.select().from(devices).where(gt(devices.expiresAt, Date.now())).limit(1).get() !== void 0;
|
|
8719
|
-
}
|
|
8720
|
-
function readDevices() {
|
|
8721
|
-
return db.select().from(devices).orderBy(devices.createdAt).all();
|
|
8722
|
-
}
|
|
8723
|
-
function removeDevices(hashes) {
|
|
8724
|
-
db.delete(devices).where(inArray(devices.hash, hashes)).run();
|
|
8725
|
-
}
|
|
8726
|
-
function recordUse(device) {
|
|
8727
|
-
db.update(devices).set({ lastUsedAt: Date.now() }).where(eq(devices.hash, device)).run();
|
|
8728
|
-
}
|
|
8729
|
-
function hash(secret) {
|
|
8730
|
-
return createHash("sha256").update(secret).digest("hex");
|
|
8731
|
-
}
|
|
8732
8892
|
// src/main.ts
|
|
8733
8893
|
const UNPAIR_CHECK = 5e3;
|
|
8734
8894
|
const TICKET_LIFETIME = 6e4;
|
|
@@ -8794,7 +8954,25 @@ const publicDir = path.join(import.meta.dirname, "..", "..", "client", "dist");
|
|
|
8794
8954
|
const code = args.auth && args.pair && !hasDevices() ? addCode(null) : null;
|
|
8795
8955
|
const tickets = /* @__PURE__ */ new Map();
|
|
8796
8956
|
const server = createServer((request, response) => {
|
|
8797
|
-
|
|
8957
|
+
try {
|
|
8958
|
+
answerRequest(request, response);
|
|
8959
|
+
} catch (error) {
|
|
8960
|
+
console.warn(`a request failed: ${error}`);
|
|
8961
|
+
if (response.headersSent) {
|
|
8962
|
+
response.destroy();
|
|
8963
|
+
return;
|
|
8964
|
+
}
|
|
8965
|
+
response.statusCode = 500;
|
|
8966
|
+
response.end("request failed");
|
|
8967
|
+
}
|
|
8968
|
+
});
|
|
8969
|
+
function answerRequest(request, response) {
|
|
8970
|
+
const url = URL.parse(request.url ?? "/", "http://localhost");
|
|
8971
|
+
if (url === null) {
|
|
8972
|
+
response.statusCode = 400;
|
|
8973
|
+
response.end("bad url");
|
|
8974
|
+
return;
|
|
8975
|
+
}
|
|
8798
8976
|
if (url.pathname === "/api/media") {
|
|
8799
8977
|
serveMedia(url.searchParams.get("token"), request, response);
|
|
8800
8978
|
return;
|
|
@@ -8821,25 +8999,30 @@ const server = createServer((request, response) => {
|
|
|
8821
8999
|
return;
|
|
8822
9000
|
}
|
|
8823
9001
|
serveFile(url.pathname, request, response);
|
|
8824
|
-
}
|
|
9002
|
+
}
|
|
8825
9003
|
const sockets = new WebSocketServer({
|
|
8826
9004
|
noServer: true,
|
|
8827
9005
|
perMessageDeflate: true
|
|
8828
9006
|
});
|
|
8829
9007
|
const paired = /* @__PURE__ */ new Map();
|
|
8830
9008
|
server.on("upgrade", (request, socket, head) => {
|
|
8831
|
-
const url =
|
|
9009
|
+
const url = URL.parse(request.url ?? "/", "http://localhost");
|
|
9010
|
+
if (url === null) {
|
|
9011
|
+
socket.destroy();
|
|
9012
|
+
return;
|
|
9013
|
+
}
|
|
8832
9014
|
if (url.pathname.startsWith("/api/proxy/")) {
|
|
8833
9015
|
proxySocket(request, socket, head);
|
|
8834
9016
|
return;
|
|
8835
9017
|
}
|
|
8836
9018
|
const device = args.auth ? redeemTicket(url.searchParams.get("ticket")) : null;
|
|
8837
|
-
|
|
9019
|
+
const remoteAddress = request.socket.remoteAddress;
|
|
9020
|
+
if (url.pathname !== "/ws" || remoteAddress === void 0 || args.auth && device === null) {
|
|
8838
9021
|
socket.destroy();
|
|
8839
9022
|
return;
|
|
8840
9023
|
}
|
|
8841
9024
|
sockets.handleUpgrade(request, socket, head, (client) => {
|
|
8842
|
-
connectClient(client);
|
|
9025
|
+
connectClient(client, device, remoteAddress);
|
|
8843
9026
|
if (device === null) return;
|
|
8844
9027
|
paired.set(client, device);
|
|
8845
9028
|
client.on("close", () => paired.delete(client));
|
|
@@ -8948,7 +9131,7 @@ function serveBrowse(pathname, request, response) {
|
|
|
8948
9131
|
}
|
|
8949
9132
|
const rest = pathname.slice(12);
|
|
8950
9133
|
const slash = rest.indexOf("/");
|
|
8951
|
-
if (slash === -1 || rest.slice(0, slash)
|
|
9134
|
+
if (slash === -1 || !canBrowse(rest.slice(0, slash), request.socket.remoteAddress)) {
|
|
8952
9135
|
response.statusCode = 404;
|
|
8953
9136
|
response.end("This browser link expired");
|
|
8954
9137
|
return;
|
|
@@ -8961,13 +9144,23 @@ function serveBrowse(pathname, request, response) {
|
|
|
8961
9144
|
response.end("bad path");
|
|
8962
9145
|
return;
|
|
8963
9146
|
}
|
|
8964
|
-
|
|
8965
|
-
|
|
8966
|
-
|
|
8967
|
-
|
|
9147
|
+
let stats;
|
|
9148
|
+
let contentType;
|
|
9149
|
+
try {
|
|
9150
|
+
stats = statSync(file, { throwIfNoEntry: false });
|
|
9151
|
+
if (stats === void 0 || !stats.isFile()) {
|
|
9152
|
+
response.statusCode = 404;
|
|
9153
|
+
response.end(`${file} is not a file`);
|
|
9154
|
+
return;
|
|
9155
|
+
}
|
|
9156
|
+
accessSync(file, constants.R_OK);
|
|
9157
|
+
contentType = browseTypes.get(path.extname(file).toLowerCase()) ?? sniffType(file);
|
|
9158
|
+
} catch (error) {
|
|
9159
|
+
response.statusCode = 500;
|
|
9160
|
+
response.end(`Could not read ${file}: ${error}`);
|
|
8968
9161
|
return;
|
|
8969
9162
|
}
|
|
8970
|
-
setBrowseHeaders(response,
|
|
9163
|
+
setBrowseHeaders(response, contentType);
|
|
8971
9164
|
streamFile(file, stats.size, request, response);
|
|
8972
9165
|
}
|
|
8973
9166
|
async function answerOpen(request, response) {
|
|
@@ -8998,43 +9191,6 @@ async function answerOpen(request, response) {
|
|
|
8998
9191
|
response.statusCode = 204;
|
|
8999
9192
|
response.end();
|
|
9000
9193
|
}
|
|
9001
|
-
function streamFile(file, size, request, response) {
|
|
9002
|
-
response.setHeader("accept-ranges", "bytes");
|
|
9003
|
-
if (request.headers.range === void 0) {
|
|
9004
|
-
response.setHeader("content-length", size);
|
|
9005
|
-
createReadStream(file).on("error", () => response.destroy()).pipe(response);
|
|
9006
|
-
return;
|
|
9007
|
-
}
|
|
9008
|
-
const range = byteRange(request.headers.range, size);
|
|
9009
|
-
if (range === null) {
|
|
9010
|
-
response.statusCode = 416;
|
|
9011
|
-
response.setHeader("content-range", `bytes */${size}`);
|
|
9012
|
-
response.end();
|
|
9013
|
-
return;
|
|
9014
|
-
}
|
|
9015
|
-
response.statusCode = 206;
|
|
9016
|
-
response.setHeader("content-range", `bytes ${range.start}-${range.end}/${size}`);
|
|
9017
|
-
response.setHeader("content-length", range.end - range.start + 1);
|
|
9018
|
-
createReadStream(file, range).on("error", () => response.destroy()).pipe(response);
|
|
9019
|
-
}
|
|
9020
|
-
function byteRange(header, size) {
|
|
9021
|
-
const match = /^bytes=(?:(\d+)-(\d*)|-(\d+))$/.exec(header);
|
|
9022
|
-
if (match === null) return null;
|
|
9023
|
-
if (match[3] !== void 0) {
|
|
9024
|
-
if (size === 0 || Number(match[3]) === 0) return null;
|
|
9025
|
-
return {
|
|
9026
|
-
start: Math.max(size - Number(match[3]), 0),
|
|
9027
|
-
end: size - 1
|
|
9028
|
-
};
|
|
9029
|
-
}
|
|
9030
|
-
const start = Number(match[1]);
|
|
9031
|
-
const end = match[2] === "" ? size - 1 : Math.min(Number(match[2]), size - 1);
|
|
9032
|
-
if (start > end) return null;
|
|
9033
|
-
return {
|
|
9034
|
-
start,
|
|
9035
|
-
end
|
|
9036
|
-
};
|
|
9037
|
-
}
|
|
9038
9194
|
function answerJson(response, body) {
|
|
9039
9195
|
response.setHeader("content-type", "application/json");
|
|
9040
9196
|
response.end(JSON.stringify(body));
|