@topline-build/harness-mcp 0.5.2 → 0.6.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.
@@ -0,0 +1,146 @@
1
+ import { createHash } from "node:crypto";
2
+ import { mkdir, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { gunzipSync } from "node:zlib";
5
+ export const MAX_SOURCE_ARCHIVE_BYTES = 64 * 1024 * 1024;
6
+ const MAX_CONTENT_BYTES = 512 * 1024 * 1024;
7
+ const MAX_FILES = 25_000;
8
+ const BLOCK = 512;
9
+ const forbiddenDirs = new Set([".git", "node_modules", ".next", ".vite", ".turbo", ".cache", "dist", "build", "coverage"]);
10
+ /** All path rules apply on every host, including Windows aliases and devices. */
11
+ export function safeSourcePath(raw) {
12
+ if (!raw || raw.startsWith("/") || raw.includes("\\"))
13
+ throw new Error("artifact_source_path_invalid");
14
+ const segments = raw.replace(/^\.\//, "").split("/");
15
+ if (segments.at(-1) === "")
16
+ segments.pop();
17
+ if (!segments.length || segments.length > 64 || segments.some(part => !part || part === "." || part === ".." || /[\u0000-\u001f\u007f:]/u.test(part) || /[. ]$/.test(part) || /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(part)))
18
+ throw new Error("artifact_source_path_invalid");
19
+ if (segments.some(part => forbiddenDirs.has(part.toLowerCase()) || part.toLowerCase().startsWith(".env") || /(?:\.(?:pem|key|p12|pfx)|^\.?(?:credentials|netrc|npmrc|aws|ssh)$)/i.test(part)))
20
+ throw new Error("artifact_source_credential_or_cache_forbidden");
21
+ return segments.join("/");
22
+ }
23
+ function text(header, offset, length) {
24
+ const field = header.subarray(offset, offset + length);
25
+ const nul = field.indexOf(0);
26
+ return field.subarray(0, nul < 0 ? field.length : nul).toString("utf8");
27
+ }
28
+ function octal(header, offset, length) {
29
+ const value = text(header, offset, length).trim();
30
+ if (!/^[0-7]+$/.test(value))
31
+ throw new Error("artifact_source_tar_invalid");
32
+ const number = Number.parseInt(value, 8);
33
+ if (!Number.isSafeInteger(number) || number < 0)
34
+ throw new Error("artifact_source_tar_invalid");
35
+ return number;
36
+ }
37
+ /** Extract only regular files into a task-created empty staging directory. */
38
+ export async function extractArtifactSourceArchive(body, stagingRoot) {
39
+ if (!body.length || body.length > MAX_SOURCE_ARCHIVE_BYTES)
40
+ throw new Error("artifact_source_transfer_too_large");
41
+ let tar;
42
+ try {
43
+ tar = gunzipSync(body, { maxOutputLength: MAX_CONTENT_BYTES + MAX_FILES * 1024 });
44
+ }
45
+ catch {
46
+ throw new Error("artifact_source_archive_invalid");
47
+ }
48
+ let position = 0;
49
+ let fileCount = 0;
50
+ let byteCount = 0;
51
+ let ended = false;
52
+ let entryCount = 0;
53
+ const files = new Set();
54
+ const dirs = new Set();
55
+ const paths = new Map();
56
+ while (position + BLOCK <= tar.length) {
57
+ const header = tar.subarray(position, position + BLOCK);
58
+ position += BLOCK;
59
+ if (header.every(byte => byte === 0)) {
60
+ if (position + BLOCK > tar.length || !tar.subarray(position).every(byte => byte === 0))
61
+ throw new Error("artifact_source_tar_invalid");
62
+ ended = true;
63
+ break;
64
+ }
65
+ if (++entryCount > MAX_FILES * 2)
66
+ throw new Error("artifact_source_unpacked_too_large");
67
+ const expectedChecksum = octal(header, 148, 8);
68
+ const checksum = header.reduce((sum, byte, index) => sum + (index >= 148 && index < 156 ? 32 : byte), 0);
69
+ if (checksum !== expectedChecksum || text(header, 257, 6) !== "ustar")
70
+ throw new Error("artifact_source_tar_invalid");
71
+ const prefix = text(header, 345, 155);
72
+ const rawName = `${prefix ? prefix + "/" : ""}${text(header, 0, 100)}`;
73
+ const size = octal(header, 124, 12);
74
+ const padded = Math.ceil(size / BLOCK) * BLOCK;
75
+ if (position + padded > tar.length)
76
+ throw new Error("artifact_source_tar_invalid");
77
+ const kind = String.fromCharCode(header[156]);
78
+ if ((rawName === "." || rawName === "./") && kind === "5" && size === 0)
79
+ continue;
80
+ const relative = safeSourcePath(rawName);
81
+ const key = relative.toLowerCase();
82
+ const segments = relative.split("/");
83
+ for (let length = 1; length <= segments.length; length++) {
84
+ const actual = segments.slice(0, length).join("/");
85
+ const canonical = actual.toLowerCase();
86
+ if (paths.has(canonical) && paths.get(canonical) !== actual)
87
+ throw new Error("artifact_source_duplicate_path");
88
+ paths.set(canonical, actual);
89
+ }
90
+ if (paths.size > MAX_FILES * 2)
91
+ throw new Error("artifact_source_unpacked_too_large");
92
+ if (kind !== "0" && kind !== "\0" && kind !== "5")
93
+ throw new Error("artifact_source_entry_type_forbidden");
94
+ if (files.has(key) || dirs.has(key))
95
+ throw new Error("artifact_source_duplicate_path");
96
+ const ancestors = key.split("/").slice(0, -1);
97
+ if (ancestors.some((_, index) => files.has(ancestors.slice(0, index + 1).join("/"))))
98
+ throw new Error("artifact_source_duplicate_path");
99
+ const output = path.resolve(stagingRoot, relative);
100
+ const diff = path.relative(stagingRoot, output);
101
+ if (diff === ".." || diff.startsWith(`..${path.sep}`) || path.isAbsolute(diff))
102
+ throw new Error("artifact_source_path_invalid");
103
+ if (kind === "5") {
104
+ if (size !== 0)
105
+ throw new Error("artifact_source_tar_invalid");
106
+ dirs.add(key);
107
+ await mkdir(output, { recursive: true, mode: 0o700 });
108
+ }
109
+ else {
110
+ if (++fileCount > MAX_FILES || (byteCount += size) > MAX_CONTENT_BYTES)
111
+ throw new Error("artifact_source_unpacked_too_large");
112
+ files.add(key);
113
+ await mkdir(path.dirname(output), { recursive: true, mode: 0o700 });
114
+ await writeFile(output, tar.subarray(position, position + size), { flag: "wx", mode: 0o600 });
115
+ }
116
+ position += padded;
117
+ }
118
+ if (!ended || !fileCount)
119
+ throw new Error("artifact_source_archive_invalid");
120
+ return { fileCount, byteCount, sha256: createHash("sha256").update(body).digest("hex") };
121
+ }
122
+ export async function boundedTransfer(response, maxBytes, expectedSize) {
123
+ const rawLength = response.headers.get("content-length");
124
+ const declared = rawLength === null ? null : Number(rawLength);
125
+ if (declared !== null && (!Number.isSafeInteger(declared) || declared < 0 || declared > maxBytes || (expectedSize !== undefined && declared !== expectedSize)))
126
+ throw new Error("build_transfer_size_invalid");
127
+ if (!response.body)
128
+ throw new Error("build_transfer_size_invalid");
129
+ const chunks = [];
130
+ let total = 0;
131
+ const reader = response.body.getReader();
132
+ for (;;) {
133
+ const part = await reader.read();
134
+ if (part.done)
135
+ break;
136
+ total += part.value.byteLength;
137
+ if (total > maxBytes) {
138
+ await reader.cancel().catch(() => undefined);
139
+ throw new Error("build_transfer_size_invalid");
140
+ }
141
+ chunks.push(Buffer.from(part.value));
142
+ }
143
+ if ((!total && expectedSize !== 0) || (declared !== null && total !== declared) || (expectedSize !== undefined && total !== expectedSize))
144
+ throw new Error("build_transfer_size_invalid");
145
+ return Buffer.concat(chunks);
146
+ }