schornai 0.2.0 → 0.3.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.
|
@@ -7,7 +7,7 @@ import { dirname, join, resolve } from 'node:path';
|
|
|
7
7
|
import { createInterface } from 'node:readline/promises';
|
|
8
8
|
import { Transform } from 'node:stream';
|
|
9
9
|
import { pipeline } from 'node:stream/promises';
|
|
10
|
-
import { r as runServiceCLI, e as extractArchive } from './shared/cli-
|
|
10
|
+
import { r as runServiceCLI, e as extractArchive } from './shared/cli-DwJIziOh.mjs';
|
|
11
11
|
import { i as initConfig, w as writeJson, l as loadConfig, r as resolveFrom, o as object, a as literal, d as defaultConfigPath, s as string, c as createLogger, b as run, e as binary, n as number, f as boolean } from './shared/docker-Dj7cdbkw.mjs';
|
|
12
12
|
import { g as generateSecuritySecrets, h as hasLegacyLayout } from './shared/installer-CWopk_6Z.mjs';
|
|
13
13
|
import { execFile } from 'node:child_process';
|
|
@@ -409,6 +409,7 @@ async function runDefaultCLI() {
|
|
|
409
409
|
console.log(`Downloading schorn.ai setup from ${latestSetupUrl}`);
|
|
410
410
|
const archive = join(temporary, "setup.tar");
|
|
411
411
|
const id = await downloadSetup(archive, signal);
|
|
412
|
+
console.log("Download complete. Verifying and extracting schorn.ai setup...");
|
|
412
413
|
const payload = join(temporary, "payload");
|
|
413
414
|
await mkdir(payload, { mode: 0o700 });
|
|
414
415
|
const inspected = await extractArchive(archive, payload, id, signal);
|
package/dist/service/cli.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import 'node:path';
|
|
|
6
6
|
import 'node:child_process';
|
|
7
7
|
import 'node:timers/promises';
|
|
8
8
|
import './shared/docker-Dj7cdbkw.mjs';
|
|
9
|
-
export { r as runServiceCLI } from './shared/cli-
|
|
9
|
+
export { r as runServiceCLI } from './shared/cli-DwJIziOh.mjs';
|
|
10
10
|
import 'node:os';
|
|
11
11
|
import 'node:crypto';
|
|
12
12
|
import 'node:http';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { promisify, isDeepStrictEqual, parseArgs } from 'node:util';
|
|
3
3
|
import u, { createReadStream, existsSync, createWriteStream, openSync, closeSync } from 'node:fs';
|
|
4
|
-
import ro, { mkdir, readFile, mkdtemp, rm, cp, writeFile, rename, statfs,
|
|
4
|
+
import ro, { lstat, readdir, realpath, mkdir, symlink, readlink, unlink, readFile, mkdtemp, rm, cp, writeFile, rename, statfs, chmod, stat, open } from 'node:fs/promises';
|
|
5
5
|
import R, { win32, posix, basename, join, resolve, dirname as dirname$1 } from 'node:path';
|
|
6
6
|
import { execFile, spawn } from 'node:child_process';
|
|
7
7
|
import { setTimeout as setTimeout$1 } from 'node:timers/promises';
|
|
@@ -151,6 +151,46 @@ function archivePath(value) {
|
|
|
151
151
|
}
|
|
152
152
|
return posix.normalize(value).replace(/^\.\//, "");
|
|
153
153
|
}
|
|
154
|
+
function assertRelativeLink(target) {
|
|
155
|
+
if (!target ||
|
|
156
|
+
target.startsWith("/") ||
|
|
157
|
+
target.includes("\\") ||
|
|
158
|
+
/^[A-Za-z]:/.test(target))
|
|
159
|
+
throw new Error("XRAG_UNSAFE_LINK");
|
|
160
|
+
}
|
|
161
|
+
async function assertSafeLink(name, link, lookup) {
|
|
162
|
+
assertRelativeLink(link.target);
|
|
163
|
+
const resolved = link.hard
|
|
164
|
+
? []
|
|
165
|
+
: posix
|
|
166
|
+
.dirname(name)
|
|
167
|
+
.split("/")
|
|
168
|
+
.filter((part) => part !== ".");
|
|
169
|
+
const pending = link.target.split("/");
|
|
170
|
+
let hops = 0;
|
|
171
|
+
while (pending.length) {
|
|
172
|
+
const part = pending.shift();
|
|
173
|
+
if (!part || part === ".")
|
|
174
|
+
continue;
|
|
175
|
+
if (part === "..") {
|
|
176
|
+
if (!resolved.length)
|
|
177
|
+
throw new Error("XRAG_UNSAFE_LINK");
|
|
178
|
+
resolved.pop();
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
const nested = await lookup([...resolved, part].join("/"));
|
|
182
|
+
if (nested) {
|
|
183
|
+
assertRelativeLink(nested.target);
|
|
184
|
+
if (++hops > 40)
|
|
185
|
+
throw new Error("XRAG_UNSAFE_LINK");
|
|
186
|
+
if (nested.hard)
|
|
187
|
+
resolved.length = 0;
|
|
188
|
+
pending.unshift(...nested.target.split("/"));
|
|
189
|
+
}
|
|
190
|
+
else
|
|
191
|
+
resolved.push(part);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
154
194
|
/** Stream even very large archives; only small metadata files are held in memory. */
|
|
155
195
|
async function inspectArchive(source, signal) {
|
|
156
196
|
const hash = createHash("sha256");
|
|
@@ -178,11 +218,7 @@ async function inspectArchive(source, signal) {
|
|
|
178
218
|
names.add(name);
|
|
179
219
|
if (entry.type === "SymbolicLink" || entry.type === "Link") {
|
|
180
220
|
const target = entry.linkpath ?? "";
|
|
181
|
-
|
|
182
|
-
target.startsWith("/") ||
|
|
183
|
-
target.includes("\\") ||
|
|
184
|
-
/^[A-Za-z]:/.test(target))
|
|
185
|
-
throw new Error("XRAG_UNSAFE_LINK");
|
|
221
|
+
assertRelativeLink(target);
|
|
186
222
|
archivePath(posix.normalize(posix.join(entry.type === "Link" ? "." : posix.dirname(name), target)));
|
|
187
223
|
links.set(name, { target, hard: entry.type === "Link" });
|
|
188
224
|
}
|
|
@@ -227,37 +263,8 @@ async function inspectArchive(source, signal) {
|
|
|
227
263
|
}
|
|
228
264
|
// Resolve links component by component: normalizing "link/../x" first can hide
|
|
229
265
|
// an escape when that intermediate link points at an ancestor directory.
|
|
230
|
-
for (const [name, link] of links)
|
|
231
|
-
|
|
232
|
-
? []
|
|
233
|
-
: posix
|
|
234
|
-
.dirname(name)
|
|
235
|
-
.split("/")
|
|
236
|
-
.filter((part) => part !== ".");
|
|
237
|
-
const pending = link.target.split("/");
|
|
238
|
-
let hops = 0;
|
|
239
|
-
while (pending.length) {
|
|
240
|
-
const part = pending.shift();
|
|
241
|
-
if (!part || part === ".")
|
|
242
|
-
continue;
|
|
243
|
-
if (part === "..") {
|
|
244
|
-
if (!resolved.length)
|
|
245
|
-
throw new Error("XRAG_UNSAFE_LINK");
|
|
246
|
-
resolved.pop();
|
|
247
|
-
continue;
|
|
248
|
-
}
|
|
249
|
-
const nested = links.get([...resolved, part].join("/"));
|
|
250
|
-
if (nested) {
|
|
251
|
-
if (++hops > 40)
|
|
252
|
-
throw new Error("XRAG_UNSAFE_LINK");
|
|
253
|
-
if (nested.hard)
|
|
254
|
-
resolved.length = 0;
|
|
255
|
-
pending.unshift(...nested.target.split("/"));
|
|
256
|
-
}
|
|
257
|
-
else
|
|
258
|
-
resolved.push(part);
|
|
259
|
-
}
|
|
260
|
-
}
|
|
266
|
+
for (const [name, link] of links)
|
|
267
|
+
await assertSafeLink(name, link, (path) => links.get(path));
|
|
261
268
|
const xrag = xragSchema.parse(JSON.parse(metadata.get("xrag.json")?.toString("utf8") ?? "null"));
|
|
262
269
|
const required = xrag.xrag_type === "setup"
|
|
263
270
|
? ["setup.sh", "update.mts", "apps/service/cli.mjs"]
|
|
@@ -270,21 +277,85 @@ async function inspectArchive(source, signal) {
|
|
|
270
277
|
: [];
|
|
271
278
|
if (required.some((name) => !files.has(name)))
|
|
272
279
|
throw new Error("XRAG_MISSING_PAYLOAD");
|
|
273
|
-
return { id: hash.digest("hex"), size, xrag, metadata };
|
|
280
|
+
return { id: hash.digest("hex"), size, xrag, metadata, links };
|
|
274
281
|
}
|
|
282
|
+
/** Extract only into a fresh, private directory owned by the caller. */
|
|
275
283
|
async function extractArchive(file, destination, expectedId, signal) {
|
|
284
|
+
if (!(await lstat(destination)).isDirectory())
|
|
285
|
+
throw new Error("XRAG_UNSAFE_DESTINATION");
|
|
286
|
+
if ((await readdir(destination)).length)
|
|
287
|
+
throw new Error("XRAG_DESTINATION_NOT_EMPTY");
|
|
288
|
+
const root = await realpath(destination);
|
|
276
289
|
const result = await inspectArchive(createReadStream(file), signal);
|
|
277
290
|
if (result.id !== expectedId)
|
|
278
291
|
throw new Error("XRAG_CHECKSUM_MISMATCH");
|
|
279
292
|
signal?.throwIfAborted();
|
|
293
|
+
let extractionError;
|
|
280
294
|
await So({
|
|
281
295
|
file,
|
|
282
|
-
cwd:
|
|
283
|
-
strict
|
|
296
|
+
cwd: root,
|
|
297
|
+
// tar's strict mode rejects while other entries are still being written.
|
|
298
|
+
// Keep warnings fatal, but wait for those writes before callers clean up.
|
|
299
|
+
strict: false,
|
|
300
|
+
onwarn(code, message, data) {
|
|
301
|
+
extractionError ??= Object.assign(new Error(`${code}: ${message}`), data);
|
|
302
|
+
},
|
|
284
303
|
preservePaths: false,
|
|
285
304
|
noChmod: false,
|
|
286
305
|
noMtime: true,
|
|
306
|
+
// tar rejects even validated in-tree symlink chains. Install these last,
|
|
307
|
+
// unchanged, so extraction never writes through an archive symlink.
|
|
308
|
+
filter: (_path, entry) => !("type" in entry && entry.type === "SymbolicLink"),
|
|
287
309
|
});
|
|
310
|
+
if (extractionError)
|
|
311
|
+
throw extractionError;
|
|
312
|
+
signal?.throwIfAborted();
|
|
313
|
+
const created = [];
|
|
314
|
+
try {
|
|
315
|
+
for (const [name, link] of result.links) {
|
|
316
|
+
if (link.hard)
|
|
317
|
+
continue;
|
|
318
|
+
signal?.throwIfAborted();
|
|
319
|
+
let parent = root;
|
|
320
|
+
for (const part of posix.dirname(name).split("/")) {
|
|
321
|
+
if (part === ".")
|
|
322
|
+
continue;
|
|
323
|
+
parent = join(parent, part);
|
|
324
|
+
await mkdir(parent).catch((error) => {
|
|
325
|
+
if (error.code !== "EEXIST")
|
|
326
|
+
throw error;
|
|
327
|
+
});
|
|
328
|
+
if (!(await lstat(parent)).isDirectory())
|
|
329
|
+
throw new Error("XRAG_LINK_PARENT");
|
|
330
|
+
}
|
|
331
|
+
const path = join(root, name);
|
|
332
|
+
await symlink(link.target, path);
|
|
333
|
+
created.push(path);
|
|
334
|
+
}
|
|
335
|
+
// Check actual filesystem resolution too: case/Unicode aliases can make
|
|
336
|
+
// distinct archive names refer to the same path on macOS.
|
|
337
|
+
for (const [name, link] of result.links) {
|
|
338
|
+
if (link.hard)
|
|
339
|
+
continue;
|
|
340
|
+
signal?.throwIfAborted();
|
|
341
|
+
await assertSafeLink(name, link, async (path) => {
|
|
342
|
+
try {
|
|
343
|
+
if (!(await lstat(join(root, path))).isSymbolicLink())
|
|
344
|
+
return;
|
|
345
|
+
return { target: await readlink(join(root, path)), hard: false };
|
|
346
|
+
}
|
|
347
|
+
catch (error) {
|
|
348
|
+
if (["ENOENT", "ENOTDIR"].includes(error.code ?? ""))
|
|
349
|
+
return;
|
|
350
|
+
throw error;
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
catch (error) {
|
|
356
|
+
await Promise.allSettled(created.map((path) => unlink(path)));
|
|
357
|
+
throw error;
|
|
358
|
+
}
|
|
288
359
|
return result;
|
|
289
360
|
}
|
|
290
361
|
|