Replace fixed Linux Hopper release pins with current vendor feed metadata, complete detached-PGP package verification under the stable publisher primary key, and authenticated package evidence for offline admission. Share parsed publisher facts across approved setup, doctor and the pre-execution guard. Preserve existing ownership, cancellation, private-display and cleanup rules. Fixes #3577 Normalize empty XDG settings, preserve backups during approved cache repair, and retain publisher cleanup through cancellation and shutdown. Exercise cold offline verification without reusing process-local proof state. Serialize approved cache publication across processes, restore old evidence on failure, and share the existing lease owner from process without changing its identities. Validation: 438 focused tests (six platform-related skips), full static/documentation checks, real Ubuntu 24.04 official Hopper 6.6.0 setup/CLI/MCP, offline ordinary-user admission, launcher mutation rejection, and cold/warm verification measurements. Official RPM metadata mismatch is tracked separately in #3578 and fails closed. BREAKING CHANGE: Older Linux Hopper builds admitted by the fixed allowlist now require retained publisher-signed package evidence or an approved update to the current public demo. Update explicit HOPPER_LAUNCHER_PATH values when migrating; the live feed exposes only its current release. Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com> Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
58 lines
1.9 KiB
TypeScript
58 lines
1.9 KiB
TypeScript
/** Maximum admitted vendor response bytes, shared by Hopper setup adapters. */
|
|
export const HOPPER_DOWNLOAD_BYTES = 100_000_000;
|
|
|
|
/** Bound observed response bytes before retaining them, including chunked bodies. */
|
|
export const downloadHopperPackage = async (
|
|
url: string,
|
|
options: {
|
|
readonly signal?: AbortSignal;
|
|
readonly maxBytes?: number;
|
|
readonly timeoutMs?: number;
|
|
} = {},
|
|
): Promise<{ readonly ok: boolean; readonly bytes: Uint8Array }> => {
|
|
const timeoutMs = options.timeoutMs ?? 120_000;
|
|
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 120_000)
|
|
throw new RangeError("Invalid Hopper response time budget");
|
|
const deadline = AbortSignal.timeout(timeoutMs);
|
|
const maxBytes = options.maxBytes ?? HOPPER_DOWNLOAD_BYTES;
|
|
if (
|
|
!Number.isSafeInteger(maxBytes) ||
|
|
maxBytes < 1 ||
|
|
maxBytes > HOPPER_DOWNLOAD_BYTES
|
|
)
|
|
throw new RangeError("Invalid Hopper response byte budget");
|
|
const response = await fetch(url, {
|
|
headers: { accept: "application/json", "user-agent": "rea-installer" },
|
|
signal:
|
|
options.signal === undefined
|
|
? deadline
|
|
: AbortSignal.any([options.signal, deadline]),
|
|
});
|
|
const refused = { ok: false, bytes: new Uint8Array() };
|
|
if (
|
|
!response.ok ||
|
|
Number(response.headers.get("content-length")) > maxBytes
|
|
) {
|
|
await response.body?.cancel();
|
|
return refused;
|
|
}
|
|
if (response.body === null) return { ok: true, bytes: new Uint8Array() };
|
|
const reader = response.body.getReader();
|
|
const chunks: Uint8Array[] = [];
|
|
let bytes = 0;
|
|
try {
|
|
while (true) {
|
|
const chunk = await reader.read();
|
|
if (chunk.done) return { ok: true, bytes: Buffer.concat(chunks, bytes) };
|
|
if (chunk.value.byteLength > maxBytes - bytes) {
|
|
await reader.cancel();
|
|
return refused;
|
|
}
|
|
chunks.push(chunk.value);
|
|
bytes += chunk.value.byteLength;
|
|
}
|
|
} finally {
|
|
reader.releaseLock();
|
|
}
|
|
};
|