berimor 0.9.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 ADDED
@@ -0,0 +1,39 @@
1
+ # bootstrap
2
+
3
+ npm-пакет установщика Berimor — см. `docs/arch/deployment.md` §2–3, ADR-0025.
4
+
5
+ ## Первая установка: скачивание, верификация, распаковка (D3)
6
+
7
+ При первом запуске (`dist/index.js`) bootstrap определяет платформу
8
+ (`platform.ts`), проверяет локальный кэш (`~/.cache/berimor/bin` на Linux,
9
+ `~/Library/Caches/berimor/bin` на macOS, `%LOCALAPPDATA%\berimor\bin` на
10
+ Windows — `cache-dir.ts`), при отсутствии — скачивает платформенный архив с
11
+ GitHub Releases (`download.ts`), сверяет его SHA-256 с записью в
12
+ `checksums.json` (`checksum.ts` + `checksums-manifest.ts`) и только затем
13
+ атомарно распаковывает (`extract.ts`: временная директория рядом с целевой +
14
+ `rename` одной операцией). Несовпадение хэша или отсутствие записи в
15
+ `checksums.json` — явный отказ, ничего не распаковывается.
16
+
17
+ `checksums.json` пишется не разработчиком, а CI (`release.yml`, job
18
+ `publish`) на этапе публикации — сравнение идёт с независимым от GitHub
19
+ Release каналом (npm), см. `docs/ROADMAP.md` §14 (D3) для полного разбора,
20
+ почему верификация через уже существующий `verify.ts` (`berimor verify`,
21
+ делегирование нативному бинарнику) циркулярна именно на первой установке и
22
+ не используется здесь; `verify.ts` остаётся заделом для D4 (само-обновление).
23
+
24
+ ```sh
25
+ npm run build
26
+ npm test # node:test, встроенный в Node ≥20 — новых dev-зависимостей нет
27
+ ```
28
+
29
+ ## Локальная разработка
30
+
31
+ `postinstall` запускает `dist/postinstall.js`, поэтому при первом клоне репозитория нужна сборка до `npm install` от зависимостей:
32
+
33
+ ```sh
34
+ npm install --ignore-scripts
35
+ npm run build
36
+ npm install # теперь postinstall находит dist/ и проходит
37
+ ```
38
+
39
+ В опубликованном пакете `dist/` уже собран и входит в тарбол — этот порядок нужен только локально, до первого `npm run build`.
package/checksums.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "berimor-v0.9.0-darwin-arm64.tar.gz": "89ba8ce69974bc35d1081afbd99035eec58095a266f0f305d7d4777c95e481ce",
3
+ "berimor-v0.9.0-linux-arm64.tar.gz": "9cef54507cf5654dfd64baeb43b862a48bfcd13433b8b335c10189a47fa330dc",
4
+ "berimor-v0.9.0-linux-x64.tar.gz": "b25e3055ce795475389cf67addb851c411bc89889ea3b6e78149bcc74f20dea2",
5
+ "berimor-v0.9.0-win32-x64.zip": "abceb5824b153d0326770164c4441d3823d1f99b6a2c701c850616c3aed6f8fd"
6
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Локальный кэш скачанных и распакованных платформенных артефактов.
3
+ *
4
+ * Источник: docs/arch/deployment.md §2 (bootstrap кэширует результат
5
+ * определения платформы и — здесь — сами скачанные бинарники, повторный
6
+ * запуск с уже установленной версией не должен снова обращаться к сети).
7
+ * ROADMAP: D3.
8
+ */
9
+ import path from "node:path";
10
+ export function resolveCacheDir(platform, env) {
11
+ if (platform === "win32") {
12
+ const base = env.localAppData ?? path.join(env.home, "AppData", "Local");
13
+ return path.join(base, "berimor", "bin");
14
+ }
15
+ if (platform === "darwin") {
16
+ return path.join(env.home, "Library", "Caches", "berimor", "bin");
17
+ }
18
+ const base = env.xdgCacheHome ?? path.join(env.home, ".cache");
19
+ return path.join(base, "berimor", "bin");
20
+ }
21
+ export function currentEnv() {
22
+ return {
23
+ home: process.env.HOME ?? process.env.USERPROFILE ?? "",
24
+ xdgCacheHome: process.env.XDG_CACHE_HOME,
25
+ localAppData: process.env.LOCALAPPDATA,
26
+ };
27
+ }
28
+ //# sourceMappingURL=cache-dir.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache-dir.js","sourceRoot":"","sources":["../src/cache-dir.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,IAAI,MAAM,WAAW,CAAC;AAS7B,MAAM,UAAU,eAAe,CAAC,QAAkB,EAAE,GAAgB;IAClE,IAAI,QAAQ,KAAK,OAAO,EAAE,CAAC;QACzB,MAAM,IAAI,GAAG,GAAG,CAAC,YAAY,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC;QACzE,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,SAAS,EAAE,KAAK,CAAC,CAAC;IAC3C,CAAC;IACD,IAAI,QAAQ,KAAK,QAAQ,EAAE,CAAC;QAC1B,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,CAAC,CAAC;IACpE,CAAC;IACD,MAAM,IAAI,GAAG,GAAG,CAAC,YAAY,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;IAC/D,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,SAAS,EAAE,KAAK,CAAC,CAAC;AAC3C,CAAC;AAED,MAAM,UAAU,UAAU;IACxB,OAAO;QACL,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,EAAE;QACvD,YAAY,EAAE,OAAO,CAAC,GAAG,CAAC,cAAc;QACxC,YAAY,EAAE,OAAO,CAAC,GAAG,CAAC,YAAY;KACvC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,25 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import path from "node:path";
4
+ import { resolveCacheDir } from "./cache-dir.js";
5
+ test("linux: XDG_CACHE_HOME задан", () => {
6
+ const dir = resolveCacheDir("linux", { home: "/home/u", xdgCacheHome: "/custom/cache" });
7
+ assert.equal(dir, path.join("/custom/cache", "berimor", "bin"));
8
+ });
9
+ test("linux: XDG_CACHE_HOME не задан — падаем на ~/.cache", () => {
10
+ const dir = resolveCacheDir("linux", { home: "/home/u" });
11
+ assert.equal(dir, path.join("/home/u", ".cache", "berimor", "bin"));
12
+ });
13
+ test("darwin: Library/Caches", () => {
14
+ const dir = resolveCacheDir("darwin", { home: "/Users/u" });
15
+ assert.equal(dir, path.join("/Users/u", "Library", "Caches", "berimor", "bin"));
16
+ });
17
+ test("win32: LOCALAPPDATA задан", () => {
18
+ const dir = resolveCacheDir("win32", { home: "C:\\Users\\u", localAppData: "C:\\Users\\u\\AppData\\Local" });
19
+ assert.equal(dir, path.join("C:\\Users\\u\\AppData\\Local", "berimor", "bin"));
20
+ });
21
+ test("win32: LOCALAPPDATA не задан — падаем на home/AppData/Local", () => {
22
+ const dir = resolveCacheDir("win32", { home: "C:\\Users\\u" });
23
+ assert.equal(dir, path.join("C:\\Users\\u", "AppData", "Local", "berimor", "bin"));
24
+ });
25
+ //# sourceMappingURL=cache-dir.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache-dir.test.js","sourceRoot":"","sources":["../src/cache-dir.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAEjD,IAAI,CAAC,6BAA6B,EAAE,GAAG,EAAE;IACvC,MAAM,GAAG,GAAG,eAAe,CAAC,OAAO,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,YAAY,EAAE,eAAe,EAAE,CAAC,CAAC;IACzF,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,SAAS,EAAE,KAAK,CAAC,CAAC,CAAC;AAClE,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,qDAAqD,EAAE,GAAG,EAAE;IAC/D,MAAM,GAAG,GAAG,eAAe,CAAC,OAAO,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,CAAC;IAC1D,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,CAAC,CAAC,CAAC;AACtE,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,wBAAwB,EAAE,GAAG,EAAE;IAClC,MAAM,GAAG,GAAG,eAAe,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,CAAC;IAC5D,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,SAAS,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,CAAC,CAAC,CAAC;AAClF,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,2BAA2B,EAAE,GAAG,EAAE;IACrC,MAAM,GAAG,GAAG,eAAe,CAAC,OAAO,EAAE,EAAE,IAAI,EAAE,cAAc,EAAE,YAAY,EAAE,8BAA8B,EAAE,CAAC,CAAC;IAC7G,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,CAAC,8BAA8B,EAAE,SAAS,EAAE,KAAK,CAAC,CAAC,CAAC;AACjF,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,6DAA6D,EAAE,GAAG,EAAE;IACvE,MAAM,GAAG,GAAG,eAAe,CAAC,OAAO,EAAE,EAAE,IAAI,EAAE,cAAc,EAAE,CAAC,CAAC;IAC/D,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,CAAC,cAAc,EAAE,SAAS,EAAE,OAAO,EAAE,SAAS,EAAE,KAAK,CAAC,CAAC,CAAC;AACrF,CAAC,CAAC,CAAC"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Верификация скачанного архива по SHA-256 из checksums.json (см.
3
+ * checksums-manifest.ts) — решение циркулярности первой установки: см.
4
+ * docstring verify.ts и ROADMAP D3. Сравнение — константным временем, раз
5
+ * один из операндов получен из недоверенного источника (скачанный файл).
6
+ */
7
+ import crypto from "node:crypto";
8
+ import fs from "node:fs";
9
+ export function sha256Hex(filePath) {
10
+ const hash = crypto.createHash("sha256");
11
+ hash.update(fs.readFileSync(filePath));
12
+ return hash.digest("hex");
13
+ }
14
+ export class ChecksumMismatchError extends Error {
15
+ constructor(filePath, expectedHex, actualHex) {
16
+ super(`контрольная сумма не совпала для ${filePath}: ожидалось ${expectedHex}, получено ${actualHex} — файл не распаковывается`);
17
+ this.name = "ChecksumMismatchError";
18
+ }
19
+ }
20
+ export function verifyChecksum(filePath, expectedHex) {
21
+ const actualHex = sha256Hex(filePath);
22
+ const expected = Buffer.from(expectedHex, "hex");
23
+ const actual = Buffer.from(actualHex, "hex");
24
+ if (expected.length !== actual.length || !crypto.timingSafeEqual(expected, actual)) {
25
+ throw new ChecksumMismatchError(filePath, expectedHex, actualHex);
26
+ }
27
+ }
28
+ //# sourceMappingURL=checksum.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checksum.js","sourceRoot":"","sources":["../src/checksum.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,MAAM,MAAM,aAAa,CAAC;AACjC,OAAO,EAAE,MAAM,SAAS,CAAC;AAEzB,MAAM,UAAU,SAAS,CAAC,QAAgB;IACxC,MAAM,IAAI,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;IACzC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,CAAC;IACvC,OAAO,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC5B,CAAC;AAED,MAAM,OAAO,qBAAsB,SAAQ,KAAK;IAC9C,YAAY,QAAgB,EAAE,WAAmB,EAAE,SAAiB;QAClE,KAAK,CACH,oCAAoC,QAAQ,eAAe,WAAW,cAAc,SAAS,4BAA4B,CAC1H,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,uBAAuB,CAAC;IACtC,CAAC;CACF;AAED,MAAM,UAAU,cAAc,CAAC,QAAgB,EAAE,WAAmB;IAClE,MAAM,SAAS,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;IACtC,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC;IACjD,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;IAC7C,IAAI,QAAQ,CAAC,MAAM,KAAK,MAAM,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,eAAe,CAAC,QAAQ,EAAE,MAAM,CAAC,EAAE,CAAC;QACnF,MAAM,IAAI,qBAAqB,CAAC,QAAQ,EAAE,WAAW,EAAE,SAAS,CAAC,CAAC;IACpE,CAAC;AACH,CAAC"}
@@ -0,0 +1,31 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import crypto from "node:crypto";
7
+ import { sha256Hex, verifyChecksum, ChecksumMismatchError } from "./checksum.js";
8
+ function tempFileWith(content) {
9
+ const p = path.join(os.tmpdir(), `berimor-checksum-test-${crypto.randomUUID()}`);
10
+ fs.writeFileSync(p, content);
11
+ return p;
12
+ }
13
+ test("sha256Hex совпадает с независимо посчитанным хэшем", () => {
14
+ const p = tempFileWith("hello berimor");
15
+ const expected = crypto.createHash("sha256").update("hello berimor").digest("hex");
16
+ assert.equal(sha256Hex(p), expected);
17
+ fs.rmSync(p);
18
+ });
19
+ test("verifyChecksum не бросает на совпадающем хэше", () => {
20
+ const p = tempFileWith("ok");
21
+ const hex = crypto.createHash("sha256").update("ok").digest("hex");
22
+ assert.doesNotThrow(() => verifyChecksum(p, hex));
23
+ fs.rmSync(p);
24
+ });
25
+ test("verifyChecksum бросает ChecksumMismatchError на несовпадающем хэше", () => {
26
+ const p = tempFileWith("tampered");
27
+ const wrongHex = crypto.createHash("sha256").update("original").digest("hex");
28
+ assert.throws(() => verifyChecksum(p, wrongHex), ChecksumMismatchError);
29
+ fs.rmSync(p);
30
+ });
31
+ //# sourceMappingURL=checksum.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checksum.test.js","sourceRoot":"","sources":["../src/checksum.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,MAAM,MAAM,aAAa,CAAC;AACjC,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAEjF,SAAS,YAAY,CAAC,OAAe;IACnC,MAAM,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,yBAAyB,MAAM,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;IACjF,EAAE,CAAC,aAAa,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;IAC7B,OAAO,CAAC,CAAC;AACX,CAAC;AAED,IAAI,CAAC,oDAAoD,EAAE,GAAG,EAAE;IAC9D,MAAM,CAAC,GAAG,YAAY,CAAC,eAAe,CAAC,CAAC;IACxC,MAAM,QAAQ,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACnF,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;IACrC,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;AACf,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,+CAA+C,EAAE,GAAG,EAAE;IACzD,MAAM,CAAC,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;IAC7B,MAAM,GAAG,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACnE,MAAM,CAAC,YAAY,CAAC,GAAG,EAAE,CAAC,cAAc,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;IAClD,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;AACf,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,oEAAoE,EAAE,GAAG,EAAE;IAC9E,MAAM,CAAC,GAAG,YAAY,CAAC,UAAU,CAAC,CAAC;IACnC,MAAM,QAAQ,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAC9E,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,cAAc,CAAC,CAAC,EAAE,QAAQ,CAAC,EAAE,qBAAqB,CAAC,CAAC;IACxE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;AACf,CAAC,CAAC,CAAC"}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Пин SHA-256 платформенных артефактов внутри самого npm-пакета.
3
+ *
4
+ * Публикуется в checksums.json рядом с package.json на этапе `publish` job
5
+ * release.yml (см. комментарий там) — доверие к первой установке приходит
6
+ * из npm-канала, независимого от GitHub Release, который эти же файлы
7
+ * раздаёт (см. docstring verify.ts и ROADMAP D3 про циркулярность). Нет
8
+ * файла или нет записи под конкретный артефакт — явный отказ, не молчаливый
9
+ * пропуск верификации (тот же принцип, что и в остальном проекте).
10
+ */
11
+ import fs from "node:fs";
12
+ import path from "node:path";
13
+ import { fileURLToPath } from "node:url";
14
+ function defaultManifestPath() {
15
+ const distDir = path.dirname(fileURLToPath(import.meta.url));
16
+ return path.join(distDir, "..", "checksums.json");
17
+ }
18
+ export class ChecksumsManifestMissingError extends Error {
19
+ constructor(manifestPath) {
20
+ super(`не найден список контрольных сумм (${manifestPath}) — bootstrap не может верифицировать первую установку, скачивание отклонено`);
21
+ this.name = "ChecksumsManifestMissingError";
22
+ }
23
+ }
24
+ export class ChecksumMissingForAssetError extends Error {
25
+ constructor(assetName, manifestPath) {
26
+ super(`в списке контрольных сумм (${manifestPath}) нет записи для ${assetName} — скачивание отклонено`);
27
+ this.name = "ChecksumMissingForAssetError";
28
+ }
29
+ }
30
+ export function loadChecksums(manifestPath = defaultManifestPath()) {
31
+ if (!fs.existsSync(manifestPath)) {
32
+ throw new ChecksumsManifestMissingError(manifestPath);
33
+ }
34
+ const raw = fs.readFileSync(manifestPath, "utf-8");
35
+ return JSON.parse(raw);
36
+ }
37
+ export function expectedChecksumFor(manifest, assetName, manifestPath = defaultManifestPath()) {
38
+ const hex = manifest[assetName];
39
+ if (!hex) {
40
+ throw new ChecksumMissingForAssetError(assetName, manifestPath);
41
+ }
42
+ return hex;
43
+ }
44
+ //# sourceMappingURL=checksums-manifest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checksums-manifest.js","sourceRoot":"","sources":["../src/checksums-manifest.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAIzC,SAAS,mBAAmB;IAC1B,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IAC7D,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE,gBAAgB,CAAC,CAAC;AACpD,CAAC;AAED,MAAM,OAAO,6BAA8B,SAAQ,KAAK;IACtD,YAAY,YAAoB;QAC9B,KAAK,CACH,sCAAsC,YAAY,8EAA8E,CACjI,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,+BAA+B,CAAC;IAC9C,CAAC;CACF;AAED,MAAM,OAAO,4BAA6B,SAAQ,KAAK;IACrD,YAAY,SAAiB,EAAE,YAAoB;QACjD,KAAK,CACH,8BAA8B,YAAY,oBAAoB,SAAS,yBAAyB,CACjG,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,8BAA8B,CAAC;IAC7C,CAAC;CACF;AAED,MAAM,UAAU,aAAa,CAAC,eAAuB,mBAAmB,EAAE;IACxE,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,YAAY,CAAC,EAAE,CAAC;QACjC,MAAM,IAAI,6BAA6B,CAAC,YAAY,CAAC,CAAC;IACxD,CAAC;IACD,MAAM,GAAG,GAAG,EAAE,CAAC,YAAY,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;IACnD,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAsB,CAAC;AAC9C,CAAC;AAED,MAAM,UAAU,mBAAmB,CACjC,QAA2B,EAC3B,SAAiB,EACjB,eAAuB,mBAAmB,EAAE;IAE5C,MAAM,GAAG,GAAG,QAAQ,CAAC,SAAS,CAAC,CAAC;IAChC,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,MAAM,IAAI,4BAA4B,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC;IAClE,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
@@ -0,0 +1,35 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import crypto from "node:crypto";
7
+ import { loadChecksums, expectedChecksumFor, ChecksumsManifestMissingError, ChecksumMissingForAssetError, } from "./checksums-manifest.js";
8
+ function tempManifest(content) {
9
+ const p = path.join(os.tmpdir(), `berimor-checksums-test-${crypto.randomUUID()}.json`);
10
+ fs.writeFileSync(p, JSON.stringify(content));
11
+ return p;
12
+ }
13
+ test("loadChecksums бросает ChecksumsManifestMissingError на отсутствующем файле", () => {
14
+ const missing = path.join(os.tmpdir(), `berimor-does-not-exist-${crypto.randomUUID()}.json`);
15
+ assert.throws(() => loadChecksums(missing), ChecksumsManifestMissingError);
16
+ });
17
+ test("loadChecksums читает существующий файл", () => {
18
+ const p = tempManifest({ "asset.tar.gz": "deadbeef" });
19
+ const manifest = loadChecksums(p);
20
+ assert.equal(manifest["asset.tar.gz"], "deadbeef");
21
+ fs.rmSync(p);
22
+ });
23
+ test("expectedChecksumFor бросает ChecksumMissingForAssetError на отсутствующей записи", () => {
24
+ const p = tempManifest({ "other.tar.gz": "deadbeef" });
25
+ const manifest = loadChecksums(p);
26
+ assert.throws(() => expectedChecksumFor(manifest, "asset.tar.gz", p), ChecksumMissingForAssetError);
27
+ fs.rmSync(p);
28
+ });
29
+ test("expectedChecksumFor возвращает значение на существующей записи", () => {
30
+ const p = tempManifest({ "asset.tar.gz": "cafebabe" });
31
+ const manifest = loadChecksums(p);
32
+ assert.equal(expectedChecksumFor(manifest, "asset.tar.gz", p), "cafebabe");
33
+ fs.rmSync(p);
34
+ });
35
+ //# sourceMappingURL=checksums-manifest.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checksums-manifest.test.js","sourceRoot":"","sources":["../src/checksums-manifest.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,MAAM,MAAM,aAAa,CAAC;AACjC,OAAO,EACL,aAAa,EACb,mBAAmB,EACnB,6BAA6B,EAC7B,4BAA4B,GAC7B,MAAM,yBAAyB,CAAC;AAEjC,SAAS,YAAY,CAAC,OAAe;IACnC,MAAM,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,0BAA0B,MAAM,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;IACvF,EAAE,CAAC,aAAa,CAAC,CAAC,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC;IAC7C,OAAO,CAAC,CAAC;AACX,CAAC;AAED,IAAI,CAAC,4EAA4E,EAAE,GAAG,EAAE;IACtF,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,0BAA0B,MAAM,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;IAC7F,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,aAAa,CAAC,OAAO,CAAC,EAAE,6BAA6B,CAAC,CAAC;AAC7E,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,wCAAwC,EAAE,GAAG,EAAE;IAClD,MAAM,CAAC,GAAG,YAAY,CAAC,EAAE,cAAc,EAAE,UAAU,EAAE,CAAC,CAAC;IACvD,MAAM,QAAQ,GAAG,aAAa,CAAC,CAAC,CAAC,CAAC;IAClC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,cAAc,CAAC,EAAE,UAAU,CAAC,CAAC;IACnD,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;AACf,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,kFAAkF,EAAE,GAAG,EAAE;IAC5F,MAAM,CAAC,GAAG,YAAY,CAAC,EAAE,cAAc,EAAE,UAAU,EAAE,CAAC,CAAC;IACvD,MAAM,QAAQ,GAAG,aAAa,CAAC,CAAC,CAAC,CAAC;IAClC,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,mBAAmB,CAAC,QAAQ,EAAE,cAAc,EAAE,CAAC,CAAC,EAAE,4BAA4B,CAAC,CAAC;IACpG,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;AACf,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,gEAAgE,EAAE,GAAG,EAAE;IAC1E,MAAM,CAAC,GAAG,YAAY,CAAC,EAAE,cAAc,EAAE,UAAU,EAAE,CAAC,CAAC;IACvD,MAAM,QAAQ,GAAG,aAAa,CAAC,CAAC,CAAC,CAAC;IAClC,MAAM,CAAC,KAAK,CAAC,mBAAmB,CAAC,QAAQ,EAAE,cAAc,EAAE,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC;IAC3E,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;AACf,CAAC,CAAC,CAAC"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Скачивание платформенного артефакта с GitHub Releases репозитория проекта.
3
+ *
4
+ * Источник: docs/arch/deployment.md §2–3. ROADMAP: D3. Верификация — не
5
+ * здесь: вызывающий код (index.ts) сверяет скачанный файл с пином из
6
+ * checksums-manifest.ts ДО распаковки (checksum.ts) — эта функция только
7
+ * переносит байты, ничего не проверяет и не исполняет.
8
+ */
9
+ import fs from "node:fs";
10
+ export const RELEASE_REPOSITORY = "devpilgrin/berimor";
11
+ export function releaseAssetUrl(version, assetName) {
12
+ return `https://github.com/${RELEASE_REPOSITORY}/releases/download/v${version}/${assetName}`;
13
+ }
14
+ export class DownloadError extends Error {
15
+ }
16
+ export async function downloadAsset(url, destPath, fetchImpl = fetch) {
17
+ const response = await fetchImpl(url);
18
+ if (!response.ok) {
19
+ throw new DownloadError(`скачивание ${url} завершилось с HTTP ${response.status}`);
20
+ }
21
+ const buffer = Buffer.from(await response.arrayBuffer());
22
+ fs.writeFileSync(destPath, buffer);
23
+ }
24
+ //# sourceMappingURL=download.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"download.js","sourceRoot":"","sources":["../src/download.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,MAAM,SAAS,CAAC;AAEzB,MAAM,CAAC,MAAM,kBAAkB,GAAG,oBAAoB,CAAC;AAEvD,MAAM,UAAU,eAAe,CAAC,OAAe,EAAE,SAAiB;IAChE,OAAO,sBAAsB,kBAAkB,uBAAuB,OAAO,IAAI,SAAS,EAAE,CAAC;AAC/F,CAAC;AAED,MAAM,OAAO,aAAc,SAAQ,KAAK;CAAG;AAE3C,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,GAAW,EACX,QAAgB,EAChB,YAA0B,KAAK;IAE/B,MAAM,QAAQ,GAAG,MAAM,SAAS,CAAC,GAAG,CAAC,CAAC;IACtC,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,MAAM,IAAI,aAAa,CAAC,cAAc,GAAG,uBAAuB,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;IACrF,CAAC;IACD,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,QAAQ,CAAC,WAAW,EAAE,CAAC,CAAC;IACzD,EAAE,CAAC,aAAa,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;AACrC,CAAC"}
@@ -0,0 +1,26 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import crypto from "node:crypto";
7
+ import { downloadAsset, releaseAssetUrl, DownloadError, RELEASE_REPOSITORY } from "./download.js";
8
+ function tempDestPath() {
9
+ return path.join(os.tmpdir(), `berimor-download-test-${crypto.randomUUID()}`);
10
+ }
11
+ test("releaseAssetUrl строит URL на GitHub Releases репозитория проекта", () => {
12
+ const url = releaseAssetUrl("1.2.3", "berimor-1.2.3-linux-x64.tar.gz");
13
+ assert.equal(url, `https://github.com/${RELEASE_REPOSITORY}/releases/download/v1.2.3/berimor-1.2.3-linux-x64.tar.gz`);
14
+ });
15
+ test("downloadAsset пишет тело успешного ответа в destPath", async () => {
16
+ const dest = tempDestPath();
17
+ const fakeFetch = (async () => new Response(new Blob([Buffer.from("archive-bytes")]), { status: 200 }));
18
+ await downloadAsset("https://example.invalid/asset", dest, fakeFetch);
19
+ assert.equal(fs.readFileSync(dest, "utf-8"), "archive-bytes");
20
+ fs.rmSync(dest, { force: true });
21
+ });
22
+ test("downloadAsset бросает DownloadError на не-2xx ответе", async () => {
23
+ const fakeFetch = (async () => new Response(null, { status: 404 }));
24
+ await assert.rejects(() => downloadAsset("https://example.invalid/missing", tempDestPath(), fakeFetch), DownloadError);
25
+ });
26
+ //# sourceMappingURL=download.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"download.test.js","sourceRoot":"","sources":["../src/download.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,MAAM,MAAM,aAAa,CAAC;AACjC,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAElG,SAAS,YAAY;IACnB,OAAO,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,yBAAyB,MAAM,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;AAChF,CAAC;AAED,IAAI,CAAC,mEAAmE,EAAE,GAAG,EAAE;IAC7E,MAAM,GAAG,GAAG,eAAe,CAAC,OAAO,EAAE,gCAAgC,CAAC,CAAC;IACvE,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,sBAAsB,kBAAkB,0DAA0D,CAAC,CAAC;AACxH,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,sDAAsD,EAAE,KAAK,IAAI,EAAE;IACtE,MAAM,IAAI,GAAG,YAAY,EAAE,CAAC;IAC5B,MAAM,SAAS,GAAG,CAAC,KAAK,IAAI,EAAE,CAC5B,IAAI,QAAQ,CAAC,IAAI,IAAI,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAiB,CAAC;IAC3F,MAAM,aAAa,CAAC,+BAA+B,EAAE,IAAI,EAAE,SAAS,CAAC,CAAC;IACtE,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,EAAE,eAAe,CAAC,CAAC;IAC9D,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;AACnC,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,sDAAsD,EAAE,KAAK,IAAI,EAAE;IACtE,MAAM,SAAS,GAAG,CAAC,KAAK,IAAI,EAAE,CAAC,IAAI,QAAQ,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAiB,CAAC;IACpF,MAAM,MAAM,CAAC,OAAO,CAClB,GAAG,EAAE,CAAC,aAAa,CAAC,iCAAiC,EAAE,YAAY,EAAE,EAAE,SAAS,CAAC,EACjF,aAAa,CACd,CAAC;AACJ,CAAC,CAAC,CAAC"}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Атомарная распаковка платформенного архива в кэш.
3
+ *
4
+ * Источник: docs/arch/deployment.md §2 («атомарная распаковка»). ROADMAP: D3.
5
+ * Распаковка — во временную директорию рядом с целевой (та же ФС — условие
6
+ * атомарности rename), затем переименование на финальное имя одной
7
+ * операцией: до переименования частично распакованное содержимое никогда
8
+ * не видно под конечным путём. Используются нативные средства платформы
9
+ * (`tar` на unix, `Expand-Archive` на Windows), не новая npm-зависимость —
10
+ * то же соглашение, что и в D1 (нативные раннеры вместо кросс-инструментов).
11
+ */
12
+ import { randomUUID } from "node:crypto";
13
+ import fs from "node:fs";
14
+ import path from "node:path";
15
+ import { spawnSync } from "node:child_process";
16
+ export class ExtractError extends Error {
17
+ }
18
+ function extractInto(archivePath, tmpDir, platform) {
19
+ if (platform === "win32") {
20
+ const escaped = (p) => `'${p.replace(/'/g, "''")}'`;
21
+ const command = `Expand-Archive -LiteralPath ${escaped(archivePath)} -DestinationPath ${escaped(tmpDir)} -Force`;
22
+ const result = spawnSync("powershell", ["-NoProfile", "-NonInteractive", "-Command", command], {
23
+ stdio: "pipe",
24
+ encoding: "utf-8",
25
+ });
26
+ if (result.status !== 0) {
27
+ throw new ExtractError(`Expand-Archive завершился с ошибкой: ${result.stderr || result.error?.message}`);
28
+ }
29
+ return;
30
+ }
31
+ const result = spawnSync("tar", ["-xzf", archivePath, "-C", tmpDir], {
32
+ stdio: "pipe",
33
+ encoding: "utf-8",
34
+ });
35
+ if (result.status !== 0) {
36
+ throw new ExtractError(`tar завершился с ошибкой: ${result.stderr || result.error?.message}`);
37
+ }
38
+ }
39
+ export function extractAtomically(archivePath, destDir, platform) {
40
+ if (fs.existsSync(destDir)) {
41
+ return;
42
+ }
43
+ const parent = path.dirname(destDir);
44
+ fs.mkdirSync(parent, { recursive: true });
45
+ const tmpDir = `${destDir}.tmp-${randomUUID()}`;
46
+ fs.mkdirSync(tmpDir, { recursive: true });
47
+ try {
48
+ extractInto(archivePath, tmpDir, platform);
49
+ fs.renameSync(tmpDir, destDir);
50
+ }
51
+ catch (err) {
52
+ const code = err.code;
53
+ if (code === "ENOTEMPTY" || code === "EEXIST") {
54
+ // Конкурентная/повторная установка уже материализовала destDir между
55
+ // проверкой существования выше и переименованием — принимаем её как
56
+ // валидную (она сама появилась тем же атомарным путём), не ошибка.
57
+ fs.rmSync(tmpDir, { recursive: true, force: true });
58
+ return;
59
+ }
60
+ fs.rmSync(tmpDir, { recursive: true, force: true });
61
+ throw err;
62
+ }
63
+ }
64
+ //# sourceMappingURL=extract.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extract.js","sourceRoot":"","sources":["../src/extract.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,42 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import crypto from "node:crypto";
7
+ import { fileURLToPath } from "node:url";
8
+ import { extractAtomically } from "./extract.js";
9
+ const fixturesDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "fixtures", "golden", "bootstrap");
10
+ function tempDestDir() {
11
+ return path.join(os.tmpdir(), `berimor-extract-test-${crypto.randomUUID()}`);
12
+ }
13
+ test("unix: tar.gz распаковывается в целевую директорию", () => {
14
+ const archive = path.join(fixturesDir, "sample.tar.gz");
15
+ const dest = tempDestDir();
16
+ extractAtomically(archive, dest, "linux");
17
+ const content = fs.readFileSync(path.join(dest, "hello.txt"), "utf-8");
18
+ assert.equal(content, "berimor bootstrap fixture\n");
19
+ fs.rmSync(dest, { recursive: true, force: true });
20
+ });
21
+ test("повторный вызов на уже существующую целевую директорию — не падает", () => {
22
+ const archive = path.join(fixturesDir, "sample.tar.gz");
23
+ const dest = tempDestDir();
24
+ extractAtomically(archive, dest, "linux");
25
+ assert.doesNotThrow(() => extractAtomically(archive, dest, "linux"));
26
+ const content = fs.readFileSync(path.join(dest, "hello.txt"), "utf-8");
27
+ assert.equal(content, "berimor bootstrap fixture\n");
28
+ fs.rmSync(dest, { recursive: true, force: true });
29
+ });
30
+ test("win32: zip распаковывается в целевую директорию (только на Windows)", (t) => {
31
+ if (os.platform() !== "win32") {
32
+ t.skip("Expand-Archive недоступен вне Windows — CI (bootstrap job) прогоняет только ubuntu-latest");
33
+ return;
34
+ }
35
+ const archive = path.join(fixturesDir, "sample.zip");
36
+ const dest = tempDestDir();
37
+ extractAtomically(archive, dest, "win32");
38
+ const content = fs.readFileSync(path.join(dest, "hello.txt"), "utf-8");
39
+ assert.equal(content, "berimor bootstrap fixture\n");
40
+ fs.rmSync(dest, { recursive: true, force: true });
41
+ });
42
+ //# sourceMappingURL=extract.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extract.test.js","sourceRoot":"","sources":["../src/extract.test.ts"],"names":[],"mappings":"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"}
package/dist/index.js ADDED
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * berimor — bootstrap-точка входа.
4
+ *
5
+ * Определяет платформу; при первом запуске скачивает и верифицирует
6
+ * нативный артефакт по SHA-256, пришпиленному в checksums.json самого
7
+ * npm-пакета (см. checksums-manifest.ts — почему не через `berimor verify`
8
+ * на первой установке), атомарно распаковывает; дальше и при повторных
9
+ * запусках делегирует нативному бинарнику с передачей аргументов.
10
+ * Источник: docs/arch/deployment.md §5. ROADMAP: D3 (это), D4 (само-
11
+ * обновление — не здесь).
12
+ */
13
+ import fs from "node:fs";
14
+ import os from "node:os";
15
+ import path from "node:path";
16
+ import { randomUUID } from "node:crypto";
17
+ import { spawnSync } from "node:child_process";
18
+ import { fileURLToPath } from "node:url";
19
+ import { detectPlatform } from "./platform.js";
20
+ import { resolveCacheDir, currentEnv } from "./cache-dir.js";
21
+ import { downloadAsset, releaseAssetUrl } from "./download.js";
22
+ import { verifyChecksum } from "./checksum.js";
23
+ import { loadChecksums, expectedChecksumFor } from "./checksums-manifest.js";
24
+ import { extractAtomically } from "./extract.js";
25
+ export function ownVersion() {
26
+ const distDir = path.dirname(fileURLToPath(import.meta.url));
27
+ const packageJsonPath = path.join(distDir, "..", "package.json");
28
+ const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
29
+ return pkg.version;
30
+ }
31
+ /**
32
+ * Гарантирует, что нативный бинарник версии `opts.version` присутствует в
33
+ * `opts.cacheDir` — скачивает, верифицирует по пину и атомарно распаковывает
34
+ * при необходимости; возвращает путь к бинарнику. Параметры инжектируются
35
+ * (не читаются из реального окружения напрямую) ради тестируемости без
36
+ * настоящей сети/npm — см. index.test.ts.
37
+ */
38
+ export async function ensureInstalled(opts) {
39
+ const info = detectPlatform(opts.version);
40
+ const binaryName = info.platform === "win32" ? "berimor.exe" : "berimor";
41
+ const targetDir = path.join(opts.cacheDir, opts.version);
42
+ const binaryPath = path.join(targetDir, binaryName);
43
+ if (fs.existsSync(binaryPath)) {
44
+ return binaryPath;
45
+ }
46
+ const manifest = loadChecksums(opts.manifestPath);
47
+ const expectedHex = expectedChecksumFor(manifest, info.assetName, opts.manifestPath);
48
+ const tmpArchivePath = path.join(os.tmpdir(), `${randomUUID()}-${info.assetName}`);
49
+ await downloadAsset(releaseAssetUrl(opts.version, info.assetName), tmpArchivePath, opts.fetchImpl ?? fetch);
50
+ try {
51
+ verifyChecksum(tmpArchivePath, expectedHex);
52
+ extractAtomically(tmpArchivePath, targetDir, info.platform);
53
+ }
54
+ finally {
55
+ fs.rmSync(tmpArchivePath, { force: true });
56
+ }
57
+ return binaryPath;
58
+ }
59
+ function delegate(binaryPath, args) {
60
+ const result = spawnSync(binaryPath, args, { stdio: "inherit" });
61
+ process.exitCode = result.status ?? 1;
62
+ }
63
+ async function main() {
64
+ const version = ownVersion();
65
+ const binaryPath = await ensureInstalled({
66
+ version,
67
+ cacheDir: resolveCacheDir(detectPlatform(version).platform, currentEnv()),
68
+ });
69
+ delegate(binaryPath, process.argv.slice(2));
70
+ }
71
+ const isMainModule = process.argv[1] === fileURLToPath(import.meta.url);
72
+ if (isMainModule) {
73
+ main().catch((err) => {
74
+ console.error(`[berimor] ${err.message}`);
75
+ process.exitCode = 1;
76
+ });
77
+ }
78
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC/C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAC7D,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAC/D,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAC7E,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AASjD,MAAM,UAAU,UAAU;IACxB,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IAC7D,MAAM,eAAe,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE,cAAc,CAAC,CAAC;IACjE,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,eAAe,EAAE,OAAO,CAAC,CAAwB,CAAC;IACzF,OAAO,GAAG,CAAC,OAAO,CAAC;AACrB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,IAA4B;IAChE,MAAM,IAAI,GAAG,cAAc,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAC1C,MAAM,UAAU,GAAG,IAAI,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,SAAS,CAAC;IACzE,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;IACzD,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;IAEpD,IAAI,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC9B,OAAO,UAAU,CAAC;IACpB,CAAC;IAED,MAAM,QAAQ,GAAG,aAAa,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;IAClD,MAAM,WAAW,GAAG,mBAAmB,CAAC,QAAQ,EAAE,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC;IAErF,MAAM,cAAc,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,GAAG,UAAU,EAAE,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC;IACnF,MAAM,aAAa,CAAC,eAAe,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,cAAc,EAAE,IAAI,CAAC,SAAS,IAAI,KAAK,CAAC,CAAC;IAE5G,IAAI,CAAC;QACH,cAAc,CAAC,cAAc,EAAE,WAAW,CAAC,CAAC;QAC5C,iBAAiB,CAAC,cAAc,EAAE,SAAS,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC9D,CAAC;YAAS,CAAC;QACT,EAAE,CAAC,MAAM,CAAC,cAAc,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IAC7C,CAAC;IAED,OAAO,UAAU,CAAC;AACpB,CAAC;AAED,SAAS,QAAQ,CAAC,UAAkB,EAAE,IAAc;IAClD,MAAM,MAAM,GAAG,SAAS,CAAC,UAAU,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC;IACjE,OAAO,CAAC,QAAQ,GAAG,MAAM,CAAC,MAAM,IAAI,CAAC,CAAC;AACxC,CAAC;AAED,KAAK,UAAU,IAAI;IACjB,MAAM,OAAO,GAAG,UAAU,EAAE,CAAC;IAC7B,MAAM,UAAU,GAAG,MAAM,eAAe,CAAC;QACvC,OAAO;QACP,QAAQ,EAAE,eAAe,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC,QAAQ,EAAE,UAAU,EAAE,CAAC;KAC1E,CAAC,CAAC;IACH,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;AAC9C,CAAC;AAED,MAAM,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACxE,IAAI,YAAY,EAAE,CAAC;IACjB,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;QACnB,OAAO,CAAC,KAAK,CAAC,aAAc,GAAa,CAAC,OAAO,EAAE,CAAC,CAAC;QACrD,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;IACvB,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,74 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import crypto from "node:crypto";
7
+ import { fileURLToPath } from "node:url";
8
+ import { ensureInstalled } from "./index.js";
9
+ import { detectPlatform } from "./platform.js";
10
+ import { sha256Hex } from "./checksum.js";
11
+ import { ChecksumMismatchError } from "./checksum.js";
12
+ const fixturesDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "fixtures", "golden", "bootstrap");
13
+ const sampleArchive = path.join(fixturesDir, "sample.tar.gz");
14
+ function tempDir() {
15
+ const p = path.join(os.tmpdir(), `berimor-index-test-${crypto.randomUUID()}`);
16
+ fs.mkdirSync(p, { recursive: true });
17
+ return p;
18
+ }
19
+ function writeManifest(dir, assetName, hex) {
20
+ const p = path.join(dir, "checksums.json");
21
+ fs.writeFileSync(p, JSON.stringify({ [assetName]: hex }));
22
+ return p;
23
+ }
24
+ test("ensureInstalled: нет кэша → скачивает (фейковый fetch) → сверяет хэш → распаковывает", async () => {
25
+ const version = "9.9.9-test";
26
+ const info = detectPlatform(version);
27
+ const archiveBytes = fs.readFileSync(sampleArchive);
28
+ const correctHex = sha256Hex(sampleArchive);
29
+ const workDir = tempDir();
30
+ const manifestPath = writeManifest(workDir, info.assetName, correctHex);
31
+ const cacheDir = path.join(workDir, "cache");
32
+ const fakeFetch = (async () => new Response(new Blob([archiveBytes]), { status: 200 }));
33
+ const binaryPath = await ensureInstalled({ version, cacheDir, manifestPath, fetchImpl: fakeFetch });
34
+ const binaryName = info.platform === "win32" ? "berimor.exe" : "berimor";
35
+ assert.equal(binaryPath, path.join(cacheDir, version, binaryName));
36
+ // sample.tar.gz распаковывает hello.txt, не berimor(.exe) — реального
37
+ // бинарника во фикстуре нет, важна только сама распаковка по месту.
38
+ assert.equal(fs.readFileSync(path.join(cacheDir, version, "hello.txt"), "utf-8"), "berimor bootstrap fixture\n");
39
+ fs.rmSync(workDir, { recursive: true, force: true });
40
+ });
41
+ test("ensureInstalled: уже скачанный бинарник в кэше — сеть не трогается", async () => {
42
+ const version = "9.9.9-cached";
43
+ const info = detectPlatform(version);
44
+ const binaryName = info.platform === "win32" ? "berimor.exe" : "berimor";
45
+ const workDir = tempDir();
46
+ const cacheDir = path.join(workDir, "cache");
47
+ fs.mkdirSync(path.join(cacheDir, version), { recursive: true });
48
+ fs.writeFileSync(path.join(cacheDir, version, binaryName), "already here");
49
+ const fetchThatMustNotBeCalled = (async () => {
50
+ throw new Error("сеть не должна была вызываться — бинарник уже в кэше");
51
+ });
52
+ const binaryPath = await ensureInstalled({
53
+ version,
54
+ cacheDir,
55
+ manifestPath: path.join(workDir, "does-not-matter.json"),
56
+ fetchImpl: fetchThatMustNotBeCalled,
57
+ });
58
+ assert.equal(binaryPath, path.join(cacheDir, version, binaryName));
59
+ fs.rmSync(workDir, { recursive: true, force: true });
60
+ });
61
+ test("ensureInstalled: подменённый архив (неверный хэш) — отказ без распаковки", async () => {
62
+ const version = "9.9.9-tampered";
63
+ const info = detectPlatform(version);
64
+ const archiveBytes = fs.readFileSync(sampleArchive);
65
+ const wrongHex = crypto.createHash("sha256").update("не тот файл").digest("hex");
66
+ const workDir = tempDir();
67
+ const manifestPath = writeManifest(workDir, info.assetName, wrongHex);
68
+ const cacheDir = path.join(workDir, "cache");
69
+ const fakeFetch = (async () => new Response(new Blob([archiveBytes]), { status: 200 }));
70
+ await assert.rejects(() => ensureInstalled({ version, cacheDir, manifestPath, fetchImpl: fakeFetch }), ChecksumMismatchError);
71
+ assert.equal(fs.existsSync(path.join(cacheDir, version)), false);
72
+ fs.rmSync(workDir, { recursive: true, force: true });
73
+ });
74
+ //# sourceMappingURL=index.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.test.js","sourceRoot":"","sources":["../src/index.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Определение платформы и имени ожидаемого артефакта.
3
+ *
4
+ * Источник: docs/arch/deployment.md §3. ROADMAP: D3.
5
+ * Определение — кодом (os.platform()/os.arch()), без эвристик; неизвестная
6
+ * комбинация — явный отказ, а не попытка угадать ближайшую (deployment.md §3).
7
+ */
8
+ import os from "node:os";
9
+ const SUPPORTED_PLATFORMS = ["linux", "darwin", "win32"];
10
+ const SUPPORTED_ARCHES = ["x64", "arm64"];
11
+ export function detectPlatform(version) {
12
+ const platform = os.platform();
13
+ const arch = os.arch();
14
+ if (!SUPPORTED_PLATFORMS.includes(platform)) {
15
+ throw new Error(`неподдерживаемая платформа: ${platform}`);
16
+ }
17
+ if (!SUPPORTED_ARCHES.includes(arch)) {
18
+ throw new Error(`неподдерживаемая архитектура: ${arch}`);
19
+ }
20
+ const ext = platform === "win32" ? "zip" : "tar.gz";
21
+ const assetName = `berimor-${version}-${platform}-${arch}.${ext}`;
22
+ return { platform: platform, arch: arch, assetName };
23
+ }
24
+ //# sourceMappingURL=platform.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"platform.js","sourceRoot":"","sources":["../src/platform.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,MAAM,SAAS,CAAC;AAWzB,MAAM,mBAAmB,GAAe,CAAC,OAAO,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC;AACrE,MAAM,gBAAgB,GAAW,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;AAElD,MAAM,UAAU,cAAc,CAAC,OAAe;IAC5C,MAAM,QAAQ,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC;IAC/B,MAAM,IAAI,GAAG,EAAE,CAAC,IAAI,EAAE,CAAC;IAEvB,IAAI,CAAC,mBAAmB,CAAC,QAAQ,CAAC,QAAoB,CAAC,EAAE,CAAC;QACxD,MAAM,IAAI,KAAK,CAAC,+BAA+B,QAAQ,EAAE,CAAC,CAAC;IAC7D,CAAC;IACD,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAAC,IAAY,CAAC,EAAE,CAAC;QAC7C,MAAM,IAAI,KAAK,CAAC,iCAAiC,IAAI,EAAE,CAAC,CAAC;IAC3D,CAAC;IAED,MAAM,GAAG,GAAG,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC;IACpD,MAAM,SAAS,GAAG,WAAW,OAAO,IAAI,QAAQ,IAAI,IAAI,IAAI,GAAG,EAAE,CAAC;IAElE,OAAO,EAAE,QAAQ,EAAE,QAAoB,EAAE,IAAI,EAAE,IAAY,EAAE,SAAS,EAAE,CAAC;AAC3E,CAAC"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * postinstall-хук npm.
3
+ *
4
+ * Источник: docs/arch/deployment.md §9 — граница №4: «bootstrap не выполняет
5
+ * ничего, кроме определения платформы, на этапе установки». Это крупнейшая
6
+ * типовая поверхность атаки цепочки поставок для npm-пакетов, поэтому здесь
7
+ * нет ни скачивания, ни исполнения чего-либо — только определение платформы.
8
+ * Скачивание и верификация — на первом запуске (index.ts), не здесь.
9
+ */
10
+ import { detectPlatform } from "./platform.js";
11
+ try {
12
+ const info = detectPlatform(process.env.npm_package_version ?? "0.0.0");
13
+ console.log(`[berimor] платформа определена: ${info.platform}/${info.arch}`);
14
+ }
15
+ catch (err) {
16
+ console.error(`[berimor] ${err.message}`);
17
+ process.exitCode = 1;
18
+ }
19
+ //# sourceMappingURL=postinstall.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"postinstall.js","sourceRoot":"","sources":["../src/postinstall.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAE/C,IAAI,CAAC;IACH,MAAM,IAAI,GAAG,cAAc,CAAC,OAAO,CAAC,GAAG,CAAC,mBAAmB,IAAI,OAAO,CAAC,CAAC;IACxE,OAAO,CAAC,GAAG,CAAC,mCAAmC,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;AAC/E,CAAC;AAAC,OAAO,GAAG,EAAE,CAAC;IACb,OAAO,CAAC,KAAK,CAAC,aAAc,GAAa,CAAC,OAAO,EAAE,CAAC,CAAC;IACrD,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;AACvB,CAAC"}
package/dist/verify.js ADDED
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Верификация подписи артефакта уже доверенным нативным бинарником.
3
+ *
4
+ * Источник: docs/arch/deployment.md §9, ADR-0025 — bootstrap не реализует
5
+ * криптографическую проверку сам, а вызывает подкоманду `berimor verify`
6
+ * уже доверенного нативного бинарника, а не переизобретает её на другом
7
+ * языке (риск расхождения двух независимых реализаций одной проверки).
8
+ *
9
+ * Не используется на первой установке (см. checksums-manifest.ts): на первом
10
+ * запуске ещё нет доверенного нативного бинарника, который мог бы проверить
11
+ * сам себя — вызов этой функции над только что скачанным, ещё не
12
+ * верифицированным бинарником циркулярен (поддельный бинарник соврёт на
13
+ * собственный `verify`). Первая установка верифицируется отдельно, по
14
+ * SHA-256, пришпиленному в сам npm-пакет (ROADMAP D3, checksum.ts). Эта
15
+ * функция — задел для D4 (само-обновление): там уже есть предыдущий
16
+ * доверенный бинарник, который проверяет новый, — не циркулярно.
17
+ */
18
+ import { spawnSync } from "node:child_process";
19
+ export function verifyArtifact(nativeBinaryPath, artifactPath) {
20
+ const result = spawnSync(nativeBinaryPath, ["verify", artifactPath], {
21
+ stdio: "inherit",
22
+ });
23
+ return result.status === 0;
24
+ }
25
+ //# sourceMappingURL=verify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify.js","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAE/C,MAAM,UAAU,cAAc,CAAC,gBAAwB,EAAE,YAAoB;IAC3E,MAAM,MAAM,GAAG,SAAS,CAAC,gBAAgB,EAAE,CAAC,QAAQ,EAAE,YAAY,CAAC,EAAE;QACnE,KAAK,EAAE,SAAS;KACjB,CAAC,CAAC;IACH,OAAO,MAAM,CAAC,MAAM,KAAK,CAAC,CAAC;AAC7B,CAAC"}
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "berimor",
3
+ "version": "0.9.0",
4
+ "description": "Bootstrap-установщик Berimor: определение платформы, скачивание и верификация платформенного артефакта, оркестрация само-обновления.",
5
+ "license": "Apache-2.0",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/devpilgrin/berimor",
9
+ "directory": "bootstrap"
10
+ },
11
+ "type": "module",
12
+ "bin": {
13
+ "berimor": "dist/index.js"
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "checksums.json"
18
+ ],
19
+ "engines": {
20
+ "node": ">=20"
21
+ },
22
+ "scripts": {
23
+ "build": "tsc -p tsconfig.json",
24
+ "postinstall": "node dist/postinstall.js",
25
+ "test": "node --test dist/*.test.js"
26
+ },
27
+ "devDependencies": {
28
+ "@types/node": "^20.14.0",
29
+ "typescript": "^5.6.0"
30
+ }
31
+ }