mcp-scraper 0.37.0 → 0.38.0
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/README.md +3 -2
- package/dist/bin/api-server.cjs +631 -9
- package/dist/bin/api-server.cjs.map +1 -1
- package/dist/bin/api-server.js +1 -1
- package/dist/bin/mcp-scraper-cli.cjs +1 -1
- package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
- package/dist/bin/mcp-scraper-cli.js +1 -1
- package/dist/bin/mcp-scraper-install.cjs +2 -2
- package/dist/bin/mcp-scraper-install.cjs.map +1 -1
- package/dist/bin/mcp-scraper-install.js +2 -2
- package/dist/bin/mcp-stdio-server.cjs +122 -2
- package/dist/bin/mcp-stdio-server.cjs.map +1 -1
- package/dist/bin/mcp-stdio-server.js +3 -3
- package/dist/chunk-H3M3O457.js +7 -0
- package/dist/chunk-H3M3O457.js.map +1 -0
- package/dist/{chunk-G7KAVJ3F.js → chunk-NNW3O6ZD.js} +2 -2
- package/dist/{chunk-G7KAVJ3F.js.map → chunk-NNW3O6ZD.js.map} +1 -1
- package/dist/{chunk-JK2FRDAP.js → chunk-P64PZUCA.js} +122 -2
- package/dist/chunk-P64PZUCA.js.map +1 -0
- package/dist/{server-QKDDEBVQ.js → server-3Q6XIJPF.js} +498 -4
- package/dist/server-3Q6XIJPF.js.map +1 -0
- package/docs/mcp-tool-craft-lint.generated.md +5 -3
- package/docs/mcp-tool-manifest.generated.json +205 -3
- package/package.json +4 -2
- package/dist/chunk-3LWYPAU5.js +0 -7
- package/dist/chunk-3LWYPAU5.js.map +0 -1
- package/dist/chunk-JK2FRDAP.js.map +0 -1
- package/dist/server-QKDDEBVQ.js.map +0 -1
|
@@ -56,7 +56,7 @@ import {
|
|
|
56
56
|
sanitizeAttempts,
|
|
57
57
|
sanitizeHarvestResult,
|
|
58
58
|
transcribeMediaUrl
|
|
59
|
-
} from "./chunk-
|
|
59
|
+
} from "./chunk-P64PZUCA.js";
|
|
60
60
|
import {
|
|
61
61
|
auditImageUrls,
|
|
62
62
|
auditImages,
|
|
@@ -101,7 +101,7 @@ import {
|
|
|
101
101
|
RawMapsOverviewSchema,
|
|
102
102
|
RawMapsReviewStatsSchema
|
|
103
103
|
} from "./chunk-CB5C3BPB.js";
|
|
104
|
-
import "./chunk-
|
|
104
|
+
import "./chunk-H3M3O457.js";
|
|
105
105
|
import {
|
|
106
106
|
abandonExtractSettlement,
|
|
107
107
|
countSuccessfulPages,
|
|
@@ -14026,6 +14026,22 @@ var DiffPageBodySchema = z15.object({
|
|
|
14026
14026
|
allowLocal: z15.boolean().optional(),
|
|
14027
14027
|
resetBaseline: z15.boolean().optional()
|
|
14028
14028
|
});
|
|
14029
|
+
var ArchiveReadBodySchema = z15.object({
|
|
14030
|
+
url: z15.string().url("url must be a valid HTTPS ZIP URL"),
|
|
14031
|
+
path: z15.string().trim().min(1).max(2e3).optional(),
|
|
14032
|
+
offset: z15.number().int().min(0).optional(),
|
|
14033
|
+
maxBytes: z15.number().int().min(1).max(2e5).optional(),
|
|
14034
|
+
maxEntries: z15.number().int().min(1).max(1e3).optional(),
|
|
14035
|
+
depositToLibrary: z15.boolean().optional()
|
|
14036
|
+
}).superRefine((value, ctx) => {
|
|
14037
|
+
if (value.depositToLibrary && !value.path) {
|
|
14038
|
+
ctx.addIssue({
|
|
14039
|
+
code: z15.ZodIssueCode.custom,
|
|
14040
|
+
path: ["path"],
|
|
14041
|
+
message: "path is required when depositToLibrary is true"
|
|
14042
|
+
});
|
|
14043
|
+
}
|
|
14044
|
+
});
|
|
14029
14045
|
var MapUrlsBodySchema = z15.object({
|
|
14030
14046
|
url: z15.string().min(1, "url is required"),
|
|
14031
14047
|
maxUrls: z15.number().int().min(1).max(2e3).optional(),
|
|
@@ -26845,7 +26861,14 @@ async function depositScrapeToVault(user, opts) {
|
|
|
26845
26861
|
const title = (opts.title?.trim() || opts.source).slice(0, 200);
|
|
26846
26862
|
const res = await memoryCall(
|
|
26847
26863
|
"libraryIngestTool",
|
|
26848
|
-
{
|
|
26864
|
+
{
|
|
26865
|
+
title,
|
|
26866
|
+
content: clipped,
|
|
26867
|
+
source: opts.source,
|
|
26868
|
+
vault,
|
|
26869
|
+
...opts.capturedAt ? { capturedAt: opts.capturedAt } : {},
|
|
26870
|
+
...opts.summary ? { summary: opts.summary } : {}
|
|
26871
|
+
},
|
|
26849
26872
|
key
|
|
26850
26873
|
);
|
|
26851
26874
|
if (!res.ok) return { deposited: false, vault, error: res.error ?? "ingest failed" };
|
|
@@ -26878,6 +26901,414 @@ async function persistScrapeBody(user, opts) {
|
|
|
26878
26901
|
return { ...deposit, ...fallback };
|
|
26879
26902
|
}
|
|
26880
26903
|
|
|
26904
|
+
// src/api/archive-reader.ts
|
|
26905
|
+
import { basename, extname } from "path";
|
|
26906
|
+
import yauzl from "yauzl";
|
|
26907
|
+
var MAX_ARCHIVE_DOWNLOAD_BYTES = 50 * 1024 * 1024;
|
|
26908
|
+
var MAX_ARCHIVE_ENTRIES = 1e4;
|
|
26909
|
+
var MAX_ARCHIVE_EXPANDED_BYTES = 250 * 1024 * 1024;
|
|
26910
|
+
var MAX_ARCHIVE_ENTRY_BYTES = 10 * 1024 * 1024;
|
|
26911
|
+
var MAX_LIBRARY_ENTRY_BYTES = 5 * 1024 * 1024;
|
|
26912
|
+
var MAX_COMPRESSION_RATIO = 200;
|
|
26913
|
+
var MAX_ARCHIVE_REDIRECTS = 4;
|
|
26914
|
+
var READABLE_EXTENSIONS = /* @__PURE__ */ new Map([
|
|
26915
|
+
[".txt", "text/plain"],
|
|
26916
|
+
[".md", "text/markdown"],
|
|
26917
|
+
[".markdown", "text/markdown"],
|
|
26918
|
+
[".json", "application/json"],
|
|
26919
|
+
[".jsonl", "application/x-ndjson"],
|
|
26920
|
+
[".ndjson", "application/x-ndjson"],
|
|
26921
|
+
[".csv", "text/csv"],
|
|
26922
|
+
[".tsv", "text/tab-separated-values"],
|
|
26923
|
+
[".html", "text/html"],
|
|
26924
|
+
[".htm", "text/html"],
|
|
26925
|
+
[".xml", "application/xml"],
|
|
26926
|
+
[".yaml", "application/yaml"],
|
|
26927
|
+
[".yml", "application/yaml"],
|
|
26928
|
+
[".css", "text/css"],
|
|
26929
|
+
[".js", "text/javascript"],
|
|
26930
|
+
[".mjs", "text/javascript"],
|
|
26931
|
+
[".cjs", "text/javascript"],
|
|
26932
|
+
[".ts", "text/typescript"],
|
|
26933
|
+
[".tsx", "text/typescript"],
|
|
26934
|
+
[".jsx", "text/javascript"],
|
|
26935
|
+
[".log", "text/plain"],
|
|
26936
|
+
[".toml", "text/plain"],
|
|
26937
|
+
[".ini", "text/plain"],
|
|
26938
|
+
[".conf", "text/plain"],
|
|
26939
|
+
[".sql", "text/plain"],
|
|
26940
|
+
[".py", "text/x-python"],
|
|
26941
|
+
[".rb", "text/plain"],
|
|
26942
|
+
[".go", "text/plain"],
|
|
26943
|
+
[".java", "text/plain"],
|
|
26944
|
+
[".sh", "text/x-shellscript"],
|
|
26945
|
+
[".graphql", "text/plain"]
|
|
26946
|
+
]);
|
|
26947
|
+
var READABLE_FILENAMES = /* @__PURE__ */ new Set(["readme", "license", "changelog", "makefile", "dockerfile", "gemfile"]);
|
|
26948
|
+
var ArchiveReadError = class extends Error {
|
|
26949
|
+
constructor(code, message, status = 400, retryable = false) {
|
|
26950
|
+
super(message);
|
|
26951
|
+
this.code = code;
|
|
26952
|
+
this.status = status;
|
|
26953
|
+
this.retryable = retryable;
|
|
26954
|
+
}
|
|
26955
|
+
code;
|
|
26956
|
+
status;
|
|
26957
|
+
retryable;
|
|
26958
|
+
};
|
|
26959
|
+
function readableContentType(path5) {
|
|
26960
|
+
const extensionType = READABLE_EXTENSIONS.get(extname(path5).toLowerCase());
|
|
26961
|
+
if (extensionType) return extensionType;
|
|
26962
|
+
return READABLE_FILENAMES.has(basename(path5).toLowerCase()) ? "text/plain" : null;
|
|
26963
|
+
}
|
|
26964
|
+
function safeEntryPath(path5) {
|
|
26965
|
+
const normalized = path5.replaceAll("\\", "/");
|
|
26966
|
+
if (!normalized || /[\u0000-\u001f\u007f]/.test(normalized) || normalized.startsWith("/") || /^[a-zA-Z]:\//.test(normalized) || normalized.split("/").some((segment) => segment === "..")) {
|
|
26967
|
+
throw new ArchiveReadError("archive_unsafe_path", `ZIP entry has an unsafe path: ${path5}`);
|
|
26968
|
+
}
|
|
26969
|
+
return normalized;
|
|
26970
|
+
}
|
|
26971
|
+
function entryUnixMode(entry) {
|
|
26972
|
+
return entry.externalFileAttributes >>> 16 & 65535;
|
|
26973
|
+
}
|
|
26974
|
+
function isSymlink(entry) {
|
|
26975
|
+
return (entryUnixMode(entry) & 61440) === 40960;
|
|
26976
|
+
}
|
|
26977
|
+
function entryModifiedAt(entry) {
|
|
26978
|
+
try {
|
|
26979
|
+
const value = entry.getLastModDate();
|
|
26980
|
+
return Number.isFinite(value.getTime()) ? value.toISOString() : null;
|
|
26981
|
+
} catch {
|
|
26982
|
+
return null;
|
|
26983
|
+
}
|
|
26984
|
+
}
|
|
26985
|
+
function openZip(buffer) {
|
|
26986
|
+
return new Promise((resolve, reject) => {
|
|
26987
|
+
yauzl.fromBuffer(buffer, {
|
|
26988
|
+
lazyEntries: true,
|
|
26989
|
+
decodeStrings: true,
|
|
26990
|
+
validateEntrySizes: true,
|
|
26991
|
+
strictFileNames: true
|
|
26992
|
+
}, (error, zip) => {
|
|
26993
|
+
if (error || !zip) {
|
|
26994
|
+
reject(new ArchiveReadError("archive_invalid_zip", "The downloaded file is not a valid ZIP archive."));
|
|
26995
|
+
return;
|
|
26996
|
+
}
|
|
26997
|
+
resolve(zip);
|
|
26998
|
+
});
|
|
26999
|
+
});
|
|
27000
|
+
}
|
|
27001
|
+
async function scanZip(buffer, visibleEntryLimit) {
|
|
27002
|
+
const zip = await openZip(buffer);
|
|
27003
|
+
return new Promise((resolve, reject) => {
|
|
27004
|
+
const entries = [];
|
|
27005
|
+
const allEntries = [];
|
|
27006
|
+
let totalUncompressedBytes = 0;
|
|
27007
|
+
let settled = false;
|
|
27008
|
+
const fail2 = (error) => {
|
|
27009
|
+
if (settled) return;
|
|
27010
|
+
settled = true;
|
|
27011
|
+
try {
|
|
27012
|
+
zip.close();
|
|
27013
|
+
} catch {
|
|
27014
|
+
}
|
|
27015
|
+
reject(error instanceof ArchiveReadError ? error : new ArchiveReadError("archive_invalid_zip", error instanceof Error ? error.message : "Failed to read ZIP archive."));
|
|
27016
|
+
};
|
|
27017
|
+
zip.on("error", fail2);
|
|
27018
|
+
zip.on("entry", (entry) => {
|
|
27019
|
+
try {
|
|
27020
|
+
if (allEntries.length >= MAX_ARCHIVE_ENTRIES) {
|
|
27021
|
+
fail2(new ArchiveReadError("archive_entry_limit", `ZIP archive exceeds the ${MAX_ARCHIVE_ENTRIES.toLocaleString()} entry limit.`));
|
|
27022
|
+
return;
|
|
27023
|
+
}
|
|
27024
|
+
const path5 = safeEntryPath(entry.fileName);
|
|
27025
|
+
if (entry.isEncrypted()) {
|
|
27026
|
+
fail2(new ArchiveReadError("archive_encrypted_entry", `Encrypted ZIP entries are not supported: ${path5}`));
|
|
27027
|
+
return;
|
|
27028
|
+
}
|
|
27029
|
+
if (isSymlink(entry)) {
|
|
27030
|
+
fail2(new ArchiveReadError("archive_symlink_entry", `Symbolic-link ZIP entries are not supported: ${path5}`));
|
|
27031
|
+
return;
|
|
27032
|
+
}
|
|
27033
|
+
const directory = path5.endsWith("/");
|
|
27034
|
+
totalUncompressedBytes += entry.uncompressedSize;
|
|
27035
|
+
if (totalUncompressedBytes > MAX_ARCHIVE_EXPANDED_BYTES) {
|
|
27036
|
+
fail2(new ArchiveReadError("archive_expanded_size_limit", `ZIP archive exceeds the ${Math.round(MAX_ARCHIVE_EXPANDED_BYTES / 1024 / 1024)} MB expanded-size limit.`));
|
|
27037
|
+
return;
|
|
27038
|
+
}
|
|
27039
|
+
const ratio = entry.compressedSize > 0 ? entry.uncompressedSize / entry.compressedSize : entry.uncompressedSize === 0 ? 1 : Number.POSITIVE_INFINITY;
|
|
27040
|
+
if (!directory && entry.uncompressedSize > 1024 * 1024 && ratio > MAX_COMPRESSION_RATIO) {
|
|
27041
|
+
fail2(new ArchiveReadError("archive_compression_ratio_limit", `ZIP entry exceeds the ${MAX_COMPRESSION_RATIO}:1 compression-ratio limit: ${path5}`));
|
|
27042
|
+
return;
|
|
27043
|
+
}
|
|
27044
|
+
const contentType = directory ? null : readableContentType(path5);
|
|
27045
|
+
const supportedCompression = entry.compressionMethod === 0 || entry.compressionMethod === 8;
|
|
27046
|
+
const info = {
|
|
27047
|
+
path: path5,
|
|
27048
|
+
directory,
|
|
27049
|
+
compressedBytes: entry.compressedSize,
|
|
27050
|
+
uncompressedBytes: entry.uncompressedSize,
|
|
27051
|
+
contentType,
|
|
27052
|
+
readable: contentType !== null && supportedCompression && entry.uncompressedSize <= MAX_ARCHIVE_ENTRY_BYTES,
|
|
27053
|
+
modifiedAt: entryModifiedAt(entry)
|
|
27054
|
+
};
|
|
27055
|
+
allEntries.push(info);
|
|
27056
|
+
if (entries.length < visibleEntryLimit) entries.push(info);
|
|
27057
|
+
zip.readEntry();
|
|
27058
|
+
} catch (error) {
|
|
27059
|
+
fail2(error);
|
|
27060
|
+
}
|
|
27061
|
+
});
|
|
27062
|
+
zip.on("end", () => {
|
|
27063
|
+
if (settled) return;
|
|
27064
|
+
settled = true;
|
|
27065
|
+
resolve({ entries, allEntries, totalUncompressedBytes });
|
|
27066
|
+
});
|
|
27067
|
+
zip.readEntry();
|
|
27068
|
+
});
|
|
27069
|
+
}
|
|
27070
|
+
function openEntryStream(zip, entry) {
|
|
27071
|
+
return new Promise((resolve, reject) => {
|
|
27072
|
+
zip.openReadStream(entry, (error, stream) => {
|
|
27073
|
+
if (error || !stream) {
|
|
27074
|
+
reject(new ArchiveReadError("archive_entry_read_failed", error?.message ?? "Failed to open ZIP entry."));
|
|
27075
|
+
return;
|
|
27076
|
+
}
|
|
27077
|
+
resolve(stream);
|
|
27078
|
+
});
|
|
27079
|
+
});
|
|
27080
|
+
}
|
|
27081
|
+
async function collectEntry(stream, expectedBytes) {
|
|
27082
|
+
const chunks = [];
|
|
27083
|
+
let bytes = 0;
|
|
27084
|
+
for await (const chunk of stream) {
|
|
27085
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
27086
|
+
bytes += buffer.length;
|
|
27087
|
+
if (bytes > MAX_ARCHIVE_ENTRY_BYTES) {
|
|
27088
|
+
stream.destroy();
|
|
27089
|
+
throw new ArchiveReadError("archive_entry_size_limit", `ZIP entry exceeds the ${Math.round(MAX_ARCHIVE_ENTRY_BYTES / 1024 / 1024)} MB readable-size limit.`);
|
|
27090
|
+
}
|
|
27091
|
+
chunks.push(buffer);
|
|
27092
|
+
}
|
|
27093
|
+
if (bytes !== expectedBytes) {
|
|
27094
|
+
throw new ArchiveReadError("archive_entry_size_mismatch", "ZIP entry expanded to a different size than declared.");
|
|
27095
|
+
}
|
|
27096
|
+
return Buffer.concat(chunks, bytes);
|
|
27097
|
+
}
|
|
27098
|
+
async function extractEntry(buffer, requestedPath) {
|
|
27099
|
+
const zip = await openZip(buffer);
|
|
27100
|
+
return new Promise((resolve, reject) => {
|
|
27101
|
+
let settled = false;
|
|
27102
|
+
const fail2 = (error) => {
|
|
27103
|
+
if (settled) return;
|
|
27104
|
+
settled = true;
|
|
27105
|
+
try {
|
|
27106
|
+
zip.close();
|
|
27107
|
+
} catch {
|
|
27108
|
+
}
|
|
27109
|
+
reject(error instanceof ArchiveReadError ? error : new ArchiveReadError("archive_entry_read_failed", error instanceof Error ? error.message : "Failed to read ZIP entry."));
|
|
27110
|
+
};
|
|
27111
|
+
zip.on("error", fail2);
|
|
27112
|
+
zip.on("entry", (entry) => {
|
|
27113
|
+
let path5;
|
|
27114
|
+
try {
|
|
27115
|
+
path5 = safeEntryPath(entry.fileName);
|
|
27116
|
+
} catch (error) {
|
|
27117
|
+
fail2(error);
|
|
27118
|
+
return;
|
|
27119
|
+
}
|
|
27120
|
+
if (path5 !== requestedPath) {
|
|
27121
|
+
zip.readEntry();
|
|
27122
|
+
return;
|
|
27123
|
+
}
|
|
27124
|
+
if (path5.endsWith("/")) {
|
|
27125
|
+
fail2(new ArchiveReadError("archive_entry_is_directory", `ZIP entry is a directory: ${path5}`));
|
|
27126
|
+
return;
|
|
27127
|
+
}
|
|
27128
|
+
if (entry.uncompressedSize > MAX_ARCHIVE_ENTRY_BYTES) {
|
|
27129
|
+
fail2(new ArchiveReadError("archive_entry_size_limit", `ZIP entry exceeds the ${Math.round(MAX_ARCHIVE_ENTRY_BYTES / 1024 / 1024)} MB readable-size limit.`));
|
|
27130
|
+
return;
|
|
27131
|
+
}
|
|
27132
|
+
if (!readableContentType(path5)) {
|
|
27133
|
+
fail2(new ArchiveReadError("archive_binary_entry", `ZIP entry is not a supported text file: ${path5}`));
|
|
27134
|
+
return;
|
|
27135
|
+
}
|
|
27136
|
+
void openEntryStream(zip, entry).then((stream) => collectEntry(stream, entry.uncompressedSize)).then((content) => {
|
|
27137
|
+
if (settled) return;
|
|
27138
|
+
settled = true;
|
|
27139
|
+
try {
|
|
27140
|
+
zip.close();
|
|
27141
|
+
} catch {
|
|
27142
|
+
}
|
|
27143
|
+
resolve({ entry, content });
|
|
27144
|
+
}).catch(fail2);
|
|
27145
|
+
});
|
|
27146
|
+
zip.on("end", () => fail2(new ArchiveReadError("archive_entry_not_found", `ZIP entry was not found: ${requestedPath}`, 404)));
|
|
27147
|
+
zip.readEntry();
|
|
27148
|
+
});
|
|
27149
|
+
}
|
|
27150
|
+
async function boundedResponseBuffer(response) {
|
|
27151
|
+
const declaredLength = Number(response.headers.get("content-length"));
|
|
27152
|
+
if (Number.isFinite(declaredLength) && declaredLength > MAX_ARCHIVE_DOWNLOAD_BYTES) {
|
|
27153
|
+
throw new ArchiveReadError("archive_download_size_limit", `ZIP download exceeds the ${Math.round(MAX_ARCHIVE_DOWNLOAD_BYTES / 1024 / 1024)} MB limit.`);
|
|
27154
|
+
}
|
|
27155
|
+
if (!response.body) throw new ArchiveReadError("archive_empty_response", "ZIP download returned no response body.", 502, true);
|
|
27156
|
+
const reader = response.body.getReader();
|
|
27157
|
+
const chunks = [];
|
|
27158
|
+
let bytes = 0;
|
|
27159
|
+
try {
|
|
27160
|
+
for (; ; ) {
|
|
27161
|
+
const { done, value } = await reader.read();
|
|
27162
|
+
if (done) break;
|
|
27163
|
+
if (!value) continue;
|
|
27164
|
+
bytes += value.byteLength;
|
|
27165
|
+
if (bytes > MAX_ARCHIVE_DOWNLOAD_BYTES) {
|
|
27166
|
+
await reader.cancel();
|
|
27167
|
+
throw new ArchiveReadError("archive_download_size_limit", `ZIP download exceeds the ${Math.round(MAX_ARCHIVE_DOWNLOAD_BYTES / 1024 / 1024)} MB limit.`);
|
|
27168
|
+
}
|
|
27169
|
+
chunks.push(value);
|
|
27170
|
+
}
|
|
27171
|
+
} finally {
|
|
27172
|
+
reader.releaseLock();
|
|
27173
|
+
}
|
|
27174
|
+
const buffer = Buffer.concat(chunks.map((chunk) => Buffer.from(chunk)), bytes);
|
|
27175
|
+
if (buffer.length < 4 || buffer[0] !== 80 || buffer[1] !== 75) {
|
|
27176
|
+
throw new ArchiveReadError("archive_invalid_zip", "The downloaded file is not a ZIP archive.");
|
|
27177
|
+
}
|
|
27178
|
+
return buffer;
|
|
27179
|
+
}
|
|
27180
|
+
async function downloadPublicZip(rawUrl) {
|
|
27181
|
+
let currentUrl = rawUrl;
|
|
27182
|
+
for (let redirectCount = 0; redirectCount <= MAX_ARCHIVE_REDIRECTS; redirectCount++) {
|
|
27183
|
+
const checked = await validatePublicHttpUrl(currentUrl, { field: "archive URL", requireHttps: true });
|
|
27184
|
+
if (checked.error || !checked.parsed) {
|
|
27185
|
+
throw new ArchiveReadError("archive_url_not_public", checked.error ?? "Invalid archive URL.");
|
|
27186
|
+
}
|
|
27187
|
+
if (checked.parsed.username || checked.parsed.password) {
|
|
27188
|
+
throw new ArchiveReadError("archive_url_credentials_forbidden", "Archive URL must not contain embedded credentials.");
|
|
27189
|
+
}
|
|
27190
|
+
const response = await fetch(checked.parsed.href, {
|
|
27191
|
+
method: "GET",
|
|
27192
|
+
redirect: "manual",
|
|
27193
|
+
headers: { accept: "application/zip, application/octet-stream;q=0.9" },
|
|
27194
|
+
signal: AbortSignal.timeout(6e4)
|
|
27195
|
+
});
|
|
27196
|
+
if ([301, 302, 303, 307, 308].includes(response.status)) {
|
|
27197
|
+
const location2 = response.headers.get("location");
|
|
27198
|
+
try {
|
|
27199
|
+
await response.body?.cancel();
|
|
27200
|
+
} catch {
|
|
27201
|
+
}
|
|
27202
|
+
if (!location2) throw new ArchiveReadError("archive_redirect_missing_location", "Archive server returned a redirect without a location.", 502, true);
|
|
27203
|
+
currentUrl = new URL(location2, checked.parsed).href;
|
|
27204
|
+
continue;
|
|
27205
|
+
}
|
|
27206
|
+
if (!response.ok) {
|
|
27207
|
+
try {
|
|
27208
|
+
await response.body?.cancel();
|
|
27209
|
+
} catch {
|
|
27210
|
+
}
|
|
27211
|
+
throw new ArchiveReadError(
|
|
27212
|
+
"archive_download_failed",
|
|
27213
|
+
`Archive download returned HTTP ${response.status}.`,
|
|
27214
|
+
response.status >= 500 ? 502 : 400,
|
|
27215
|
+
response.status >= 500 || response.status === 429
|
|
27216
|
+
);
|
|
27217
|
+
}
|
|
27218
|
+
return { archiveUrl: checked.parsed.href, buffer: await boundedResponseBuffer(response) };
|
|
27219
|
+
}
|
|
27220
|
+
throw new ArchiveReadError("archive_redirect_limit", `Archive download exceeded ${MAX_ARCHIVE_REDIRECTS} redirects.`);
|
|
27221
|
+
}
|
|
27222
|
+
async function listZipArchive(rawUrl, maxEntries) {
|
|
27223
|
+
const { archiveUrl, buffer } = await downloadPublicZip(rawUrl);
|
|
27224
|
+
const scanned = await scanZip(buffer, maxEntries);
|
|
27225
|
+
return {
|
|
27226
|
+
archiveUrl,
|
|
27227
|
+
compressedBytes: buffer.length,
|
|
27228
|
+
entryCount: scanned.allEntries.length,
|
|
27229
|
+
totalUncompressedBytes: scanned.totalUncompressedBytes,
|
|
27230
|
+
entries: scanned.entries,
|
|
27231
|
+
entriesTruncated: scanned.allEntries.length > scanned.entries.length
|
|
27232
|
+
};
|
|
27233
|
+
}
|
|
27234
|
+
function decodeUtf8(content, path5) {
|
|
27235
|
+
try {
|
|
27236
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(content);
|
|
27237
|
+
} catch {
|
|
27238
|
+
throw new ArchiveReadError("archive_text_decode_failed", `ZIP entry is not valid UTF-8 text: ${path5}`);
|
|
27239
|
+
}
|
|
27240
|
+
}
|
|
27241
|
+
function utf8Window(content, path5, offset, maxBytes) {
|
|
27242
|
+
const start = Math.min(offset, content.length);
|
|
27243
|
+
if (start < content.length && (content[start] & 192) === 128) {
|
|
27244
|
+
throw new ArchiveReadError(
|
|
27245
|
+
"archive_offset_invalid",
|
|
27246
|
+
`Byte offset ${start} falls inside a UTF-8 character in ZIP entry: ${path5}`
|
|
27247
|
+
);
|
|
27248
|
+
}
|
|
27249
|
+
let end = Math.min(content.length, start + maxBytes);
|
|
27250
|
+
while (end > start && end < content.length && (content[end] & 192) === 128) end--;
|
|
27251
|
+
if (end === start && start < content.length) {
|
|
27252
|
+
throw new ArchiveReadError(
|
|
27253
|
+
"archive_window_too_small",
|
|
27254
|
+
`maxBytes is too small for the next UTF-8 character in ZIP entry: ${path5}`
|
|
27255
|
+
);
|
|
27256
|
+
}
|
|
27257
|
+
return {
|
|
27258
|
+
content: content.subarray(start, end).toString("utf8"),
|
|
27259
|
+
offset: start,
|
|
27260
|
+
nextOffset: end < content.length ? end : null
|
|
27261
|
+
};
|
|
27262
|
+
}
|
|
27263
|
+
async function readZipArchiveFile(rawUrl, rawPath, offset, maxBytes) {
|
|
27264
|
+
const requestedPath = safeEntryPath(rawPath);
|
|
27265
|
+
const { archiveUrl, buffer } = await downloadPublicZip(rawUrl);
|
|
27266
|
+
const scanned = await scanZip(buffer, 0);
|
|
27267
|
+
const info = scanned.allEntries.find((entry) => entry.path === requestedPath);
|
|
27268
|
+
if (!info) throw new ArchiveReadError("archive_entry_not_found", `ZIP entry was not found: ${requestedPath}`, 404);
|
|
27269
|
+
if (!info.readable) {
|
|
27270
|
+
if (info.directory) throw new ArchiveReadError("archive_entry_is_directory", `ZIP entry is a directory: ${requestedPath}`);
|
|
27271
|
+
if (info.uncompressedBytes > MAX_ARCHIVE_ENTRY_BYTES) {
|
|
27272
|
+
throw new ArchiveReadError("archive_entry_size_limit", `ZIP entry exceeds the ${Math.round(MAX_ARCHIVE_ENTRY_BYTES / 1024 / 1024)} MB readable-size limit.`);
|
|
27273
|
+
}
|
|
27274
|
+
throw new ArchiveReadError("archive_binary_entry", `ZIP entry is not a supported text file: ${requestedPath}`);
|
|
27275
|
+
}
|
|
27276
|
+
const { content } = await extractEntry(buffer, requestedPath);
|
|
27277
|
+
const fullContent = decodeUtf8(content, requestedPath);
|
|
27278
|
+
const textBytes = Buffer.from(fullContent);
|
|
27279
|
+
const window2 = utf8Window(textBytes, requestedPath, offset, maxBytes);
|
|
27280
|
+
return {
|
|
27281
|
+
archiveUrl,
|
|
27282
|
+
compressedBytes: buffer.length,
|
|
27283
|
+
entryCount: scanned.allEntries.length,
|
|
27284
|
+
totalUncompressedBytes: scanned.totalUncompressedBytes,
|
|
27285
|
+
path: requestedPath,
|
|
27286
|
+
contentType: info.contentType,
|
|
27287
|
+
fileBytes: textBytes.length,
|
|
27288
|
+
offset: window2.offset,
|
|
27289
|
+
content: window2.content,
|
|
27290
|
+
nextOffset: window2.nextOffset,
|
|
27291
|
+
fullContent
|
|
27292
|
+
};
|
|
27293
|
+
}
|
|
27294
|
+
function archiveLibrarySource(archiveUrl, path5) {
|
|
27295
|
+
const source = new URL(archiveUrl);
|
|
27296
|
+
source.username = "";
|
|
27297
|
+
source.password = "";
|
|
27298
|
+
source.search = "";
|
|
27299
|
+
source.hash = `entry=${encodeURIComponent(path5)}`;
|
|
27300
|
+
return source.href;
|
|
27301
|
+
}
|
|
27302
|
+
function archiveEntryTitle(path5) {
|
|
27303
|
+
return basename(path5).slice(0, 200) || "Archive entry";
|
|
27304
|
+
}
|
|
27305
|
+
function inferredWaybackCapturedAt(content) {
|
|
27306
|
+
const timestamp = content.slice(0, 4e3).match(/^Archive timestamp:\s*(\d{14})\s*$/m)?.[1];
|
|
27307
|
+
if (!timestamp) return void 0;
|
|
27308
|
+
const iso = `${timestamp.slice(0, 4)}-${timestamp.slice(4, 6)}-${timestamp.slice(6, 8)}T${timestamp.slice(8, 10)}:${timestamp.slice(10, 12)}:${timestamp.slice(12, 14)}Z`;
|
|
27309
|
+
return Number.isFinite(Date.parse(iso)) ? iso : void 0;
|
|
27310
|
+
}
|
|
27311
|
+
|
|
26881
27312
|
// src/api/connection-memory-import.ts
|
|
26882
27313
|
import { createHash as createHash10 } from "crypto";
|
|
26883
27314
|
var CONNECTION_MEMORY_IMPORT_MAX_ARGS_BYTES = 64 * 1024;
|
|
@@ -31979,6 +32410,69 @@ app.post("/diff-page", auth2, async (c) => {
|
|
|
31979
32410
|
await releaseConcurrencyGate(gate.lockId);
|
|
31980
32411
|
}
|
|
31981
32412
|
});
|
|
32413
|
+
app.post("/archive/read", auth2, async (c) => {
|
|
32414
|
+
const raw = await c.req.json().catch(() => ({}));
|
|
32415
|
+
const bodyResult = ArchiveReadBodySchema.safeParse(raw);
|
|
32416
|
+
if (!bodyResult.success) {
|
|
32417
|
+
return c.json({
|
|
32418
|
+
error: "archive_invalid_request",
|
|
32419
|
+
error_code: "archive_invalid_request",
|
|
32420
|
+
retryable: false,
|
|
32421
|
+
message: bodyResult.error.issues[0]?.message ?? "Invalid request"
|
|
32422
|
+
}, 400);
|
|
32423
|
+
}
|
|
32424
|
+
const { url, path: path5, depositToLibrary } = bodyResult.data;
|
|
32425
|
+
const user = c.get("user");
|
|
32426
|
+
const gate = await acquireConcurrencyGate(user, "archive_read", {
|
|
32427
|
+
reuseLockId: c.req.header("x-mcp-scraper-concurrency-lock"),
|
|
32428
|
+
metadata: { url }
|
|
32429
|
+
});
|
|
32430
|
+
if (!gate.ok) return c.json(concurrencyLimitExceededResponse(gate), 429, { "Retry-After": String(gate.retryAfterSeconds) });
|
|
32431
|
+
try {
|
|
32432
|
+
if (!path5) {
|
|
32433
|
+
const inventory = await listZipArchive(url, bodyResult.data.maxEntries ?? 200);
|
|
32434
|
+
return c.json({ mode: "list", ...inventory });
|
|
32435
|
+
}
|
|
32436
|
+
const file = await readZipArchiveFile(
|
|
32437
|
+
url,
|
|
32438
|
+
path5,
|
|
32439
|
+
bodyResult.data.offset ?? 0,
|
|
32440
|
+
bodyResult.data.maxBytes ?? 5e4
|
|
32441
|
+
);
|
|
32442
|
+
if (depositToLibrary && file.fileBytes > MAX_LIBRARY_ENTRY_BYTES) {
|
|
32443
|
+
throw new ArchiveReadError(
|
|
32444
|
+
"archive_library_size_limit",
|
|
32445
|
+
`ZIP entry exceeds the ${Math.round(MAX_LIBRARY_ENTRY_BYTES / 1024 / 1024)} MB Library-ingest limit.`
|
|
32446
|
+
);
|
|
32447
|
+
}
|
|
32448
|
+
const librarySource = archiveLibrarySource(file.archiveUrl, file.path);
|
|
32449
|
+
const memory = depositToLibrary ? await depositScrapeToVault(user, {
|
|
32450
|
+
title: archiveEntryTitle(file.path),
|
|
32451
|
+
content: file.fullContent,
|
|
32452
|
+
source: librarySource,
|
|
32453
|
+
vault: "Library",
|
|
32454
|
+
capturedAt: inferredWaybackCapturedAt(file.fullContent),
|
|
32455
|
+
summary: `Source file \`${file.path}\` preserved from ZIP archive ${librarySource.split("#")[0]}.`
|
|
32456
|
+
}) : void 0;
|
|
32457
|
+
const { fullContent: _fullContent, ...responseFile } = file;
|
|
32458
|
+
return c.json({ mode: "read", ...responseFile, memory });
|
|
32459
|
+
} catch (error) {
|
|
32460
|
+
const known = error instanceof ArchiveReadError ? error : new ArchiveReadError(
|
|
32461
|
+
"archive_read_failed",
|
|
32462
|
+
error instanceof Error ? error.message : "Failed to read ZIP archive.",
|
|
32463
|
+
500,
|
|
32464
|
+
true
|
|
32465
|
+
);
|
|
32466
|
+
return c.json({
|
|
32467
|
+
error: known.code,
|
|
32468
|
+
error_code: known.code,
|
|
32469
|
+
retryable: known.retryable,
|
|
32470
|
+
message: known.message
|
|
32471
|
+
}, known.status);
|
|
32472
|
+
} finally {
|
|
32473
|
+
await releaseConcurrencyGate(gate.lockId);
|
|
32474
|
+
}
|
|
32475
|
+
});
|
|
31982
32476
|
app.post("/map-urls", auth2, async (c) => {
|
|
31983
32477
|
const raw = await c.req.json().catch(() => ({}));
|
|
31984
32478
|
const bodyResult = MapUrlsBodySchema.safeParse(raw);
|
|
@@ -32931,4 +33425,4 @@ app.get("/blog/:slug/", (c) => {
|
|
|
32931
33425
|
export {
|
|
32932
33426
|
app
|
|
32933
33427
|
};
|
|
32934
|
-
//# sourceMappingURL=server-
|
|
33428
|
+
//# sourceMappingURL=server-3Q6XIJPF.js.map
|