release-skill 0.2.6 → 0.2.7
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codebuddy-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +2 -2
- package/.kimi-plugin/plugin.json +1 -1
- package/CHANGELOG.md +21 -0
- package/INSTALL.md +12 -12
- package/INSTALL.zh-CN.md +10 -10
- package/README.md +26 -11
- package/README.zh-CN.md +22 -11
- package/adapters/claude/.claude-plugin/marketplace.json +1 -1
- package/adapters/claude/.claude-plugin/plugin.json +1 -1
- package/adapters/claude/bin/release-skill.bundle.mjs +1579 -1186
- package/adapters/claude/skills/release-prepare/SKILL.md +5 -0
- package/adapters/claude/skills/release-setup/SKILL.md +10 -1
- package/adapters/claude/skills/release-verify/SKILL.md +4 -2
- package/adapters/codex/.codex-plugin/plugin.json +2 -2
- package/adapters/codex/bin/release-skill.bundle.mjs +1579 -1186
- package/adapters/codex/skills/release-prepare/SKILL.md +5 -0
- package/adapters/codex/skills/release-setup/SKILL.md +10 -1
- package/adapters/codex/skills/release-verify/SKILL.md +4 -2
- package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
- package/adapters/kimi/bin/release-skill.bundle.mjs +1579 -1186
- package/adapters/kimi/skills/release-prepare/SKILL.md +5 -0
- package/adapters/kimi/skills/release-setup/SKILL.md +10 -1
- package/adapters/kimi/skills/release-verify/SKILL.md +4 -2
- package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
- package/adapters/workbuddy/bin/release-skill.bundle.mjs +1579 -1186
- package/adapters/workbuddy/skills/release-prepare/SKILL.md +5 -0
- package/adapters/workbuddy/skills/release-setup/SKILL.md +10 -1
- package/adapters/workbuddy/skills/release-verify/SKILL.md +4 -2
- package/bin/release-skill.bundle.mjs +1579 -1186
- package/package.json +1 -1
- package/skills/release-prepare/SKILL.md +5 -0
- package/skills/release-setup/SKILL.md +10 -1
- package/skills/release-verify/SKILL.md +4 -2
- package/skills-src/release-prepare/SKILL.md +5 -0
- package/skills-src/release-setup/SKILL.md +10 -1
- package/skills-src/release-verify/SKILL.md +4 -2
- package/src/adapters/npm.mjs +54 -2
- package/src/adapters/plugin-marketplace.mjs +4 -29
- package/src/commands/prepare.mjs +3 -3
- package/src/commands/publish.mjs +2 -2
- package/src/commands/reconcile.mjs +39 -0
- package/src/commands/setup.mjs +168 -0
- package/src/commands/verify.mjs +28 -1
- package/src/npm/npm-entry-closure.mjs +195 -0
- package/src/platforms/codebuddy.mjs +19 -11
- package/src/platforms/codex.mjs +18 -10
- package/src/platforms/kimi.mjs +26 -39
- package/src/snapshot/frozen.mjs +51 -19
|
@@ -9,7 +9,7 @@ const __bundlePkgRoot = __bundleResolve(__bundleDirname(__bundleFileURLToPath(im
|
|
|
9
9
|
// Provide a real require() for CJS packages bundled into ESM (e.g. yaml, ajv).
|
|
10
10
|
const __bundleRealRequire = __bundleCreateRequire(import.meta.url);
|
|
11
11
|
// Package identity injected at build time — closure-independent --version probe.
|
|
12
|
-
const __bundlePkg = Object.freeze({"name":"release-skill","version":"0.2.
|
|
12
|
+
const __bundlePkg = Object.freeze({"name":"release-skill","version":"0.2.7"});
|
|
13
13
|
|
|
14
14
|
var __create = Object.create;
|
|
15
15
|
var __defProp = Object.defineProperty;
|
|
@@ -17087,7 +17087,7 @@ function kimiAuthorityDir(context, planDigest, plugin) {
|
|
|
17087
17087
|
throw new Error(`kimi attestation authority requires a safe plugin id: "${plugin}"`);
|
|
17088
17088
|
}
|
|
17089
17089
|
const base = resolve4(context.root, ".release-skill", "kimi-attestations");
|
|
17090
|
-
const dir = resolve4(base,
|
|
17090
|
+
const dir = resolve4(base, plugin);
|
|
17091
17091
|
const rel = relative2(base, dir);
|
|
17092
17092
|
const sep6 = process.platform === "win32" ? "\\" : "/";
|
|
17093
17093
|
if (rel === "" || rel === ".." || isAbsolute2(rel) || rel.startsWith(`..${sep6}`) || rel.split(sep6).some((segment) => segment === ".." || segment === "")) {
|
|
@@ -17104,17 +17104,16 @@ function buildKimiManualInstructions({
|
|
|
17104
17104
|
version,
|
|
17105
17105
|
ref,
|
|
17106
17106
|
attestationDir,
|
|
17107
|
-
requiresInstalledClosure
|
|
17108
|
-
managedRoot
|
|
17107
|
+
requiresInstalledClosure
|
|
17109
17108
|
}) {
|
|
17110
17109
|
return [
|
|
17111
17110
|
`Kimi Code \u6CA1\u6709\u53EF\u811A\u672C\u5316\u7684\u63D2\u4EF6\u5B89\u88C5\u547D\u4EE4\u884C\u5DE5\u5177\uFF1B\u5B89\u88C5\u662F\u624B\u52A8\u4EA4\u4E92\u6B65\u9AA4\u3002`,
|
|
17112
17111
|
`1) publish \u5B8C\u6210\u6240\u6709\u8FDC\u7AEF\u5199\u5165\u540E\u8FDB\u5165 PUBLISHED \u72B6\u6001\uFF08\u81EA\u52A8\u5316 Git \u5206\u652F/\u6807\u7B7E\u3001npm \u548C GitHub Release \u5199\u5165\u5DF2\u5B8C\u6210\uFF09\u3002\u6B64 kimi \u68C0\u67E5\u70B9\u6807\u8BB0\u4E3A\u9700\u8981\u4EBA\u5DE5\u5B89\u88C5\u3002`,
|
|
17113
|
-
`2)
|
|
17112
|
+
`2) \u5728 Kimi Code \u4E2D\u8FD0\u884C: /plugins install ${installUrl}\uFF08\u9501\u5B9A\u5230\u51BB\u7ED3 ref "${ref}"\uFF0C\u7248\u672C ${version}\uFF09\u3002\u786E\u8BA4\u63D2\u4EF6 "${plugin}" \u7684\u4FE1\u4EFB\u63D0\u793A\uFF0C\u7136\u540E\u8FD0\u884C /plugins reload\uFF08\u6216 /new\uFF09\u3002`,
|
|
17114
17113
|
`3) \u5C06\u4EBA\u5DE5\u7ED3\u679C JSON \u5199\u5165: ${attestationDir}/${KIMI_ATTESTATION_FILE}`,
|
|
17115
|
-
` \u5FC5\u586B\u5B57\u6BB5: platform="kimi", version, planDigest\uFF08\u51BB\u7ED3\u8BA1\u5212\u6458\u8981\uFF09, result("passed" \u6216 "failed"), actor\uFF08\u786E\u8BA4\u4EBA\uFF09, confirmedAt\uFF08ISO 8601 \u65F6\u95F4\u6233\uFF09${requiresInstalledClosure ? "\uFF0CinstallPath\uFF08\u5B9E\u9645\u5B89\u88C5\u540E\u7684\u63D2\u4EF6\u76EE\u5F55\
|
|
17114
|
+
` \u5FC5\u586B\u5B57\u6BB5: platform="kimi", version, planDigest\uFF08\u51BB\u7ED3\u8BA1\u5212\u6458\u8981\uFF09, result("passed" \u6216 "failed"), actor\uFF08\u786E\u8BA4\u4EBA\uFF09, confirmedAt\uFF08ISO 8601 \u65F6\u95F4\u6233\uFF09${requiresInstalledClosure ? "\uFF0CinstallPath\uFF08\u5B9E\u9645\u5B89\u88C5\u540E\u7684\u63D2\u4EF6\u76EE\u5F55\u7684\u771F\u5B9E\u8DEF\u5F84\uFF09" : ""}`,
|
|
17116
17115
|
` \u53EF\u9009\u5B57\u6BB5: note\uFF08\u5907\u6CE8\uFF09`,
|
|
17117
|
-
`4) \u8FD0\u884C release-skill reconcile\
|
|
17116
|
+
`4) \u82E5\u53D1\u5E03\u8FD0\u884C\u662F PARTIAL\uFF0C\u5148\u8FD0\u884C release-skill reconcile\uFF1B\u82E5\u5DF2\u662F PUBLISHED\uFF0C\u76F4\u63A5\u8FD0\u884C release-skill verify\u3002verify \u4ECE\u8BE5\u63D2\u4EF6\u7684\u7A33\u5B9A\u8BC1\u660E\u76EE\u5F55\u8BFB\u53D6\u5E76\u6821\u9A8C\u5F53\u524D planDigest\uFF0C\u6210\u529F\u540E -> VERIFIED\u3002`
|
|
17118
17117
|
];
|
|
17119
17118
|
}
|
|
17120
17119
|
async function readKimiManifest(pluginRootReal) {
|
|
@@ -17259,15 +17258,13 @@ async function executeKimiManualRequirement(action, context) {
|
|
|
17259
17258
|
});
|
|
17260
17259
|
}
|
|
17261
17260
|
const requiresInstalledClosure = Boolean(context.plan?.skillResourceClosure);
|
|
17262
|
-
const managedRoot = requiresInstalledClosure ? resolve4(attestationDir, "kimi-home", "plugins", "managed") : null;
|
|
17263
17261
|
const instructions = buildKimiManualInstructions({
|
|
17264
17262
|
installUrl,
|
|
17265
17263
|
plugin: action.plugin,
|
|
17266
17264
|
version: action.version,
|
|
17267
17265
|
ref,
|
|
17268
17266
|
attestationDir,
|
|
17269
|
-
requiresInstalledClosure
|
|
17270
|
-
managedRoot
|
|
17267
|
+
requiresInstalledClosure
|
|
17271
17268
|
});
|
|
17272
17269
|
const requirement = {
|
|
17273
17270
|
kind: "kimi-manual-install-requirement",
|
|
@@ -17288,7 +17285,7 @@ async function executeKimiManualRequirement(action, context) {
|
|
|
17288
17285
|
result: '<"passed" or "failed">',
|
|
17289
17286
|
actor: "<person who confirmed the install>",
|
|
17290
17287
|
confirmedAt: "<ISO 8601 timestamp>",
|
|
17291
|
-
...requiresInstalledClosure ? { installPath:
|
|
17288
|
+
...requiresInstalledClosure ? { installPath: "<actual installed plugin directory>" } : {},
|
|
17292
17289
|
note: "<optional note>"
|
|
17293
17290
|
},
|
|
17294
17291
|
instructions
|
|
@@ -17305,17 +17302,6 @@ async function executeKimiManualRequirement(action, context) {
|
|
|
17305
17302
|
});
|
|
17306
17303
|
}
|
|
17307
17304
|
}
|
|
17308
|
-
if (requiresInstalledClosure) {
|
|
17309
|
-
try {
|
|
17310
|
-
await mkdir18(managedRoot, { recursive: true, mode: 448 });
|
|
17311
|
-
} catch (mkdirErr) {
|
|
17312
|
-
return createResult({
|
|
17313
|
-
actionType,
|
|
17314
|
-
status: ActionStatus.EXECUTE_FAILED,
|
|
17315
|
-
error: `cannot create kimi resource-closure managed root: ${mkdirErr.message}`
|
|
17316
|
-
});
|
|
17317
|
-
}
|
|
17318
|
-
}
|
|
17319
17305
|
const requirementPath = resolve4(attestationDir, KIMI_REQUIREMENT_FILE);
|
|
17320
17306
|
let existing = null;
|
|
17321
17307
|
let requirementMissing = false;
|
|
@@ -17351,11 +17337,14 @@ async function executeKimiManualRequirement(action, context) {
|
|
|
17351
17337
|
}
|
|
17352
17338
|
const { createdAt: _existingCreatedAt, ...existingBody } = existing;
|
|
17353
17339
|
if (canonicalJson(existingBody) !== canonicalJson(requirement)) {
|
|
17354
|
-
|
|
17355
|
-
|
|
17356
|
-
|
|
17357
|
-
|
|
17358
|
-
|
|
17340
|
+
if (existing.planDigest === planDigest) {
|
|
17341
|
+
return createResult({
|
|
17342
|
+
actionType,
|
|
17343
|
+
status: ActionStatus.EXECUTE_FAILED,
|
|
17344
|
+
error: "existing kimi manual-install requirement conflicts with the current frozen action (same planDigest); refusing to overwrite"
|
|
17345
|
+
});
|
|
17346
|
+
}
|
|
17347
|
+
await writeEvidenceAtomic(requirementPath, { ...requirement, createdAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
17359
17348
|
}
|
|
17360
17349
|
} else {
|
|
17361
17350
|
await writeEvidenceAtomic(requirementPath, { ...requirement, createdAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
@@ -17438,7 +17427,7 @@ function codebuddyAuthorityDir(context, planDigest, plugin) {
|
|
|
17438
17427
|
throw new Error(`codebuddy attestation authority requires a safe plugin id: "${plugin}"`);
|
|
17439
17428
|
}
|
|
17440
17429
|
const base = resolve5(context.root, ".release-skill", "codebuddy-attestations");
|
|
17441
|
-
const dir = resolve5(base,
|
|
17430
|
+
const dir = resolve5(base, plugin);
|
|
17442
17431
|
const rel = relative3(base, dir);
|
|
17443
17432
|
const sep6 = process.platform === "win32" ? "\\" : "/";
|
|
17444
17433
|
if (rel === "" || rel === ".." || isAbsolute3(rel) || rel.startsWith(`..${sep6}`) || rel.split(sep6).some((segment) => segment === ".." || segment === "")) {
|
|
@@ -17708,11 +17697,14 @@ async function executeCodeBuddyManualRequirement(action, context) {
|
|
|
17708
17697
|
}
|
|
17709
17698
|
const { createdAt: _existingCreatedAt, ...existingBody } = existing;
|
|
17710
17699
|
if (canonicalJson(existingBody) !== canonicalJson(requirement)) {
|
|
17711
|
-
|
|
17712
|
-
|
|
17713
|
-
|
|
17714
|
-
|
|
17715
|
-
|
|
17700
|
+
if (existing.planDigest === planDigest) {
|
|
17701
|
+
return createResult({
|
|
17702
|
+
actionType,
|
|
17703
|
+
status: ActionStatus.EXECUTE_FAILED,
|
|
17704
|
+
error: "existing codebuddy manual-install requirement conflicts with the current frozen action (same planDigest); refusing to overwrite"
|
|
17705
|
+
});
|
|
17706
|
+
}
|
|
17707
|
+
await writeEvidenceAtomic(requirementPath, { ...requirement, createdAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
17716
17708
|
}
|
|
17717
17709
|
} else {
|
|
17718
17710
|
await writeEvidenceAtomic(requirementPath, { ...requirement, createdAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
@@ -17792,7 +17784,7 @@ function codexAuthorityDir(context, planDigest, plugin) {
|
|
|
17792
17784
|
throw new Error(`codex attestation authority requires a safe plugin id: "${plugin}"`);
|
|
17793
17785
|
}
|
|
17794
17786
|
const base = resolve6(context.root, ".release-skill", "codex-attestations");
|
|
17795
|
-
const dir = resolve6(base,
|
|
17787
|
+
const dir = resolve6(base, plugin);
|
|
17796
17788
|
const rel = relative4(base, dir);
|
|
17797
17789
|
const sep6 = process.platform === "win32" ? "\\" : "/";
|
|
17798
17790
|
if (rel === "" || rel === ".." || isAbsolute4(rel) || rel.startsWith(`..${sep6}`) || rel.split(sep6).some((segment) => segment === ".." || segment === "")) {
|
|
@@ -17964,11 +17956,14 @@ async function executeCodexManualRequirement(action, context) {
|
|
|
17964
17956
|
}
|
|
17965
17957
|
const { createdAt: _existingCreatedAt, ...existingBody } = existing;
|
|
17966
17958
|
if (canonicalJson(existingBody) !== canonicalJson(requirement)) {
|
|
17967
|
-
|
|
17968
|
-
|
|
17969
|
-
|
|
17970
|
-
|
|
17971
|
-
|
|
17959
|
+
if (existing.planDigest === planDigest) {
|
|
17960
|
+
return createResult({
|
|
17961
|
+
actionType,
|
|
17962
|
+
status: ActionStatus.EXECUTE_FAILED,
|
|
17963
|
+
error: "existing codex manual-install requirement conflicts with the current frozen action (same planDigest); refusing to overwrite"
|
|
17964
|
+
});
|
|
17965
|
+
}
|
|
17966
|
+
await writeEvidenceAtomic(requirementPath, { ...requirement, createdAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
17972
17967
|
}
|
|
17973
17968
|
} else {
|
|
17974
17969
|
await writeEvidenceAtomic(requirementPath, { ...requirement, createdAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
@@ -18526,248 +18521,1029 @@ var init_registry = __esm({
|
|
|
18526
18521
|
}
|
|
18527
18522
|
});
|
|
18528
18523
|
|
|
18529
|
-
// src/
|
|
18530
|
-
import { createRequire } from "node:module";
|
|
18531
|
-
import { dirname as dirname3, resolve as resolve7 } from "node:path";
|
|
18532
|
-
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
18533
|
-
import { realpath as realpath2 } from "node:fs/promises";
|
|
18534
|
-
import { readFileSync as readFileSync3, realpathSync as realpathSync2, lstatSync as lstatSync2 } from "node:fs";
|
|
18524
|
+
// src/snapshot/frozen.mjs
|
|
18535
18525
|
import { createHash as createHash2 } from "node:crypto";
|
|
18536
|
-
|
|
18537
|
-
|
|
18538
|
-
|
|
18539
|
-
|
|
18540
|
-
|
|
18541
|
-
|
|
18526
|
+
import { execFile as execFileCb, spawn } from "node:child_process";
|
|
18527
|
+
import { promisify } from "node:util";
|
|
18528
|
+
import {
|
|
18529
|
+
chmod,
|
|
18530
|
+
lstat as lstat4,
|
|
18531
|
+
mkdir as mkdir3,
|
|
18532
|
+
mkdtemp,
|
|
18533
|
+
open as open3,
|
|
18534
|
+
readdir as readdir2,
|
|
18535
|
+
realpath as realpath2,
|
|
18536
|
+
rm as rm3,
|
|
18537
|
+
unlink as unlink2
|
|
18538
|
+
} from "node:fs/promises";
|
|
18539
|
+
import { constants as fsConstants } from "node:fs";
|
|
18540
|
+
import { isAbsolute as isAbsolute5, join as join7, relative as relative5, resolve as resolve7 } from "node:path";
|
|
18541
|
+
function frozenError(message, details = {}) {
|
|
18542
|
+
return new ReleaseError(GATE_FAILED, message, details);
|
|
18542
18543
|
}
|
|
18543
|
-
function
|
|
18544
|
-
|
|
18545
|
-
|
|
18546
|
-
|
|
18547
|
-
|
|
18548
|
-
|
|
18549
|
-
|
|
18550
|
-
if (lexicalStat.isSymbolicLink()) {
|
|
18551
|
-
failPrebuildManifest("SYMLINK", "prebuilds manifest must not be a symlink");
|
|
18552
|
-
}
|
|
18553
|
-
if (!lexicalStat.isFile()) {
|
|
18554
|
-
failPrebuildManifest("NOT_REGULAR_FILE", "prebuilds manifest is not a regular file");
|
|
18555
|
-
}
|
|
18556
|
-
if (lexicalStat.nlink !== 1n) {
|
|
18557
|
-
failPrebuildManifest("UNEXPECTED_HARDLINK_COUNT", "prebuilds manifest has invalid nlink count");
|
|
18558
|
-
}
|
|
18559
|
-
let physicalBase;
|
|
18560
|
-
let physicalManifest;
|
|
18561
|
-
try {
|
|
18562
|
-
physicalBase = realpathSync2(NATIVE_BASE);
|
|
18563
|
-
physicalManifest = realpathSync2(PREBUILDS_PATH);
|
|
18564
|
-
} catch {
|
|
18565
|
-
failPrebuildManifest("REALPATH_FAILED");
|
|
18544
|
+
function isInside(parent, candidate) {
|
|
18545
|
+
const rel = relative5(parent, candidate);
|
|
18546
|
+
return rel === "" || !isAbsolute5(rel) && rel !== ".." && !rel.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`);
|
|
18547
|
+
}
|
|
18548
|
+
async function resolveFrozenPath(root, relativePath, label = "frozen path") {
|
|
18549
|
+
if (!relativePath || typeof relativePath !== "string" || isAbsolute5(relativePath)) {
|
|
18550
|
+
throw frozenError(`${label} must be a non-empty project-relative path`);
|
|
18566
18551
|
}
|
|
18567
|
-
|
|
18568
|
-
|
|
18569
|
-
|
|
18570
|
-
|
|
18571
|
-
);
|
|
18552
|
+
const rootReal = await realpath2(root);
|
|
18553
|
+
const lexical = resolve7(rootReal, relativePath);
|
|
18554
|
+
if (!isInside(rootReal, lexical)) {
|
|
18555
|
+
throw frozenError(`${label} escapes project root`, { relativePath });
|
|
18572
18556
|
}
|
|
18573
|
-
|
|
18574
|
-
|
|
18575
|
-
}
|
|
18576
|
-
|
|
18557
|
+
const lexicalStat = await lstat4(lexical).catch((err) => {
|
|
18558
|
+
throw frozenError(`${label} is missing`, { relativePath, cause: err.code });
|
|
18559
|
+
});
|
|
18560
|
+
if (lexicalStat.isSymbolicLink()) {
|
|
18561
|
+
throw frozenError(`${label} must not be a symlink`, { relativePath });
|
|
18577
18562
|
}
|
|
18578
|
-
|
|
18579
|
-
|
|
18580
|
-
|
|
18581
|
-
|
|
18582
|
-
|
|
18583
|
-
return msg;
|
|
18563
|
+
const physical = await realpath2(lexical).catch((err) => {
|
|
18564
|
+
throw frozenError(`${label} is missing`, { relativePath, cause: err.code });
|
|
18565
|
+
});
|
|
18566
|
+
if (!isInside(rootReal, physical)) {
|
|
18567
|
+
throw frozenError(`${label} resolves outside project root`, { relativePath });
|
|
18584
18568
|
}
|
|
18585
|
-
return
|
|
18569
|
+
return physical;
|
|
18586
18570
|
}
|
|
18587
|
-
function
|
|
18588
|
-
const
|
|
18589
|
-
|
|
18590
|
-
|
|
18591
|
-
return null;
|
|
18592
|
-
}
|
|
18593
|
-
let lstatResult;
|
|
18594
|
-
try {
|
|
18595
|
-
lstatResult = lstatSync2(addonPath, { bigint: true });
|
|
18596
|
-
} catch {
|
|
18597
|
-
return null;
|
|
18571
|
+
async function readStableRegularFile(filePath, displayPath) {
|
|
18572
|
+
const before = await lstat4(filePath);
|
|
18573
|
+
if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1) {
|
|
18574
|
+
throw frozenError(`frozen snapshot entry is not a single-link regular file: ${displayPath}`);
|
|
18598
18575
|
}
|
|
18599
|
-
|
|
18600
|
-
if (!lstatResult.isFile()) return null;
|
|
18601
|
-
if (lstatResult.nlink !== 1n) return null;
|
|
18576
|
+
const handle = await open3(filePath, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
|
|
18602
18577
|
try {
|
|
18603
|
-
const
|
|
18604
|
-
|
|
18605
|
-
|
|
18606
|
-
return null;
|
|
18578
|
+
const opened = await handle.stat();
|
|
18579
|
+
if (!opened.isFile() || opened.nlink !== 1 || opened.dev !== before.dev || opened.ino !== before.ino) {
|
|
18580
|
+
throw frozenError(`frozen snapshot entry changed before read: ${displayPath}`);
|
|
18607
18581
|
}
|
|
18608
|
-
|
|
18609
|
-
|
|
18610
|
-
|
|
18582
|
+
const bytes = await handle.readFile();
|
|
18583
|
+
const after = await handle.stat();
|
|
18584
|
+
if (after.dev !== opened.dev || after.ino !== opened.ino || after.nlink !== 1 || after.size !== opened.size || after.mtimeMs !== opened.mtimeMs || after.ctimeMs !== opened.ctimeMs) {
|
|
18585
|
+
throw frozenError(`frozen snapshot entry changed during read: ${displayPath}`);
|
|
18586
|
+
}
|
|
18587
|
+
return { bytes, mode: opened.mode };
|
|
18588
|
+
} finally {
|
|
18589
|
+
await handle.close();
|
|
18611
18590
|
}
|
|
18612
18591
|
}
|
|
18613
|
-
function
|
|
18614
|
-
|
|
18615
|
-
|
|
18616
|
-
|
|
18617
|
-
|
|
18618
|
-
throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, `manifest entry missing path for ${key}`);
|
|
18592
|
+
async function computeFrozenSnapshot(snapshotDir, { excludeRootEntries = [] } = {}) {
|
|
18593
|
+
const root = await realpath2(snapshotDir);
|
|
18594
|
+
const rootStat = await lstat4(root);
|
|
18595
|
+
if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) {
|
|
18596
|
+
throw frozenError("frozen snapshot root must be a real directory");
|
|
18619
18597
|
}
|
|
18620
|
-
|
|
18621
|
-
|
|
18598
|
+
const entries = [];
|
|
18599
|
+
const excludedRootNames = /* @__PURE__ */ new Set();
|
|
18600
|
+
const excludedPathPrefixes = [];
|
|
18601
|
+
for (const entry of excludeRootEntries) {
|
|
18602
|
+
if (typeof entry !== "string") {
|
|
18603
|
+
throw frozenError("frozen snapshot exclusion entries must be strings");
|
|
18604
|
+
}
|
|
18605
|
+
const segments = entry.split("/").filter((segment) => segment !== "" && segment !== ".");
|
|
18606
|
+
if (segments.length === 0 || entry.startsWith("/") || entry.includes("\\") || segments.some((segment) => segment === "..")) {
|
|
18607
|
+
throw frozenError(`frozen snapshot exclusion entry is not a safe relative path: ${JSON.stringify(entry)}`);
|
|
18608
|
+
}
|
|
18609
|
+
if (segments.length === 1) {
|
|
18610
|
+
excludedRootNames.add(segments[0]);
|
|
18611
|
+
} else {
|
|
18612
|
+
excludedPathPrefixes.push(segments.join("/"));
|
|
18613
|
+
}
|
|
18622
18614
|
}
|
|
18623
|
-
|
|
18624
|
-
|
|
18615
|
+
async function walk(dir) {
|
|
18616
|
+
const children = await readdir2(dir, { withFileTypes: true });
|
|
18617
|
+
children.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
18618
|
+
for (const child of children) {
|
|
18619
|
+
if (dir === root && excludedRootNames.has(child.name)) continue;
|
|
18620
|
+
const absolute = join7(dir, child.name);
|
|
18621
|
+
const rel = relative5(root, absolute).split("\\").join("/");
|
|
18622
|
+
if (excludedPathPrefixes.some((prefix) => rel === prefix || rel.startsWith(`${prefix}/`))) continue;
|
|
18623
|
+
const st = await lstat4(absolute);
|
|
18624
|
+
if (st.isSymbolicLink()) {
|
|
18625
|
+
throw frozenError(`frozen snapshot contains symlink: ${rel}`);
|
|
18626
|
+
}
|
|
18627
|
+
if (st.isDirectory()) {
|
|
18628
|
+
await walk(absolute);
|
|
18629
|
+
continue;
|
|
18630
|
+
}
|
|
18631
|
+
const { bytes, mode } = await readStableRegularFile(absolute, rel);
|
|
18632
|
+
entries.push({
|
|
18633
|
+
path: rel,
|
|
18634
|
+
type: "file",
|
|
18635
|
+
mode,
|
|
18636
|
+
size: bytes.length,
|
|
18637
|
+
contentDigest: createHash2("sha256").update(bytes).digest("hex")
|
|
18638
|
+
});
|
|
18639
|
+
}
|
|
18625
18640
|
}
|
|
18626
|
-
|
|
18627
|
-
|
|
18628
|
-
|
|
18629
|
-
|
|
18641
|
+
__name(walk, "walk");
|
|
18642
|
+
await walk(root);
|
|
18643
|
+
entries.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
18644
|
+
const digest = createHash2("sha256").update(JSON.stringify(entries)).digest("hex");
|
|
18645
|
+
return { digest, entries };
|
|
18646
|
+
}
|
|
18647
|
+
async function verifyFrozenSnapshot({ root, snapshotPath, expectedDigest }) {
|
|
18648
|
+
const snapshotDir = await resolveFrozenPath(root, snapshotPath, "frozen snapshot path");
|
|
18649
|
+
const observed = await computeFrozenSnapshot(snapshotDir);
|
|
18650
|
+
if (!expectedDigest || observed.digest !== expectedDigest) {
|
|
18651
|
+
throw frozenError("frozen snapshot digest mismatch", {
|
|
18652
|
+
expectedDigest,
|
|
18653
|
+
observedDigest: observed.digest
|
|
18654
|
+
});
|
|
18630
18655
|
}
|
|
18631
|
-
|
|
18632
|
-
|
|
18633
|
-
|
|
18634
|
-
|
|
18656
|
+
return { snapshotDir, ...observed };
|
|
18657
|
+
}
|
|
18658
|
+
async function verifyFrozenFile({ root, filePath, expectedSha256, label = "frozen file" }) {
|
|
18659
|
+
const physical = await resolveFrozenPath(root, filePath, label);
|
|
18660
|
+
const { bytes } = await readStableRegularFile(physical, filePath);
|
|
18661
|
+
const observedSha256 = createHash2("sha256").update(bytes).digest("hex");
|
|
18662
|
+
if (!expectedSha256 || observedSha256 !== expectedSha256) {
|
|
18663
|
+
throw frozenError(`${label} SHA-256 mismatch`, { expectedSha256, observedSha256 });
|
|
18635
18664
|
}
|
|
18665
|
+
return { physical, observedSha256, size: bytes.length };
|
|
18636
18666
|
}
|
|
18637
|
-
function
|
|
18638
|
-
|
|
18639
|
-
const
|
|
18640
|
-
|
|
18641
|
-
|
|
18642
|
-
|
|
18667
|
+
async function verifyFrozenDirectoryStructure(directory, label = "frozen directory") {
|
|
18668
|
+
async function walk(current) {
|
|
18669
|
+
const children = await readdir2(current, { withFileTypes: true });
|
|
18670
|
+
for (const child of children) {
|
|
18671
|
+
const absolute = join7(current, child.name);
|
|
18672
|
+
const st = await lstat4(absolute);
|
|
18673
|
+
if (st.isSymbolicLink()) {
|
|
18674
|
+
throw frozenError(`${label} contains a symlink`);
|
|
18675
|
+
}
|
|
18676
|
+
if (st.isDirectory()) {
|
|
18677
|
+
await walk(absolute);
|
|
18678
|
+
} else if (!st.isFile() || st.nlink !== 1) {
|
|
18679
|
+
throw frozenError(`${label} contains an unsafe non-regular or hardlinked entry`);
|
|
18680
|
+
}
|
|
18681
|
+
}
|
|
18643
18682
|
}
|
|
18683
|
+
__name(walk, "walk");
|
|
18684
|
+
await walk(directory);
|
|
18644
18685
|
}
|
|
18645
|
-
function
|
|
18646
|
-
|
|
18647
|
-
|
|
18648
|
-
|
|
18649
|
-
|
|
18686
|
+
async function verifyFrozenGitRepository({
|
|
18687
|
+
root,
|
|
18688
|
+
gitObjectDir,
|
|
18689
|
+
commit,
|
|
18690
|
+
tree,
|
|
18691
|
+
parentCommit,
|
|
18692
|
+
commitTimestamp,
|
|
18693
|
+
exec = execFile
|
|
18694
|
+
}) {
|
|
18695
|
+
const gitDir = await resolveFrozenPath(root, gitObjectDir, "frozen git object directory");
|
|
18696
|
+
await verifyFrozenDirectoryStructure(gitDir, "frozen git object directory");
|
|
18697
|
+
const { stdout } = await exec("git", ["--git-dir", gitDir, "rev-parse", `${commit}^{tree}`], { shell: false });
|
|
18698
|
+
if (stdout.trim() !== tree) {
|
|
18699
|
+
throw frozenError("frozen git object tree mismatch", { commit, expectedTree: tree, observedTree: stdout.trim() });
|
|
18650
18700
|
}
|
|
18651
|
-
|
|
18652
|
-
|
|
18653
|
-
|
|
18654
|
-
|
|
18655
|
-
|
|
18656
|
-
|
|
18701
|
+
if (commitTimestamp !== void 0) {
|
|
18702
|
+
const expectedTimestamp = normalizeGitTimestamp(commitTimestamp, "frozen commit timestamp");
|
|
18703
|
+
const { stdout: datesOut } = await exec(
|
|
18704
|
+
"git",
|
|
18705
|
+
["--git-dir", gitDir, "show", "-s", "--format=%aI%n%cI", commit],
|
|
18706
|
+
{ shell: false }
|
|
18657
18707
|
);
|
|
18658
|
-
|
|
18659
|
-
|
|
18660
|
-
|
|
18661
|
-
|
|
18662
|
-
|
|
18663
|
-
|
|
18664
|
-
|
|
18665
|
-
|
|
18666
|
-
if (preStat.isSymbolicLink()) {
|
|
18667
|
-
throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, "prebuilt addon must not be a symlink");
|
|
18708
|
+
const [authorDate, committerDate] = datesOut.trim().split("\n");
|
|
18709
|
+
if (authorDate !== expectedTimestamp || committerDate !== expectedTimestamp) {
|
|
18710
|
+
throw frozenError("frozen git commit dates do not match the plan freeze timestamp", {
|
|
18711
|
+
commit,
|
|
18712
|
+
expectedTimestamp,
|
|
18713
|
+
observedAuthorDate: authorDate,
|
|
18714
|
+
observedCommitterDate: committerDate
|
|
18715
|
+
});
|
|
18668
18716
|
}
|
|
18669
|
-
|
|
18670
|
-
|
|
18717
|
+
}
|
|
18718
|
+
if (parentCommit) {
|
|
18719
|
+
if (!/^[a-f0-9]{40,64}$/.test(parentCommit)) {
|
|
18720
|
+
throw frozenError("frozen Git parent must be a full hexadecimal object id");
|
|
18671
18721
|
}
|
|
18672
|
-
|
|
18673
|
-
|
|
18722
|
+
const { stdout: parentsOut } = await exec(
|
|
18723
|
+
"git",
|
|
18724
|
+
["--git-dir", gitDir, "rev-list", "--parents", "-n", "1", commit],
|
|
18725
|
+
{ shell: false }
|
|
18726
|
+
);
|
|
18727
|
+
const [observedCommit, ...parents] = parentsOut.trim().split(/\s+/);
|
|
18728
|
+
if (observedCommit !== commit || parents.length !== 1 || parents[0] !== parentCommit) {
|
|
18729
|
+
throw frozenError("frozen Git commit parent mismatch", {
|
|
18730
|
+
commit,
|
|
18731
|
+
expectedParent: parentCommit,
|
|
18732
|
+
observedParents: parents
|
|
18733
|
+
});
|
|
18674
18734
|
}
|
|
18675
|
-
} catch (e) {
|
|
18676
|
-
if (e instanceof ReleaseError) throw e;
|
|
18677
|
-
throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, "prebuilt addon not accessible");
|
|
18678
18735
|
}
|
|
18679
|
-
|
|
18680
|
-
|
|
18681
|
-
|
|
18682
|
-
|
|
18683
|
-
|
|
18736
|
+
return { gitDir, commit, tree, ...parentCommit ? { parentCommit } : {} };
|
|
18737
|
+
}
|
|
18738
|
+
async function verifyGitTreeContent({ snapshotDir, repositoryDir, commit, expectedSnapshotDigest, exec }) {
|
|
18739
|
+
const snapshot = await computeFrozenSnapshot(snapshotDir);
|
|
18740
|
+
if (snapshot.digest !== expectedSnapshotDigest) {
|
|
18741
|
+
throw frozenError("frozen snapshot changed while deriving Git objects", {
|
|
18742
|
+
expectedDigest: expectedSnapshotDigest,
|
|
18743
|
+
observedDigest: snapshot.digest
|
|
18744
|
+
});
|
|
18684
18745
|
}
|
|
18685
|
-
const
|
|
18686
|
-
|
|
18687
|
-
|
|
18746
|
+
const { stdout: treeOut } = await exec(
|
|
18747
|
+
"git",
|
|
18748
|
+
["--git-dir", repositoryDir, "ls-tree", "-rz", commit],
|
|
18749
|
+
{ shell: false, encoding: "buffer", maxBuffer: 64 * 1024 * 1024 }
|
|
18750
|
+
);
|
|
18751
|
+
const treeEntries = Buffer.from(treeOut).toString("utf8").split("\0").filter(Boolean).map((record) => {
|
|
18752
|
+
const separator = record.indexOf(" ");
|
|
18753
|
+
const [mode, type] = record.slice(0, separator).split(" ");
|
|
18754
|
+
return { path: record.slice(separator + 1), mode, type };
|
|
18755
|
+
}).sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
18756
|
+
const names = treeEntries.map((entry) => entry.path);
|
|
18757
|
+
const expectedNames = snapshot.entries.map((entry) => entry.path);
|
|
18758
|
+
if (JSON.stringify(names) !== JSON.stringify(expectedNames)) {
|
|
18759
|
+
throw frozenError("frozen Git tree paths do not match the sealed public snapshot");
|
|
18688
18760
|
}
|
|
18689
|
-
const
|
|
18690
|
-
|
|
18691
|
-
|
|
18692
|
-
|
|
18693
|
-
|
|
18694
|
-
|
|
18761
|
+
for (const [index, entry] of snapshot.entries.entries()) {
|
|
18762
|
+
const gitEntry = treeEntries[index];
|
|
18763
|
+
const expectedMode = entry.mode & 73 ? "100755" : "100644";
|
|
18764
|
+
if (gitEntry.type !== "blob" || gitEntry.mode !== expectedMode) {
|
|
18765
|
+
throw frozenError(`frozen Git tree mode does not match the sealed public snapshot: ${entry.path}`, {
|
|
18766
|
+
expectedMode,
|
|
18767
|
+
observedMode: gitEntry.mode,
|
|
18768
|
+
observedType: gitEntry.type
|
|
18769
|
+
});
|
|
18770
|
+
}
|
|
18771
|
+
const { stdout } = await exec(
|
|
18772
|
+
"git",
|
|
18773
|
+
["--git-dir", repositoryDir, "show", `${commit}:${entry.path}`],
|
|
18774
|
+
{ shell: false, encoding: "buffer", maxBuffer: Math.max(64 * 1024 * 1024, entry.size + 1024) }
|
|
18775
|
+
);
|
|
18776
|
+
const digest = createHash2("sha256").update(Buffer.from(stdout)).digest("hex");
|
|
18777
|
+
if (digest !== entry.contentDigest) {
|
|
18778
|
+
throw frozenError(`frozen Git tree bytes do not match the sealed public snapshot: ${entry.path}`);
|
|
18779
|
+
}
|
|
18695
18780
|
}
|
|
18696
|
-
|
|
18697
|
-
|
|
18698
|
-
|
|
18781
|
+
}
|
|
18782
|
+
function normalizeGitTimestamp(value, label = "freeze timestamp") {
|
|
18783
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
18784
|
+
throw frozenError(`${label} must be a non-empty strict ISO 8601 timestamp string`, {
|
|
18785
|
+
received: typeof value === "string" ? value : typeof value
|
|
18786
|
+
});
|
|
18699
18787
|
}
|
|
18700
|
-
|
|
18701
|
-
|
|
18702
|
-
|
|
18703
|
-
|
|
18704
|
-
|
|
18788
|
+
const trimmed = value.trim();
|
|
18789
|
+
const match = GIT_ISO8601_STRICT_RE.exec(trimmed);
|
|
18790
|
+
if (!match) {
|
|
18791
|
+
throw frozenError(`${label} must be strict ISO 8601 with an explicit UTC offset`, {
|
|
18792
|
+
received: trimmed
|
|
18793
|
+
});
|
|
18705
18794
|
}
|
|
18706
|
-
|
|
18707
|
-
|
|
18795
|
+
const [, yearStr, monthStr, dayStr, hourStr, minuteStr, secondStr, zone] = match;
|
|
18796
|
+
const inputYear = Number(yearStr);
|
|
18797
|
+
const inputMonth = Number(monthStr);
|
|
18798
|
+
const inputDay = Number(dayStr);
|
|
18799
|
+
const inputHour = Number(hourStr);
|
|
18800
|
+
const inputMinute = Number(minuteStr);
|
|
18801
|
+
const inputSecond = Number(secondStr);
|
|
18802
|
+
if (inputYear < 1e3 || inputYear > 9999) {
|
|
18803
|
+
throw frozenError(`${label} year must be a four-digit ISO year`, { received: trimmed });
|
|
18708
18804
|
}
|
|
18709
|
-
const
|
|
18710
|
-
|
|
18711
|
-
|
|
18712
|
-
|
|
18713
|
-
} catch {
|
|
18714
|
-
throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, "prebuilt addon identity check failed after require");
|
|
18805
|
+
const probeMs = Date.UTC(inputYear, inputMonth - 1, inputDay, inputHour, inputMinute, inputSecond);
|
|
18806
|
+
const probe = new Date(probeMs);
|
|
18807
|
+
if (probe.getUTCFullYear() !== inputYear || probe.getUTCMonth() !== inputMonth - 1 || probe.getUTCDate() !== inputDay || probe.getUTCHours() !== inputHour || probe.getUTCMinutes() !== inputMinute || probe.getUTCSeconds() !== inputSecond) {
|
|
18808
|
+
throw frozenError(`${label} is not a valid calendar date`, { received: trimmed });
|
|
18715
18809
|
}
|
|
18716
|
-
|
|
18717
|
-
|
|
18810
|
+
let offsetMs = 0;
|
|
18811
|
+
if (zone !== "Z") {
|
|
18812
|
+
const sign = zone.startsWith("-") ? -1 : 1;
|
|
18813
|
+
const zoneHours = Number(zone.slice(1, 3));
|
|
18814
|
+
const zoneMinutes = Number(zone.slice(4, 6));
|
|
18815
|
+
if (zoneHours > 23 || zoneMinutes > 59) {
|
|
18816
|
+
throw frozenError(`${label} has an invalid UTC offset`, { received: trimmed });
|
|
18817
|
+
}
|
|
18818
|
+
offsetMs = sign * (zoneHours * 60 + zoneMinutes) * 6e4;
|
|
18718
18819
|
}
|
|
18719
|
-
const
|
|
18720
|
-
|
|
18721
|
-
|
|
18722
|
-
throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, "addon exports do not match manifest");
|
|
18820
|
+
const date = new Date(probeMs - offsetMs);
|
|
18821
|
+
if (date.getUTCFullYear() < 1e3 || date.getUTCFullYear() > 9999) {
|
|
18822
|
+
throw frozenError(`${label} normalizes outside the four-digit ISO year range`, { received: trimmed });
|
|
18723
18823
|
}
|
|
18724
|
-
|
|
18824
|
+
const pad = /* @__PURE__ */ __name((num) => String(num).padStart(2, "0"), "pad");
|
|
18825
|
+
return `${date.getUTCFullYear()}-${pad(date.getUTCMonth() + 1)}-${pad(date.getUTCDate())}T${pad(date.getUTCHours())}:${pad(date.getUTCMinutes())}:${pad(date.getUTCSeconds())}+00:00`;
|
|
18725
18826
|
}
|
|
18726
|
-
function
|
|
18727
|
-
if (
|
|
18728
|
-
|
|
18729
|
-
|
|
18730
|
-
|
|
18731
|
-
|
|
18732
|
-
|
|
18827
|
+
function publicGitRemote({ repo, githubHost = "github.com" }) {
|
|
18828
|
+
if (typeof repo !== "string" || !/^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/.test(repo)) {
|
|
18829
|
+
throw frozenError("Git parent repo must use owner/name format");
|
|
18830
|
+
}
|
|
18831
|
+
if (typeof githubHost !== "string" || !/^[A-Za-z0-9.-]+$/.test(githubHost) || githubHost.startsWith(".") || githubHost.endsWith(".") || githubHost.includes("..")) {
|
|
18832
|
+
throw frozenError("Git parent githubHost must be a valid hostname");
|
|
18833
|
+
}
|
|
18834
|
+
return `https://${githubHost}/${repo}.git`;
|
|
18835
|
+
}
|
|
18836
|
+
async function buildFrozenGitRepository({
|
|
18837
|
+
snapshotDir,
|
|
18838
|
+
repositoryDir,
|
|
18839
|
+
version,
|
|
18840
|
+
expectedSnapshotDigest,
|
|
18841
|
+
parent,
|
|
18842
|
+
commitTimestamp,
|
|
18843
|
+
exec = execFile
|
|
18844
|
+
}) {
|
|
18845
|
+
const canonicalTimestamp = normalizeGitTimestamp(commitTimestamp, "frozen commit timestamp");
|
|
18846
|
+
if (!expectedSnapshotDigest) throw frozenError("Git object build requires the sealed snapshot digest");
|
|
18847
|
+
await mkdir3(repositoryDir, { recursive: true });
|
|
18848
|
+
await exec("git", ["init", "--bare", repositoryDir], { shell: false });
|
|
18849
|
+
let parentCommit = null;
|
|
18850
|
+
if (parent) {
|
|
18851
|
+
if (!parent.ref || typeof parent.ref !== "string") {
|
|
18852
|
+
throw frozenError("Git parent ref must be a non-empty string");
|
|
18733
18853
|
}
|
|
18734
|
-
|
|
18735
|
-
|
|
18736
|
-
throw new ReleaseError(
|
|
18737
|
-
SAFE_WRITE_UNAVAILABLE,
|
|
18738
|
-
"addonPath is outside the package build directory"
|
|
18739
|
-
);
|
|
18854
|
+
if (!parent.commit || !/^[a-f0-9]{40,64}$/.test(parent.commit)) {
|
|
18855
|
+
throw frozenError("Git parent commit must be a full hexadecimal object id");
|
|
18740
18856
|
}
|
|
18741
|
-
|
|
18742
|
-
|
|
18743
|
-
|
|
18744
|
-
|
|
18745
|
-
|
|
18746
|
-
|
|
18747
|
-
|
|
18857
|
+
const remoteUrl = publicGitRemote(parent);
|
|
18858
|
+
await exec(
|
|
18859
|
+
"git",
|
|
18860
|
+
["--git-dir", repositoryDir, "fetch", "--no-tags", remoteUrl, parent.ref],
|
|
18861
|
+
{ shell: false }
|
|
18862
|
+
);
|
|
18863
|
+
const { stdout: fetchedOut } = await exec(
|
|
18864
|
+
"git",
|
|
18865
|
+
["--git-dir", repositoryDir, "rev-parse", "FETCH_HEAD^{commit}"],
|
|
18866
|
+
{ shell: false }
|
|
18867
|
+
);
|
|
18868
|
+
if (fetchedOut.trim() !== parent.commit) {
|
|
18869
|
+
throw frozenError("fetched Git parent does not match the observed public baseline", {
|
|
18870
|
+
expectedCommit: parent.commit,
|
|
18871
|
+
observedCommit: fetchedOut.trim(),
|
|
18872
|
+
ref: parent.ref
|
|
18873
|
+
});
|
|
18748
18874
|
}
|
|
18749
|
-
|
|
18875
|
+
parentCommit = parent.commit;
|
|
18750
18876
|
}
|
|
18751
|
-
|
|
18752
|
-
}
|
|
18753
|
-
|
|
18754
|
-
|
|
18755
|
-
|
|
18756
|
-
|
|
18757
|
-
|
|
18758
|
-
|
|
18759
|
-
|
|
18760
|
-
|
|
18761
|
-
|
|
18762
|
-
|
|
18763
|
-
|
|
18764
|
-
|
|
18765
|
-
|
|
18766
|
-
|
|
18767
|
-
|
|
18768
|
-
|
|
18769
|
-
|
|
18770
|
-
|
|
18877
|
+
const indexPath = join7(repositoryDir, "release-index");
|
|
18878
|
+
const env = { ...process.env, GIT_INDEX_FILE: indexPath };
|
|
18879
|
+
await exec("git", [
|
|
18880
|
+
"--git-dir",
|
|
18881
|
+
repositoryDir,
|
|
18882
|
+
"--work-tree",
|
|
18883
|
+
snapshotDir,
|
|
18884
|
+
"add",
|
|
18885
|
+
"--all",
|
|
18886
|
+
"--force",
|
|
18887
|
+
"--",
|
|
18888
|
+
"."
|
|
18889
|
+
], { cwd: snapshotDir, env, shell: false });
|
|
18890
|
+
const { stdout: treeOut } = await exec("git", ["--git-dir", repositoryDir, "write-tree"], {
|
|
18891
|
+
env,
|
|
18892
|
+
shell: false
|
|
18893
|
+
});
|
|
18894
|
+
const tree = treeOut.trim();
|
|
18895
|
+
const commitEnv = {
|
|
18896
|
+
...env,
|
|
18897
|
+
GIT_AUTHOR_NAME: "release-skill",
|
|
18898
|
+
GIT_AUTHOR_EMAIL: "release-skill@localhost",
|
|
18899
|
+
GIT_COMMITTER_NAME: "release-skill",
|
|
18900
|
+
GIT_COMMITTER_EMAIL: "release-skill@localhost",
|
|
18901
|
+
GIT_AUTHOR_DATE: canonicalTimestamp,
|
|
18902
|
+
GIT_COMMITTER_DATE: canonicalTimestamp
|
|
18903
|
+
};
|
|
18904
|
+
const commitArgs = ["--git-dir", repositoryDir, "commit-tree", tree];
|
|
18905
|
+
if (parentCommit) commitArgs.push("-p", parentCommit);
|
|
18906
|
+
commitArgs.push("-m", `Release ${version}`);
|
|
18907
|
+
const { stdout: commitOut } = await exec(
|
|
18908
|
+
"git",
|
|
18909
|
+
commitArgs,
|
|
18910
|
+
{ env: commitEnv, shell: false }
|
|
18911
|
+
);
|
|
18912
|
+
const commit = commitOut.trim();
|
|
18913
|
+
await verifyGitTreeContent({ snapshotDir, repositoryDir, commit, expectedSnapshotDigest, exec });
|
|
18914
|
+
if (parentCommit) {
|
|
18915
|
+
const { stdout: parentsOut } = await exec(
|
|
18916
|
+
"git",
|
|
18917
|
+
["--git-dir", repositoryDir, "rev-list", "--parents", "-n", "1", commit],
|
|
18918
|
+
{ shell: false }
|
|
18919
|
+
);
|
|
18920
|
+
const [observedCommit, ...parents] = parentsOut.trim().split(/\s+/);
|
|
18921
|
+
if (observedCommit !== commit || parents.length !== 1 || parents[0] !== parentCommit) {
|
|
18922
|
+
throw frozenError("derived Git commit does not have the exact planned parent", {
|
|
18923
|
+
commit,
|
|
18924
|
+
parentCommit,
|
|
18925
|
+
observedParents: parents
|
|
18926
|
+
});
|
|
18927
|
+
}
|
|
18928
|
+
}
|
|
18929
|
+
return { tree, commit, ...parentCommit ? { parentCommit } : {} };
|
|
18930
|
+
}
|
|
18931
|
+
function contentEntries(entries) {
|
|
18932
|
+
return entries.map(({ path: path3, size, contentDigest }) => ({ path: path3, size, contentDigest }));
|
|
18933
|
+
}
|
|
18934
|
+
async function createDetachedReadHandle(bytes, directory) {
|
|
18935
|
+
const tempDir = await mkdtemp(join7(directory, ".tar-fd-"));
|
|
18936
|
+
const tempPath = join7(tempDir, "package.tgz");
|
|
18937
|
+
let handle;
|
|
18938
|
+
try {
|
|
18939
|
+
const writer = await open3(tempPath, "wx", 256);
|
|
18940
|
+
try {
|
|
18941
|
+
await writer.writeFile(bytes);
|
|
18942
|
+
await writer.sync();
|
|
18943
|
+
} finally {
|
|
18944
|
+
await writer.close();
|
|
18945
|
+
}
|
|
18946
|
+
handle = await open3(tempPath, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
|
|
18947
|
+
await unlink2(tempPath);
|
|
18948
|
+
await rm3(tempDir, { recursive: true, force: true });
|
|
18949
|
+
return handle;
|
|
18950
|
+
} catch (err) {
|
|
18951
|
+
await handle?.close().catch(() => {
|
|
18952
|
+
});
|
|
18953
|
+
await rm3(tempDir, { recursive: true, force: true });
|
|
18954
|
+
throw err;
|
|
18955
|
+
}
|
|
18956
|
+
}
|
|
18957
|
+
async function runTarFromHandle(handle, args2) {
|
|
18958
|
+
const fdPath = process.platform === "linux" ? "/proc/self/fd/3" : "/dev/fd/3";
|
|
18959
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
18960
|
+
const child = spawn("tar", args2(fdPath), {
|
|
18961
|
+
shell: false,
|
|
18962
|
+
env: process.env,
|
|
18963
|
+
stdio: ["ignore", "pipe", "pipe", handle.fd]
|
|
18964
|
+
});
|
|
18965
|
+
let stdout = "";
|
|
18966
|
+
let stderr = "";
|
|
18967
|
+
child.stdout.setEncoding("utf8");
|
|
18968
|
+
child.stderr.setEncoding("utf8");
|
|
18969
|
+
child.stdout.on("data", (chunk) => {
|
|
18970
|
+
stdout += chunk;
|
|
18971
|
+
});
|
|
18972
|
+
child.stderr.on("data", (chunk) => {
|
|
18973
|
+
stderr += chunk;
|
|
18974
|
+
});
|
|
18975
|
+
child.on("error", rejectPromise);
|
|
18976
|
+
child.on("close", (code) => {
|
|
18977
|
+
if (code === 0) resolvePromise({ stdout, stderr });
|
|
18978
|
+
else rejectPromise(new Error(`tar exited with code ${code}: ${stderr.trim()}`));
|
|
18979
|
+
});
|
|
18980
|
+
});
|
|
18981
|
+
}
|
|
18982
|
+
function validateNpmTarballListing(listOut) {
|
|
18983
|
+
const listed = listOut.split(/\r?\n/).filter(Boolean);
|
|
18984
|
+
if (listed.length === 0) {
|
|
18985
|
+
throw frozenError("npm tarball contains no entries");
|
|
18986
|
+
}
|
|
18987
|
+
const seen = /* @__PURE__ */ new Set();
|
|
18988
|
+
for (const entry of listed) {
|
|
18989
|
+
if (entry.startsWith("/") || entry.includes("\\") || !entry.startsWith("package/") && entry !== "package") {
|
|
18990
|
+
throw frozenError("npm tarball contains an unsafe or unexpected path", { entry });
|
|
18991
|
+
}
|
|
18992
|
+
const segments = entry.split("/");
|
|
18993
|
+
const pathSegments = entry.endsWith("/") ? segments.slice(0, -1) : segments;
|
|
18994
|
+
if (pathSegments.some((segment) => segment === "" || segment === "." || segment === "..")) {
|
|
18995
|
+
throw frozenError("npm tarball contains an unsafe or unexpected path", { entry });
|
|
18996
|
+
}
|
|
18997
|
+
const canonicalPath2 = entry.endsWith("/") ? entry.slice(0, -1) : entry;
|
|
18998
|
+
if (seen.has(canonicalPath2)) {
|
|
18999
|
+
throw frozenError("npm tarball contains a duplicate path", { entry: canonicalPath2 });
|
|
19000
|
+
}
|
|
19001
|
+
seen.add(canonicalPath2);
|
|
19002
|
+
}
|
|
19003
|
+
}
|
|
19004
|
+
async function inspectNpmTarballContent({ tarballBytes, tarballDir }) {
|
|
19005
|
+
const listHandle = await createDetachedReadHandle(tarballBytes, tarballDir);
|
|
19006
|
+
let listOut;
|
|
19007
|
+
try {
|
|
19008
|
+
({ stdout: listOut } = await runTarFromHandle(listHandle, (fdPath) => ["-tzf", fdPath]));
|
|
19009
|
+
} finally {
|
|
19010
|
+
await listHandle.close();
|
|
19011
|
+
}
|
|
19012
|
+
validateNpmTarballListing(listOut);
|
|
19013
|
+
const verifyDir = await mkdtemp(join7(tarballDir, ".verify-"));
|
|
19014
|
+
try {
|
|
19015
|
+
const extractHandle = await createDetachedReadHandle(tarballBytes, tarballDir);
|
|
19016
|
+
try {
|
|
19017
|
+
await runTarFromHandle(extractHandle, (fdPath) => ["-xzf", fdPath, "-C", verifyDir]);
|
|
19018
|
+
} finally {
|
|
19019
|
+
await extractHandle.close();
|
|
19020
|
+
}
|
|
19021
|
+
return await computeFrozenSnapshot(join7(verifyDir, "package"));
|
|
19022
|
+
} finally {
|
|
19023
|
+
await rm3(verifyDir, { recursive: true, force: true });
|
|
19024
|
+
}
|
|
19025
|
+
}
|
|
19026
|
+
async function verifyNpmTarballContent({ snapshotDir, tarballBytes, tarballDir, expectedSnapshotDigest }) {
|
|
19027
|
+
const packed = await inspectNpmTarballContent({ tarballBytes, tarballDir });
|
|
19028
|
+
const original = await computeFrozenSnapshot(snapshotDir);
|
|
19029
|
+
if (original.digest !== expectedSnapshotDigest) {
|
|
19030
|
+
throw frozenError("frozen snapshot changed while deriving npm tarball", {
|
|
19031
|
+
expectedDigest: expectedSnapshotDigest,
|
|
19032
|
+
observedDigest: original.digest
|
|
19033
|
+
});
|
|
19034
|
+
}
|
|
19035
|
+
if (JSON.stringify(contentEntries(packed.entries)) !== JSON.stringify(contentEntries(original.entries))) {
|
|
19036
|
+
throw frozenError("npm tarball bytes do not match the sealed public snapshot");
|
|
19037
|
+
}
|
|
19038
|
+
}
|
|
19039
|
+
async function buildNpmTarballFileIndex({ tarballBytes, tarballDir }) {
|
|
19040
|
+
const packed = await inspectNpmTarballContent({ tarballBytes, tarballDir });
|
|
19041
|
+
return new Map(packed.entries.map((entry) => [entry.path, { type: entry.type }]));
|
|
19042
|
+
}
|
|
19043
|
+
async function buildFrozenNpmTarball({ snapshotDir, tarballDir, expectedSnapshotDigest, exec = execFile }) {
|
|
19044
|
+
await mkdir3(tarballDir, { recursive: true });
|
|
19045
|
+
const { stdout } = await exec(
|
|
19046
|
+
"npm",
|
|
19047
|
+
["pack", snapshotDir, "--pack-destination", tarballDir, "--json", "--ignore-scripts"],
|
|
19048
|
+
{ shell: false, encoding: "utf8", timeout: 12e4 }
|
|
19049
|
+
);
|
|
19050
|
+
let parsed;
|
|
19051
|
+
try {
|
|
19052
|
+
parsed = JSON.parse(stdout);
|
|
19053
|
+
} catch {
|
|
19054
|
+
throw frozenError("npm pack returned invalid JSON");
|
|
19055
|
+
}
|
|
19056
|
+
const info = Array.isArray(parsed) ? parsed[0] : parsed;
|
|
19057
|
+
if (!info?.filename || !info?.integrity) {
|
|
19058
|
+
throw frozenError("npm pack did not return filename and integrity");
|
|
19059
|
+
}
|
|
19060
|
+
const tarballPath = join7(tarballDir, info.filename);
|
|
19061
|
+
const { bytes } = await readStableRegularFile(tarballPath, info.filename);
|
|
19062
|
+
if (!expectedSnapshotDigest) throw frozenError("npm tarball build requires the sealed snapshot digest");
|
|
19063
|
+
await verifyNpmTarballContent({
|
|
19064
|
+
snapshotDir,
|
|
19065
|
+
tarballBytes: bytes,
|
|
19066
|
+
tarballDir,
|
|
19067
|
+
expectedSnapshotDigest
|
|
19068
|
+
});
|
|
19069
|
+
return {
|
|
19070
|
+
tarballPath,
|
|
19071
|
+
integrity: info.integrity,
|
|
19072
|
+
sha256: createHash2("sha256").update(bytes).digest("hex"),
|
|
19073
|
+
size: bytes.length
|
|
19074
|
+
};
|
|
19075
|
+
}
|
|
19076
|
+
async function sealFrozenSnapshot(snapshotDir) {
|
|
19077
|
+
async function walk(dir) {
|
|
19078
|
+
const children = await readdir2(dir, { withFileTypes: true });
|
|
19079
|
+
for (const child of children) {
|
|
19080
|
+
const absolute = join7(dir, child.name);
|
|
19081
|
+
const st = await lstat4(absolute);
|
|
19082
|
+
if (st.isDirectory()) {
|
|
19083
|
+
await walk(absolute);
|
|
19084
|
+
await chmod(absolute, 365);
|
|
19085
|
+
} else if (st.isFile()) {
|
|
19086
|
+
await chmod(absolute, st.mode & 73 ? 365 : 292);
|
|
19087
|
+
}
|
|
19088
|
+
}
|
|
19089
|
+
}
|
|
19090
|
+
__name(walk, "walk");
|
|
19091
|
+
await walk(snapshotDir);
|
|
19092
|
+
await chmod(snapshotDir, 365);
|
|
19093
|
+
}
|
|
19094
|
+
var execFile, GIT_ISO8601_STRICT_RE;
|
|
19095
|
+
var init_frozen = __esm({
|
|
19096
|
+
"src/snapshot/frozen.mjs"() {
|
|
19097
|
+
init_errors();
|
|
19098
|
+
execFile = promisify(execFileCb);
|
|
19099
|
+
__name(frozenError, "frozenError");
|
|
19100
|
+
__name(isInside, "isInside");
|
|
19101
|
+
__name(resolveFrozenPath, "resolveFrozenPath");
|
|
19102
|
+
__name(readStableRegularFile, "readStableRegularFile");
|
|
19103
|
+
__name(computeFrozenSnapshot, "computeFrozenSnapshot");
|
|
19104
|
+
__name(verifyFrozenSnapshot, "verifyFrozenSnapshot");
|
|
19105
|
+
__name(verifyFrozenFile, "verifyFrozenFile");
|
|
19106
|
+
__name(verifyFrozenDirectoryStructure, "verifyFrozenDirectoryStructure");
|
|
19107
|
+
__name(verifyFrozenGitRepository, "verifyFrozenGitRepository");
|
|
19108
|
+
__name(verifyGitTreeContent, "verifyGitTreeContent");
|
|
19109
|
+
GIT_ISO8601_STRICT_RE = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d{1,9})?(Z|[+-]\d{2}:\d{2})$/;
|
|
19110
|
+
__name(normalizeGitTimestamp, "normalizeGitTimestamp");
|
|
19111
|
+
__name(publicGitRemote, "publicGitRemote");
|
|
19112
|
+
__name(buildFrozenGitRepository, "buildFrozenGitRepository");
|
|
19113
|
+
__name(contentEntries, "contentEntries");
|
|
19114
|
+
__name(createDetachedReadHandle, "createDetachedReadHandle");
|
|
19115
|
+
__name(runTarFromHandle, "runTarFromHandle");
|
|
19116
|
+
__name(validateNpmTarballListing, "validateNpmTarballListing");
|
|
19117
|
+
__name(inspectNpmTarballContent, "inspectNpmTarballContent");
|
|
19118
|
+
__name(verifyNpmTarballContent, "verifyNpmTarballContent");
|
|
19119
|
+
__name(buildNpmTarballFileIndex, "buildNpmTarballFileIndex");
|
|
19120
|
+
__name(buildFrozenNpmTarball, "buildFrozenNpmTarball");
|
|
19121
|
+
__name(sealFrozenSnapshot, "sealFrozenSnapshot");
|
|
19122
|
+
}
|
|
19123
|
+
});
|
|
19124
|
+
|
|
19125
|
+
// src/npm/npm-entry-closure.mjs
|
|
19126
|
+
import { tmpdir } from "node:os";
|
|
19127
|
+
function entryError(field, target, reason, message) {
|
|
19128
|
+
return { field, target, reason, message };
|
|
19129
|
+
}
|
|
19130
|
+
function validateRelativeTarget(target, field, { requireDotSlash = false } = {}) {
|
|
19131
|
+
if (typeof target !== "string" || target.length === 0) {
|
|
19132
|
+
return {
|
|
19133
|
+
error: entryError(field, String(target ?? ""), "invalid_type", `${field} target must be a non-empty string`)
|
|
19134
|
+
};
|
|
19135
|
+
}
|
|
19136
|
+
if (target.includes("\0")) {
|
|
19137
|
+
return { error: entryError(field, target, "nul_in_path", `${field} target must not contain NUL`) };
|
|
19138
|
+
}
|
|
19139
|
+
if (target.startsWith("/") || /^[A-Za-z]:[/\\]/.test(target) || target.startsWith("\\\\")) {
|
|
19140
|
+
return { error: entryError(field, target, "absolute_path", `${field} target must be package-relative`) };
|
|
19141
|
+
}
|
|
19142
|
+
if (target.includes("\\")) {
|
|
19143
|
+
return { error: entryError(field, target, "backslash_path", `${field} target must use forward slashes`) };
|
|
19144
|
+
}
|
|
19145
|
+
if (requireDotSlash && !target.startsWith("./")) {
|
|
19146
|
+
return {
|
|
19147
|
+
error: entryError(field, target, "missing_dot_slash", `${field} target must start with "./"`)
|
|
19148
|
+
};
|
|
19149
|
+
}
|
|
19150
|
+
const relativeTarget = target.startsWith("./") ? target.slice(2) : target;
|
|
19151
|
+
const segments = relativeTarget.split("/");
|
|
19152
|
+
if (segments.includes("..")) {
|
|
19153
|
+
return {
|
|
19154
|
+
error: entryError(field, target, "path_escape", `${field} target escapes the package root`)
|
|
19155
|
+
};
|
|
19156
|
+
}
|
|
19157
|
+
if (relativeTarget.length === 0 || segments.some((segment) => segment === "" || segment === ".")) {
|
|
19158
|
+
return {
|
|
19159
|
+
error: entryError(field, target, "unsafe_segment", `${field} target contains an unsafe path segment`)
|
|
19160
|
+
};
|
|
19161
|
+
}
|
|
19162
|
+
if (requireDotSlash && segments.includes("node_modules")) {
|
|
19163
|
+
return {
|
|
19164
|
+
error: entryError(field, target, "unsafe_segment", `${field} target must not contain node_modules`)
|
|
19165
|
+
};
|
|
19166
|
+
}
|
|
19167
|
+
return { target: relativeTarget };
|
|
19168
|
+
}
|
|
19169
|
+
function checkTarget({ field, target, fileIndex, requireDotSlash = false }) {
|
|
19170
|
+
const validated = validateRelativeTarget(target, field, { requireDotSlash });
|
|
19171
|
+
if (validated.error) return { errors: [validated.error], entries: [] };
|
|
19172
|
+
const indexed = fileIndex.get(validated.target);
|
|
19173
|
+
const found = indexed?.type === "file";
|
|
19174
|
+
const entries = [{ field, target: validated.target, found }];
|
|
19175
|
+
if (found) return { entries, errors: [] };
|
|
19176
|
+
const reason = indexed === void 0 ? "entry_missing" : "entry_not_regular_file";
|
|
19177
|
+
return {
|
|
19178
|
+
entries,
|
|
19179
|
+
errors: [
|
|
19180
|
+
entryError(
|
|
19181
|
+
field,
|
|
19182
|
+
validated.target,
|
|
19183
|
+
reason,
|
|
19184
|
+
indexed === void 0 ? `${field} target "${validated.target}" is missing` : `${field} target "${validated.target}" is not a regular file`
|
|
19185
|
+
)
|
|
19186
|
+
]
|
|
19187
|
+
};
|
|
19188
|
+
}
|
|
19189
|
+
function collectExports(value, location, result) {
|
|
19190
|
+
if (value === null) return;
|
|
19191
|
+
if (typeof value === "string") {
|
|
19192
|
+
if (value.includes("*")) {
|
|
19193
|
+
result.errors.push(
|
|
19194
|
+
entryError(
|
|
19195
|
+
"exports",
|
|
19196
|
+
value,
|
|
19197
|
+
"unsupported_entry_shape",
|
|
19198
|
+
`${location} uses a wildcard target that the static gate cannot verify`
|
|
19199
|
+
)
|
|
19200
|
+
);
|
|
19201
|
+
} else {
|
|
19202
|
+
result.targets.push({ field: `exports ${location}`, target: value });
|
|
19203
|
+
}
|
|
19204
|
+
return;
|
|
19205
|
+
}
|
|
19206
|
+
if (Array.isArray(value)) {
|
|
19207
|
+
result.errors.push(
|
|
19208
|
+
entryError("exports", location, "unsupported_entry_shape", `${location} uses unsupported fallback-array semantics`)
|
|
19209
|
+
);
|
|
19210
|
+
return;
|
|
19211
|
+
}
|
|
19212
|
+
if (typeof value !== "object" || value === void 0) {
|
|
19213
|
+
result.errors.push(
|
|
19214
|
+
entryError("exports", String(value), "invalid_exports_type", `${location} has an unsupported exports value`)
|
|
19215
|
+
);
|
|
19216
|
+
return;
|
|
19217
|
+
}
|
|
19218
|
+
for (const [key, child] of Object.entries(value)) {
|
|
19219
|
+
const childLocation = `${location}.${key}`;
|
|
19220
|
+
if (key.includes("*")) {
|
|
19221
|
+
result.errors.push(
|
|
19222
|
+
entryError(
|
|
19223
|
+
"exports",
|
|
19224
|
+
key,
|
|
19225
|
+
"unsupported_entry_shape",
|
|
19226
|
+
`${childLocation} uses a wildcard subpath that the static gate cannot verify`
|
|
19227
|
+
)
|
|
19228
|
+
);
|
|
19229
|
+
continue;
|
|
19230
|
+
}
|
|
19231
|
+
collectExports(child, childLocation, result);
|
|
19232
|
+
}
|
|
19233
|
+
}
|
|
19234
|
+
function checkNpmEntryClosure(manifest, fileIndex) {
|
|
19235
|
+
const result = { entries: [], errors: [], diagnostics: [] };
|
|
19236
|
+
if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) {
|
|
19237
|
+
result.errors.push(entryError("manifest", "", "invalid_manifest", "manifest must be an object"));
|
|
19238
|
+
return result;
|
|
19239
|
+
}
|
|
19240
|
+
if (!(fileIndex instanceof Map)) {
|
|
19241
|
+
result.errors.push(entryError("fileIndex", "", "invalid_file_index", "fileIndex must be a Map"));
|
|
19242
|
+
return result;
|
|
19243
|
+
}
|
|
19244
|
+
if (manifest.bin !== void 0 && manifest.bin !== null) {
|
|
19245
|
+
if (typeof manifest.bin === "string") {
|
|
19246
|
+
const checked = checkTarget({ field: "bin", target: manifest.bin, fileIndex });
|
|
19247
|
+
result.entries.push(...checked.entries);
|
|
19248
|
+
result.errors.push(...checked.errors);
|
|
19249
|
+
} else if (typeof manifest.bin === "object" && !Array.isArray(manifest.bin)) {
|
|
19250
|
+
for (const [name, target] of Object.entries(manifest.bin)) {
|
|
19251
|
+
const checked = checkTarget({ field: "bin", target, fileIndex });
|
|
19252
|
+
result.entries.push(...checked.entries);
|
|
19253
|
+
result.errors.push(...checked.errors);
|
|
19254
|
+
}
|
|
19255
|
+
} else {
|
|
19256
|
+
result.errors.push(entryError("bin", String(manifest.bin), "invalid_type", "bin must be a string or object"));
|
|
19257
|
+
}
|
|
19258
|
+
}
|
|
19259
|
+
for (const field of SIMPLE_FIELDS) {
|
|
19260
|
+
if (manifest[field] === void 0 || manifest[field] === null) continue;
|
|
19261
|
+
const checked = checkTarget({ field, target: manifest[field], fileIndex });
|
|
19262
|
+
result.entries.push(...checked.entries);
|
|
19263
|
+
result.errors.push(...checked.errors);
|
|
19264
|
+
}
|
|
19265
|
+
if (manifest.exports !== void 0) {
|
|
19266
|
+
const exportsResult = { targets: [], errors: [], diagnostics: [] };
|
|
19267
|
+
collectExports(manifest.exports, "exports", exportsResult);
|
|
19268
|
+
result.errors.push(...exportsResult.errors);
|
|
19269
|
+
result.diagnostics.push(...exportsResult.diagnostics);
|
|
19270
|
+
for (const target of exportsResult.targets) {
|
|
19271
|
+
const checked = checkTarget({
|
|
19272
|
+
field: "exports",
|
|
19273
|
+
target: target.target,
|
|
19274
|
+
fileIndex,
|
|
19275
|
+
requireDotSlash: true
|
|
19276
|
+
});
|
|
19277
|
+
result.entries.push(...checked.entries);
|
|
19278
|
+
result.errors.push(...checked.errors);
|
|
19279
|
+
}
|
|
19280
|
+
}
|
|
19281
|
+
return result;
|
|
19282
|
+
}
|
|
19283
|
+
async function buildTarballFileIndex(tarballBytes, tarballDir = tmpdir()) {
|
|
19284
|
+
return buildNpmTarballFileIndex({ tarballBytes, tarballDir });
|
|
19285
|
+
}
|
|
19286
|
+
async function buildDirectoryFileIndex(packageDir) {
|
|
19287
|
+
const snapshot = await computeFrozenSnapshot(packageDir, { excludeRootEntries: [".git"] });
|
|
19288
|
+
return new Map(snapshot.entries.map((entry) => [entry.path, { type: entry.type }]));
|
|
19289
|
+
}
|
|
19290
|
+
var SIMPLE_FIELDS;
|
|
19291
|
+
var init_npm_entry_closure = __esm({
|
|
19292
|
+
"src/npm/npm-entry-closure.mjs"() {
|
|
19293
|
+
init_frozen();
|
|
19294
|
+
SIMPLE_FIELDS = ["main", "module", "types", "typings"];
|
|
19295
|
+
__name(entryError, "entryError");
|
|
19296
|
+
__name(validateRelativeTarget, "validateRelativeTarget");
|
|
19297
|
+
__name(checkTarget, "checkTarget");
|
|
19298
|
+
__name(collectExports, "collectExports");
|
|
19299
|
+
__name(checkNpmEntryClosure, "checkNpmEntryClosure");
|
|
19300
|
+
__name(buildTarballFileIndex, "buildTarballFileIndex");
|
|
19301
|
+
__name(buildDirectoryFileIndex, "buildDirectoryFileIndex");
|
|
19302
|
+
}
|
|
19303
|
+
});
|
|
19304
|
+
|
|
19305
|
+
// src/artifacts/safe-fs-backend-internal.mjs
|
|
19306
|
+
import { createRequire } from "node:module";
|
|
19307
|
+
import { dirname as dirname3, resolve as resolve8 } from "node:path";
|
|
19308
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
19309
|
+
import { realpath as realpath3 } from "node:fs/promises";
|
|
19310
|
+
import { readFileSync as readFileSync3, realpathSync as realpathSync2, lstatSync as lstatSync2 } from "node:fs";
|
|
19311
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
19312
|
+
function failPrebuildManifest(reason, message = "prebuilds manifest not readable") {
|
|
19313
|
+
throw new ReleaseError(
|
|
19314
|
+
SAFE_WRITE_UNAVAILABLE,
|
|
19315
|
+
message,
|
|
19316
|
+
{ resource: PREBUILDS_RESOURCE, reason }
|
|
19317
|
+
);
|
|
19318
|
+
}
|
|
19319
|
+
function readTrustedPrebuildManifest() {
|
|
19320
|
+
let lexicalStat;
|
|
19321
|
+
try {
|
|
19322
|
+
lexicalStat = lstatSync2(PREBUILDS_PATH, { bigint: true });
|
|
19323
|
+
} catch {
|
|
19324
|
+
failPrebuildManifest("MISSING");
|
|
19325
|
+
}
|
|
19326
|
+
if (lexicalStat.isSymbolicLink()) {
|
|
19327
|
+
failPrebuildManifest("SYMLINK", "prebuilds manifest must not be a symlink");
|
|
19328
|
+
}
|
|
19329
|
+
if (!lexicalStat.isFile()) {
|
|
19330
|
+
failPrebuildManifest("NOT_REGULAR_FILE", "prebuilds manifest is not a regular file");
|
|
19331
|
+
}
|
|
19332
|
+
if (lexicalStat.nlink !== 1n) {
|
|
19333
|
+
failPrebuildManifest("UNEXPECTED_HARDLINK_COUNT", "prebuilds manifest has invalid nlink count");
|
|
19334
|
+
}
|
|
19335
|
+
let physicalBase;
|
|
19336
|
+
let physicalManifest;
|
|
19337
|
+
try {
|
|
19338
|
+
physicalBase = realpathSync2(NATIVE_BASE);
|
|
19339
|
+
physicalManifest = realpathSync2(PREBUILDS_PATH);
|
|
19340
|
+
} catch {
|
|
19341
|
+
failPrebuildManifest("REALPATH_FAILED");
|
|
19342
|
+
}
|
|
19343
|
+
if (!physicalManifest.startsWith(physicalBase + "/") && physicalManifest !== physicalBase) {
|
|
19344
|
+
failPrebuildManifest(
|
|
19345
|
+
"PHYSICAL_PATH_ESCAPE",
|
|
19346
|
+
"prebuilds manifest physical path escapes base directory"
|
|
19347
|
+
);
|
|
19348
|
+
}
|
|
19349
|
+
try {
|
|
19350
|
+
return JSON.parse(readFileSync3(physicalManifest, "utf8"));
|
|
19351
|
+
} catch {
|
|
19352
|
+
failPrebuildManifest("READ_FAILED");
|
|
19353
|
+
}
|
|
19354
|
+
}
|
|
19355
|
+
function sanitizeError(err) {
|
|
19356
|
+
if (typeof err?.message === "string") {
|
|
19357
|
+
let msg = err.message.replace(/\/[^\s]*:\s*/g, "");
|
|
19358
|
+
msg = msg.replace(/\/\S+/g, "<redacted>");
|
|
19359
|
+
return msg;
|
|
19360
|
+
}
|
|
19361
|
+
return "native addon error";
|
|
19362
|
+
}
|
|
19363
|
+
function isWithinPackageBuildDir(addonPath) {
|
|
19364
|
+
const buildDir = resolve8(NATIVE_BASE, "build/");
|
|
19365
|
+
const resolved = resolve8(addonPath);
|
|
19366
|
+
if (!resolved.startsWith(buildDir + "/") && resolved !== buildDir) {
|
|
19367
|
+
return null;
|
|
19368
|
+
}
|
|
19369
|
+
let lstatResult;
|
|
19370
|
+
try {
|
|
19371
|
+
lstatResult = lstatSync2(addonPath, { bigint: true });
|
|
19372
|
+
} catch {
|
|
19373
|
+
return null;
|
|
19374
|
+
}
|
|
19375
|
+
if (lstatResult.isSymbolicLink()) return null;
|
|
19376
|
+
if (!lstatResult.isFile()) return null;
|
|
19377
|
+
if (lstatResult.nlink !== 1n) return null;
|
|
19378
|
+
try {
|
|
19379
|
+
const physicalBuildDir = realpathSync2(buildDir);
|
|
19380
|
+
const physicalAddon = realpathSync2(addonPath);
|
|
19381
|
+
if (!(physicalAddon.startsWith(physicalBuildDir + "/") || physicalAddon === physicalBuildDir)) {
|
|
19382
|
+
return null;
|
|
19383
|
+
}
|
|
19384
|
+
return physicalAddon;
|
|
19385
|
+
} catch {
|
|
19386
|
+
return null;
|
|
19387
|
+
}
|
|
19388
|
+
}
|
|
19389
|
+
function validateManifestEntry(entry, key) {
|
|
19390
|
+
if (typeof entry !== "object" || entry === null) {
|
|
19391
|
+
throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, `invalid manifest entry for ${key}`);
|
|
19392
|
+
}
|
|
19393
|
+
if (typeof entry.path !== "string" || entry.path.length === 0) {
|
|
19394
|
+
throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, `manifest entry missing path for ${key}`);
|
|
19395
|
+
}
|
|
19396
|
+
if (typeof entry.sha256 !== "string" || !/^[0-9a-f]{64}$/.test(entry.sha256)) {
|
|
19397
|
+
throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, `manifest entry missing or invalid sha256 for ${key}`);
|
|
19398
|
+
}
|
|
19399
|
+
if (typeof entry.exports !== "object" || !Array.isArray(entry.exports)) {
|
|
19400
|
+
throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, `manifest entry missing exports for ${key}`);
|
|
19401
|
+
}
|
|
19402
|
+
const resolved = resolve8(NATIVE_BASE, entry.path);
|
|
19403
|
+
const baseDir = NATIVE_BASE;
|
|
19404
|
+
if (!resolved.startsWith(baseDir + "/") && resolved !== baseDir) {
|
|
19405
|
+
throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, `manifest path escapes base directory for ${key}`);
|
|
19406
|
+
}
|
|
19407
|
+
const sortedActual = [...entry.exports].sort();
|
|
19408
|
+
const sortedExpected = [...EXPECTED_EXPORTS].sort();
|
|
19409
|
+
if (sortedActual.length !== sortedExpected.length || sortedActual.some((v, i) => v !== sortedExpected[i])) {
|
|
19410
|
+
throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, `manifest exports mismatch for ${key}`);
|
|
19411
|
+
}
|
|
19412
|
+
}
|
|
19413
|
+
function requireAddon(addonPath) {
|
|
19414
|
+
try {
|
|
19415
|
+
const require2 = createRequire(resolve8(PKG_ROOT2, "package.json"));
|
|
19416
|
+
return require2(addonPath);
|
|
19417
|
+
} catch (err) {
|
|
19418
|
+
throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, sanitizeError(err));
|
|
19419
|
+
}
|
|
19420
|
+
}
|
|
19421
|
+
function readStableAddon(options = {}) {
|
|
19422
|
+
const hooks = options._hooks || {};
|
|
19423
|
+
const manifest = readTrustedPrebuildManifest();
|
|
19424
|
+
if (!manifest.prebuilds || typeof manifest.prebuilds !== "object") {
|
|
19425
|
+
throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, "prebuilds manifest has invalid structure");
|
|
19426
|
+
}
|
|
19427
|
+
const key = `${process.platform}-${process.arch}`;
|
|
19428
|
+
const entry = manifest.prebuilds[key];
|
|
19429
|
+
if (!entry) {
|
|
19430
|
+
throw new ReleaseError(
|
|
19431
|
+
SAFE_WRITE_UNAVAILABLE,
|
|
19432
|
+
`no prebuilt addon for ${key}; build from source with native:build`
|
|
19433
|
+
);
|
|
19434
|
+
}
|
|
19435
|
+
validateManifestEntry(entry, key);
|
|
19436
|
+
const addonFile = resolve8(NATIVE_BASE, entry.path);
|
|
19437
|
+
const lstatFn = hooks.lstatSync || lstatSync2;
|
|
19438
|
+
const lstatOpts = { bigint: true };
|
|
19439
|
+
let preStat;
|
|
19440
|
+
try {
|
|
19441
|
+
preStat = lstatFn(addonFile, lstatOpts);
|
|
19442
|
+
if (preStat.isSymbolicLink()) {
|
|
19443
|
+
throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, "prebuilt addon must not be a symlink");
|
|
19444
|
+
}
|
|
19445
|
+
if (!preStat.isFile()) {
|
|
19446
|
+
throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, "prebuilt addon is not a regular file");
|
|
19447
|
+
}
|
|
19448
|
+
if (preStat.nlink !== 1n) {
|
|
19449
|
+
throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, "prebuilt addon has invalid nlink count");
|
|
19450
|
+
}
|
|
19451
|
+
} catch (e) {
|
|
19452
|
+
if (e instanceof ReleaseError) throw e;
|
|
19453
|
+
throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, "prebuilt addon not accessible");
|
|
19454
|
+
}
|
|
19455
|
+
let physicalAddonFile;
|
|
19456
|
+
try {
|
|
19457
|
+
physicalAddonFile = realpathSync2(addonFile);
|
|
19458
|
+
} catch {
|
|
19459
|
+
throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, "prebuilt addon not accessible");
|
|
19460
|
+
}
|
|
19461
|
+
const nativeBaseDir = realpathSync2(NATIVE_BASE);
|
|
19462
|
+
if (!physicalAddonFile.startsWith(nativeBaseDir + "/") && physicalAddonFile !== nativeBaseDir) {
|
|
19463
|
+
throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, "prebuilt addon physical path escapes base directory");
|
|
19464
|
+
}
|
|
19465
|
+
const readFn = hooks.readFileSync || readFileSync3;
|
|
19466
|
+
let data;
|
|
19467
|
+
try {
|
|
19468
|
+
data = readFn(physicalAddonFile);
|
|
19469
|
+
} catch {
|
|
19470
|
+
throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, "prebuilt addon not readable");
|
|
19471
|
+
}
|
|
19472
|
+
const hash = createHash3("sha256").update(data).digest("hex");
|
|
19473
|
+
if (hash !== entry.sha256) {
|
|
19474
|
+
throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, "prebuilt addon SHA-256 mismatch");
|
|
19475
|
+
}
|
|
19476
|
+
let postStat;
|
|
19477
|
+
try {
|
|
19478
|
+
postStat = lstatFn(addonFile, lstatOpts);
|
|
19479
|
+
} catch {
|
|
19480
|
+
throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, "prebuilt addon identity check failed");
|
|
19481
|
+
}
|
|
19482
|
+
if (preStat.dev !== postStat.dev || preStat.ino !== postStat.ino || preStat.mode !== postStat.mode || preStat.nlink !== postStat.nlink || preStat.size !== postStat.size || preStat.mtimeNs !== postStat.mtimeNs || preStat.ctimeNs !== postStat.ctimeNs) {
|
|
19483
|
+
throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, "prebuilt addon identity changed during read (TOCTOU detected)");
|
|
19484
|
+
}
|
|
19485
|
+
const addon = requireAddon(physicalAddonFile);
|
|
19486
|
+
let postRequireStat;
|
|
19487
|
+
try {
|
|
19488
|
+
postRequireStat = lstatFn(addonFile, lstatOpts);
|
|
19489
|
+
} catch {
|
|
19490
|
+
throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, "prebuilt addon identity check failed after require");
|
|
19491
|
+
}
|
|
19492
|
+
if (preStat.dev !== postRequireStat.dev || preStat.ino !== postRequireStat.ino || preStat.mode !== postRequireStat.mode || preStat.nlink !== postRequireStat.nlink || preStat.size !== postRequireStat.size || preStat.mtimeNs !== postRequireStat.mtimeNs || preStat.ctimeNs !== postRequireStat.ctimeNs) {
|
|
19493
|
+
throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, "prebuilt addon identity changed during require (TOCTOU detected)");
|
|
19494
|
+
}
|
|
19495
|
+
const addonExports = Object.keys(addon).sort();
|
|
19496
|
+
const expectedExports = [...EXPECTED_EXPORTS].sort();
|
|
19497
|
+
if (addonExports.length !== expectedExports.length || addonExports.some((v, i) => v !== expectedExports[i])) {
|
|
19498
|
+
throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, "addon exports do not match manifest");
|
|
19499
|
+
}
|
|
19500
|
+
return addon;
|
|
19501
|
+
}
|
|
19502
|
+
function loadNativeAddon(addonPath) {
|
|
19503
|
+
if (addonPath) {
|
|
19504
|
+
if (process.env.RELEASE_SKILL_NATIVE_DEV !== "1") {
|
|
19505
|
+
throw new ReleaseError(
|
|
19506
|
+
SAFE_WRITE_UNAVAILABLE,
|
|
19507
|
+
"explicit addonPath is only allowed in development mode"
|
|
19508
|
+
);
|
|
19509
|
+
}
|
|
19510
|
+
const physicalPath = isWithinPackageBuildDir(addonPath);
|
|
19511
|
+
if (!physicalPath) {
|
|
19512
|
+
throw new ReleaseError(
|
|
19513
|
+
SAFE_WRITE_UNAVAILABLE,
|
|
19514
|
+
"addonPath is outside the package build directory"
|
|
19515
|
+
);
|
|
19516
|
+
}
|
|
19517
|
+
return requireAddon(physicalPath);
|
|
19518
|
+
}
|
|
19519
|
+
if (process.env.RELEASE_SKILL_NATIVE_DEV === "1") {
|
|
19520
|
+
const devPath = DEFAULT_ADDON_PATH;
|
|
19521
|
+
const physicalPath = isWithinPackageBuildDir(devPath);
|
|
19522
|
+
if (!physicalPath) {
|
|
19523
|
+
throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, "dev addon path is outside build directory");
|
|
19524
|
+
}
|
|
19525
|
+
return requireAddon(physicalPath);
|
|
19526
|
+
}
|
|
19527
|
+
return readStableAddon();
|
|
19528
|
+
}
|
|
19529
|
+
function wrapHandle(addon, nativeHandle, nativeTokenMap, nativeIdentityMap) {
|
|
19530
|
+
const handle = {
|
|
19531
|
+
async openDir(name) {
|
|
19532
|
+
try {
|
|
19533
|
+
const nativeChild = addon.openDir(nativeHandle, name);
|
|
19534
|
+
return wrapHandle(addon, nativeChild, nativeTokenMap, nativeIdentityMap);
|
|
19535
|
+
} catch (err) {
|
|
19536
|
+
throw new ReleaseError(err.code || PATH_UNSAFE, sanitizeError(err));
|
|
19537
|
+
}
|
|
19538
|
+
},
|
|
19539
|
+
async readEntry(name) {
|
|
19540
|
+
try {
|
|
19541
|
+
return addon.readEntry(nativeHandle, name);
|
|
19542
|
+
} catch (err) {
|
|
19543
|
+
throw new ReleaseError(err.code || PATH_UNSAFE, sanitizeError(err));
|
|
19544
|
+
}
|
|
19545
|
+
},
|
|
19546
|
+
async readFile(name) {
|
|
18771
19547
|
try {
|
|
18772
19548
|
const result = addon.readFile(nativeHandle, name);
|
|
18773
19549
|
if (result === null) return null;
|
|
@@ -19066,11 +19842,11 @@ var PKG_ROOT2, NATIVE_BASE, PREBUILDS_PATH, PREBUILDS_RESOURCE, DEFAULT_ADDON_PA
|
|
|
19066
19842
|
var init_safe_fs_backend_internal = __esm({
|
|
19067
19843
|
"src/artifacts/safe-fs-backend-internal.mjs"() {
|
|
19068
19844
|
init_errors();
|
|
19069
|
-
PKG_ROOT2 = typeof __bundlePkgRoot !== "undefined" ? __bundlePkgRoot :
|
|
19070
|
-
NATIVE_BASE =
|
|
19071
|
-
PREBUILDS_PATH =
|
|
19845
|
+
PKG_ROOT2 = typeof __bundlePkgRoot !== "undefined" ? __bundlePkgRoot : resolve8(dirname3(fileURLToPath2(import.meta.url)), "..", "..");
|
|
19846
|
+
NATIVE_BASE = resolve8(PKG_ROOT2, "native/safe-write");
|
|
19847
|
+
PREBUILDS_PATH = resolve8(NATIVE_BASE, "prebuilds.json");
|
|
19072
19848
|
PREBUILDS_RESOURCE = "native/safe-write/prebuilds.json";
|
|
19073
|
-
DEFAULT_ADDON_PATH =
|
|
19849
|
+
DEFAULT_ADDON_PATH = resolve8(NATIVE_BASE, "build/Release/safe_write.node");
|
|
19074
19850
|
EXPECTED_EXPORTS = Object.freeze([
|
|
19075
19851
|
"openRoot",
|
|
19076
19852
|
"openDir",
|
|
@@ -19105,8 +19881,8 @@ __export(safe_fs_exports, {
|
|
|
19105
19881
|
loadSafeFs: () => loadSafeFs,
|
|
19106
19882
|
requireSafeFs: () => requireSafeFs
|
|
19107
19883
|
});
|
|
19108
|
-
import { realpath as
|
|
19109
|
-
import { tmpdir } from "node:os";
|
|
19884
|
+
import { realpath as realpath4 } from "node:fs/promises";
|
|
19885
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
19110
19886
|
async function loadSafeFs(options = {}) {
|
|
19111
19887
|
const addon = loadNativeAddon(options.addonPath);
|
|
19112
19888
|
return createBackend(addon);
|
|
@@ -19117,7 +19893,7 @@ async function requireSafeFs(options = {}) {
|
|
|
19117
19893
|
if (options.root) {
|
|
19118
19894
|
root = options.root;
|
|
19119
19895
|
} else {
|
|
19120
|
-
root = await
|
|
19896
|
+
root = await realpath4(tmpdir2());
|
|
19121
19897
|
}
|
|
19122
19898
|
const result = await backend.probe(root);
|
|
19123
19899
|
if (!result.supported) {
|
|
@@ -19142,26 +19918,26 @@ var setup_exports = {};
|
|
|
19142
19918
|
__export(setup_exports, {
|
|
19143
19919
|
setupProject: () => setupProject
|
|
19144
19920
|
});
|
|
19145
|
-
import { execFile as
|
|
19146
|
-
import { createHash as
|
|
19921
|
+
import { execFile as execFileCb2 } from "node:child_process";
|
|
19922
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
19147
19923
|
import { createReadStream } from "node:fs";
|
|
19148
|
-
import { promisify } from "node:util";
|
|
19924
|
+
import { promisify as promisify2 } from "node:util";
|
|
19149
19925
|
import {
|
|
19150
|
-
lstat as
|
|
19926
|
+
lstat as lstat5,
|
|
19151
19927
|
readFile as readFile7,
|
|
19152
|
-
readdir as
|
|
19153
|
-
realpath as
|
|
19928
|
+
readdir as readdir3,
|
|
19929
|
+
realpath as realpath5
|
|
19154
19930
|
} from "node:fs/promises";
|
|
19155
|
-
import { basename as basename2, dirname as dirname4, isAbsolute as
|
|
19931
|
+
import { basename as basename2, dirname as dirname4, isAbsolute as isAbsolute6, join as join8, relative as relative6, resolve as resolve9 } from "node:path";
|
|
19156
19932
|
function setupError(code, message, details = {}) {
|
|
19157
19933
|
return new ReleaseError(code, message, details);
|
|
19158
19934
|
}
|
|
19159
19935
|
function safeRelative(root, path3) {
|
|
19160
|
-
const rel =
|
|
19936
|
+
const rel = relative6(root, path3).split("\\").join("/");
|
|
19161
19937
|
return rel || ".";
|
|
19162
19938
|
}
|
|
19163
19939
|
async function readJsonBounded(path3, label) {
|
|
19164
|
-
const stat9 = await
|
|
19940
|
+
const stat9 = await lstat5(path3);
|
|
19165
19941
|
if (!stat9.isFile() || stat9.isSymbolicLink() || stat9.size > MAX_JSON_BYTES) {
|
|
19166
19942
|
throw setupError(CONFIG_INVALID, `${label} must be a regular JSON file no larger than 1 MiB`, { path: path3 });
|
|
19167
19943
|
}
|
|
@@ -19175,11 +19951,11 @@ async function walkDiscoveryFiles(root, maxDepth = 8) {
|
|
|
19175
19951
|
const found = [];
|
|
19176
19952
|
async function walk(directory, depth) {
|
|
19177
19953
|
if (depth > maxDepth) return;
|
|
19178
|
-
const children = await
|
|
19954
|
+
const children = await readdir3(directory, { withFileTypes: true });
|
|
19179
19955
|
children.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
19180
19956
|
for (const child of children) {
|
|
19181
19957
|
if (child.isSymbolicLink()) continue;
|
|
19182
|
-
const absolute =
|
|
19958
|
+
const absolute = join8(directory, child.name);
|
|
19183
19959
|
if (child.isDirectory()) {
|
|
19184
19960
|
if (!SKIP_DIRS.has(child.name)) await walk(absolute, depth + 1);
|
|
19185
19961
|
} else if (child.isFile() && (child.name === "package.json" || child.name === "public-release.json" || child.name === "SKILL.md" || /^README(?:\.|$)/i.test(child.name) || /^LICENSE(?:\.|$)/i.test(child.name) || /^CHANGELOG(?:\.|$)/i.test(child.name) || [...DISCOVERY_MANIFEST_SUFFIXES].some((suffix) => absolute.endsWith(`/${suffix}`)))) {
|
|
@@ -19192,11 +19968,11 @@ async function walkDiscoveryFiles(root, maxDepth = 8) {
|
|
|
19192
19968
|
return found;
|
|
19193
19969
|
}
|
|
19194
19970
|
async function digestFile(path3) {
|
|
19195
|
-
const before = await
|
|
19971
|
+
const before = await lstat5(path3);
|
|
19196
19972
|
if (!before.isFile() || before.isSymbolicLink()) throw setupError(CONFIG_INVALID, "discovered file must be regular", { path: path3 });
|
|
19197
|
-
const hash =
|
|
19973
|
+
const hash = createHash4("sha256");
|
|
19198
19974
|
for await (const chunk of createReadStream(path3)) hash.update(chunk);
|
|
19199
|
-
const after = await
|
|
19975
|
+
const after = await lstat5(path3);
|
|
19200
19976
|
if (before.size !== after.size || before.mtimeMs !== after.mtimeMs || before.ino !== after.ino) {
|
|
19201
19977
|
throw setupError(CONFIG_INVALID, "discovered file changed while setup was reading it", { path: path3 });
|
|
19202
19978
|
}
|
|
@@ -19235,6 +20011,8 @@ function summarizeLegacyReleaseConfig(value, path3) {
|
|
|
19235
20011
|
tagPrefix: optionalString(repo.tagPrefix),
|
|
19236
20012
|
npmPackage: optionalString(repo.npmPackage),
|
|
19237
20013
|
npmPackageDeclared: Object.hasOwn(repo, "npmPackage"),
|
|
20014
|
+
npmRequiredPathCandidates: stringList(repo.npmRequiredPackagePaths),
|
|
20015
|
+
npmRequiredPathsDeclared: Object.hasOwn(repo, "npmRequiredPackagePaths"),
|
|
19238
20016
|
docsSource: optionalString(repo.docsSource),
|
|
19239
20017
|
requiredPathCandidates: stringList(repo.requiredPackagePaths),
|
|
19240
20018
|
snapshotCommands: Array.isArray(repo.snapshotCommands) ? repo.snapshotCommands : []
|
|
@@ -19249,6 +20027,8 @@ function summarizeLegacyReleaseConfig(value, path3) {
|
|
|
19249
20027
|
tagPrefix: optionalString(value.tagPrefix),
|
|
19250
20028
|
npmPackage: plugins.filter((plugin) => plugin && typeof plugin === "object" && !Array.isArray(plugin)).map((plugin) => optionalString(plugin.npmPackage)).find(Boolean) ?? null,
|
|
19251
20029
|
npmPackageDeclared: plugins.some((plugin) => plugin && typeof plugin === "object" && !Array.isArray(plugin) && Object.hasOwn(plugin, "npmPackage")),
|
|
20030
|
+
npmRequiredPathCandidates: stringList(value.npmRequiredPackagePaths),
|
|
20031
|
+
npmRequiredPathsDeclared: Object.hasOwn(value, "npmRequiredPackagePaths"),
|
|
19252
20032
|
docsSource: null,
|
|
19253
20033
|
requiredPathCandidates: stringList(value.requiredPaths),
|
|
19254
20034
|
snapshotCommands: Array.isArray(value.snapshotCommands) ? value.snapshotCommands : []
|
|
@@ -19327,7 +20107,7 @@ function classifyScript(name, command2, unitId, distributionTypes) {
|
|
|
19327
20107
|
async function discoverGit(root) {
|
|
19328
20108
|
const run5 = /* @__PURE__ */ __name(async (args2) => {
|
|
19329
20109
|
try {
|
|
19330
|
-
const { stdout } = await
|
|
20110
|
+
const { stdout } = await execFile2("git", args2, { cwd: root, shell: false, encoding: "utf8", timeout: 5e3 });
|
|
19331
20111
|
return stdout.trim();
|
|
19332
20112
|
} catch {
|
|
19333
20113
|
return "";
|
|
@@ -19357,7 +20137,7 @@ async function discoverGit(root) {
|
|
|
19357
20137
|
async function discoverUnitGit(unitAbsDir, parentRoot) {
|
|
19358
20138
|
const run5 = /* @__PURE__ */ __name(async (cwd, args2) => {
|
|
19359
20139
|
try {
|
|
19360
|
-
const { stdout } = await
|
|
20140
|
+
const { stdout } = await execFile2("git", args2, { cwd, shell: false, encoding: "utf8", timeout: 5e3 });
|
|
19361
20141
|
return stdout.trim();
|
|
19362
20142
|
} catch {
|
|
19363
20143
|
return "";
|
|
@@ -19405,6 +20185,93 @@ async function discoverUnitGit(unitAbsDir, parentRoot) {
|
|
|
19405
20185
|
}
|
|
19406
20186
|
};
|
|
19407
20187
|
}
|
|
20188
|
+
async function pathIsGitIgnored(unitAbsDir, target) {
|
|
20189
|
+
try {
|
|
20190
|
+
await execFile2("git", ["check-ignore", "--no-index", "--quiet", "--", target], {
|
|
20191
|
+
cwd: unitAbsDir,
|
|
20192
|
+
shell: false,
|
|
20193
|
+
timeout: 5e3
|
|
20194
|
+
});
|
|
20195
|
+
return true;
|
|
20196
|
+
} catch {
|
|
20197
|
+
return false;
|
|
20198
|
+
}
|
|
20199
|
+
}
|
|
20200
|
+
async function classifyNpmEntryCandidate(unitAbsDir, target, trackedFiles) {
|
|
20201
|
+
const tracked = trackedFiles.includes(target);
|
|
20202
|
+
const ignored = await pathIsGitIgnored(unitAbsDir, target);
|
|
20203
|
+
let stat9 = null;
|
|
20204
|
+
try {
|
|
20205
|
+
stat9 = await lstat5(join8(unitAbsDir, target));
|
|
20206
|
+
} catch (error) {
|
|
20207
|
+
if (error.code !== "ENOENT") throw error;
|
|
20208
|
+
}
|
|
20209
|
+
const exists = stat9 !== null;
|
|
20210
|
+
const regular = stat9?.isFile() === true && stat9?.isSymbolicLink() !== true;
|
|
20211
|
+
const state = exists && !regular ? "NON_REGULAR" : exists && tracked ? "TRACKED_PRESENT" : exists && ignored ? "IGNORED_PRESENT" : exists ? "UNTRACKED_PRESENT" : ignored ? "IGNORED_MISSING" : "MISSING";
|
|
20212
|
+
return { path: target, state, exists, tracked, ignored };
|
|
20213
|
+
}
|
|
20214
|
+
async function discoverNpmEntryCandidates(root, pkg, matchingLegacyUnits, unitGitEntry) {
|
|
20215
|
+
const emptyIndex = /* @__PURE__ */ new Map();
|
|
20216
|
+
const semantic = checkNpmEntryClosure(pkg.entryManifest, emptyIndex);
|
|
20217
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
20218
|
+
const add = /* @__PURE__ */ __name((target, source) => {
|
|
20219
|
+
const current = byPath.get(target) ?? { path: target, sources: /* @__PURE__ */ new Set() };
|
|
20220
|
+
current.sources.add(source);
|
|
20221
|
+
byPath.set(target, current);
|
|
20222
|
+
}, "add");
|
|
20223
|
+
for (const entry of semantic.entries) add(entry.target, `package.json:${entry.field}`);
|
|
20224
|
+
const diagnostics = [
|
|
20225
|
+
...semantic.errors.filter((error) => error.reason !== "entry_missing").map((error) => ({ code: "NPM_ENTRY_DECLARATION_INVALID", ...error })),
|
|
20226
|
+
...semantic.diagnostics.map((diagnostic) => ({
|
|
20227
|
+
code: "NPM_ENTRY_WILDCARD_REQUIRES_REVIEW",
|
|
20228
|
+
...diagnostic
|
|
20229
|
+
}))
|
|
20230
|
+
];
|
|
20231
|
+
const declaredLegacyPaths = /* @__PURE__ */ new Set();
|
|
20232
|
+
const legacyCoverageDeclared = matchingLegacyUnits.some((unit) => unit.npmRequiredPathsDeclared);
|
|
20233
|
+
for (const legacy of matchingLegacyUnits) {
|
|
20234
|
+
for (const target of legacy.npmRequiredPathCandidates) {
|
|
20235
|
+
const checked = checkNpmEntryClosure({ main: target }, emptyIndex);
|
|
20236
|
+
const normalized = checked.entries[0]?.target;
|
|
20237
|
+
if (!normalized) {
|
|
20238
|
+
diagnostics.push({
|
|
20239
|
+
code: "LEGACY_NPM_REQUIRED_PATH_INVALID",
|
|
20240
|
+
path: target,
|
|
20241
|
+
reason: checked.errors[0]?.reason ?? "invalid_path"
|
|
20242
|
+
});
|
|
20243
|
+
continue;
|
|
20244
|
+
}
|
|
20245
|
+
declaredLegacyPaths.add(normalized);
|
|
20246
|
+
add(normalized, "public-release.json:npmRequiredPackagePaths");
|
|
20247
|
+
}
|
|
20248
|
+
}
|
|
20249
|
+
if (legacyCoverageDeclared) {
|
|
20250
|
+
for (const entry of semantic.entries) {
|
|
20251
|
+
if (!declaredLegacyPaths.has(entry.target)) {
|
|
20252
|
+
diagnostics.push({
|
|
20253
|
+
code: "LEGACY_NPM_ENTRY_COVERAGE_MISSING",
|
|
20254
|
+
path: entry.target,
|
|
20255
|
+
field: entry.field
|
|
20256
|
+
});
|
|
20257
|
+
}
|
|
20258
|
+
}
|
|
20259
|
+
}
|
|
20260
|
+
const unitAbsDir = resolve9(root, pkg.directory);
|
|
20261
|
+
const candidates = [];
|
|
20262
|
+
for (const item of [...byPath.values()].sort((a, b) => a.path.localeCompare(b.path))) {
|
|
20263
|
+
candidates.push({
|
|
20264
|
+
...await classifyNpmEntryCandidate(
|
|
20265
|
+
unitAbsDir,
|
|
20266
|
+
item.path,
|
|
20267
|
+
unitGitEntry?.trackedFiles ?? []
|
|
20268
|
+
),
|
|
20269
|
+
sources: [...item.sources].sort()
|
|
20270
|
+
});
|
|
20271
|
+
}
|
|
20272
|
+
diagnostics.sort((a, b) => canonicalJson(a).localeCompare(canonicalJson(b)));
|
|
20273
|
+
return { candidates, diagnostics };
|
|
20274
|
+
}
|
|
19408
20275
|
async function discoverFacts(root) {
|
|
19409
20276
|
const files = await walkDiscoveryFiles(root);
|
|
19410
20277
|
const packageFiles = files.filter((path3) => basename2(path3) === "package.json" && !/[\\/]adapters[\\/](?:claude|codex|kimi)[\\/]package\.json$/.test(path3));
|
|
@@ -19429,6 +20296,9 @@ async function discoverFacts(root) {
|
|
|
19429
20296
|
private: pkg.private === true,
|
|
19430
20297
|
repository: parseGithubRepo(pkg.repository),
|
|
19431
20298
|
publishRegistry: typeof pkg.publishConfig?.registry === "string" ? pkg.publishConfig.registry : null,
|
|
20299
|
+
entryManifest: Object.fromEntries(
|
|
20300
|
+
["bin", "main", "module", "types", "typings", "exports"].filter((field) => Object.hasOwn(pkg, field)).map((field) => [field, pkg[field]])
|
|
20301
|
+
),
|
|
19432
20302
|
files: Array.isArray(pkg.files) ? pkg.files.filter((item) => typeof item === "string").sort() : [],
|
|
19433
20303
|
scripts: Object.fromEntries(Object.entries(pkg.scripts ?? {}).filter(([, value]) => typeof value === "string").sort(([a], [b]) => a.localeCompare(b)))
|
|
19434
20304
|
});
|
|
@@ -19470,8 +20340,18 @@ async function discoverFacts(root) {
|
|
|
19470
20340
|
}).filter((item) => item.name).sort((a, b) => canonicalJson(a).localeCompare(canonicalJson(b)));
|
|
19471
20341
|
const unitGit = {};
|
|
19472
20342
|
for (const pkg of packages) {
|
|
19473
|
-
const unitAbsDir =
|
|
20343
|
+
const unitAbsDir = resolve9(root, pkg.directory);
|
|
19474
20344
|
unitGit[pkg.directory] = await discoverUnitGit(unitAbsDir, root);
|
|
20345
|
+
const inferredId = safeUnitId(pkg, pkg.directory);
|
|
20346
|
+
const matchingLegacyUnits = legacyReleaseConfigs.flatMap((config) => config.releaseUnits).filter((unit) => unit.id === inferredId || unit.source === pkg.directory || unit.source === dirname4(pkg.path));
|
|
20347
|
+
const npmDiscovery = await discoverNpmEntryCandidates(
|
|
20348
|
+
root,
|
|
20349
|
+
pkg,
|
|
20350
|
+
matchingLegacyUnits,
|
|
20351
|
+
unitGit[pkg.directory]
|
|
20352
|
+
);
|
|
20353
|
+
pkg.npmEntryCandidates = npmDiscovery.candidates;
|
|
20354
|
+
pkg.npmEntryDiagnostics = npmDiscovery.diagnostics;
|
|
19475
20355
|
}
|
|
19476
20356
|
return { git: git2, packages, manifests, skills, legacyReleaseConfigs, fileDigests, unitGit };
|
|
19477
20357
|
}
|
|
@@ -19708,6 +20588,8 @@ function buildCandidates(facts) {
|
|
|
19708
20588
|
publicFileMappingCandidates: mappingResult.mappings,
|
|
19709
20589
|
publicFileMappingConflicts: mappingResult.conflicts,
|
|
19710
20590
|
requiredPublicFileCandidates,
|
|
20591
|
+
npmEntryCandidates: pkg.npmEntryCandidates ?? [],
|
|
20592
|
+
npmEntryDiagnostics: pkg.npmEntryDiagnostics ?? [],
|
|
19711
20593
|
entrySkillCandidates: entrySkillCandidatesForUnit(facts, pkg, id),
|
|
19712
20594
|
...hasAuthorityConflict ? {
|
|
19713
20595
|
authorityConflict: {
|
|
@@ -19828,6 +20710,18 @@ function buildRecommendedProposal(facts, candidates) {
|
|
|
19828
20710
|
assumptions
|
|
19829
20711
|
};
|
|
19830
20712
|
}
|
|
20713
|
+
if (unit.distributionCandidates.includes("npm") && ((unit.npmEntryCandidates ?? []).some((candidate) => candidate.state !== "TRACKED_PRESENT") || (unit.npmEntryDiagnostics ?? []).length > 0)) {
|
|
20714
|
+
return {
|
|
20715
|
+
answers: null,
|
|
20716
|
+
conflicts: [{
|
|
20717
|
+
code: "NPM_ENTRY_REVIEW_REQUIRED",
|
|
20718
|
+
unit: unit.id,
|
|
20719
|
+
candidates: unit.npmEntryCandidates,
|
|
20720
|
+
diagnostics: unit.npmEntryDiagnostics
|
|
20721
|
+
}],
|
|
20722
|
+
assumptions
|
|
20723
|
+
};
|
|
20724
|
+
}
|
|
19831
20725
|
const repoOwner = publicRepo.includes("/") ? publicRepo.split("/")[0] : null;
|
|
19832
20726
|
const npmPublisher = legacyOwner ?? repoOwner ?? null;
|
|
19833
20727
|
const pkg = facts.packages.find((item) => item.directory === unit.source);
|
|
@@ -20001,6 +20895,12 @@ function buildDecisionsRequired(candidates, localOnly) {
|
|
|
20001
20895
|
id: `unit:${unit.id}:distributions-and-files`,
|
|
20002
20896
|
description: `\u9010\u9879\u786E\u8BA4\u6E20\u9053 ${JSON.stringify(unit.distributionCandidates)}\u3001\u516C\u5F00\u6587\u4EF6\u8FB9\u754C\u548C requiredPublicFiles\uFF1B\u5019\u9009\u4E0D\u662F\u6388\u6743\u3002`
|
|
20003
20897
|
});
|
|
20898
|
+
if ((unit.npmEntryCandidates ?? []).length > 0 || (unit.npmEntryDiagnostics ?? []).length > 0) {
|
|
20899
|
+
decisions.push({
|
|
20900
|
+
id: `unit:${unit.id}:npm-entry-closure`,
|
|
20901
|
+
description: "\u5BA1\u9605 package.json \u5165\u53E3\u53CA\u65E7 npmRequiredPackagePaths \u7684 tracked/untracked/ignored/missing \u72B6\u6001\uFF1Bsetup \u53EA\u62A5\u544A\u5019\u9009\uFF0C\u4E0D\u81EA\u52A8\u5199\u5165 publicFiles \u6216 requiredPublicFiles\u3002"
|
|
20902
|
+
});
|
|
20903
|
+
}
|
|
20004
20904
|
}
|
|
20005
20905
|
decisions.push({
|
|
20006
20906
|
id: "verification-gates",
|
|
@@ -20019,7 +20919,9 @@ function buildCompactSummary(report) {
|
|
|
20019
20919
|
branchCandidates: [...unit.branchCandidates].sort(),
|
|
20020
20920
|
distributionCandidates: [...unit.distributionCandidates].sort(),
|
|
20021
20921
|
publicFileMappingCount: (unit.publicFileMappingCandidates ?? []).length,
|
|
20022
|
-
requiredPublicFileCount: (unit.requiredPublicFileCandidates ?? []).length
|
|
20922
|
+
requiredPublicFileCount: (unit.requiredPublicFileCandidates ?? []).length,
|
|
20923
|
+
npmEntryCandidates: unit.npmEntryCandidates ?? [],
|
|
20924
|
+
npmEntryDiagnostics: unit.npmEntryDiagnostics ?? []
|
|
20023
20925
|
})),
|
|
20024
20926
|
gateCandidates: (report.gateCandidates ?? []).map((gate) => ({
|
|
20025
20927
|
id: gate.id,
|
|
@@ -20133,8 +21035,8 @@ async function assertConfigDirectoryStillBound(root, safeFs, expected) {
|
|
|
20133
21035
|
async function createConfigOnce(root, config, { beforeRename } = {}) {
|
|
20134
21036
|
const { loadSafeFs: loadSafeFs2 } = await Promise.resolve().then(() => (init_safe_fs(), safe_fs_exports));
|
|
20135
21037
|
const safeFs = await loadSafeFs2();
|
|
20136
|
-
const releaseDir =
|
|
20137
|
-
const target =
|
|
21038
|
+
const releaseDir = join8(root, ".release-skill");
|
|
21039
|
+
const target = join8(releaseDir, "project.yaml");
|
|
20138
21040
|
const bound = await openBoundConfigDirectory(root, safeFs);
|
|
20139
21041
|
let tempToken;
|
|
20140
21042
|
const bytes = Buffer.from(import_yaml.default.stringify(config, { lineWidth: 0 }), "utf8");
|
|
@@ -20188,16 +21090,16 @@ async function createConfigOnce(root, config, { beforeRename } = {}) {
|
|
|
20188
21090
|
}
|
|
20189
21091
|
}
|
|
20190
21092
|
async function setupProject({ root, answersPath, write = false, confirmSetup, faultInjector } = {}) {
|
|
20191
|
-
if (!root || typeof root !== "string" || !
|
|
21093
|
+
if (!root || typeof root !== "string" || !isAbsolute6(root)) {
|
|
20192
21094
|
throw setupError(CONFIG_INVALID, "setup root must be an absolute path");
|
|
20193
21095
|
}
|
|
20194
|
-
const rootReal = await
|
|
21096
|
+
const rootReal = await realpath5(root).catch((error) => {
|
|
20195
21097
|
throw setupError(CONFIG_INVALID, `cannot resolve setup root: ${error.message}`);
|
|
20196
21098
|
});
|
|
20197
|
-
const configPath =
|
|
21099
|
+
const configPath = join8(rootReal, ".release-skill", "project.yaml");
|
|
20198
21100
|
let configExists = false;
|
|
20199
21101
|
try {
|
|
20200
|
-
const stat9 = await
|
|
21102
|
+
const stat9 = await lstat5(configPath);
|
|
20201
21103
|
configExists = true;
|
|
20202
21104
|
if (!stat9.isFile() || stat9.isSymbolicLink()) {
|
|
20203
21105
|
throw setupError(CONFIG_INVALID, "existing project.yaml must be a regular file");
|
|
@@ -20253,7 +21155,8 @@ async function setupProject({ root, answersPath, write = false, confirmSetup, fa
|
|
|
20253
21155
|
...parseError ? ["\u5DF2\u6709\u914D\u7F6E\u65E0\u6CD5\u89E3\u6790\uFF1B\u5148\u4EBA\u5DE5\u4FEE\u590D\uFF0C\u518D\u8FD0\u884C release-assess\u3002"] : [],
|
|
20254
21156
|
...validationErrors.length > 0 ? ["\u5DF2\u6709\u914D\u7F6E\u4E0D\u7B26\u5408 release-project schema\uFF1B\u6309 validationErrors \u4EBA\u5DE5\u589E\u91CF\u4FEE\u590D\uFF0C\u4E0D\u91CD\u65B0\u751F\u6210\u3002"] : [],
|
|
20255
21157
|
...canonicalJson(configuredUnitIds) !== canonicalJson(discoveredUnitIds) ? ["\u53D1\u73B0\u7684\u53D1\u5E03\u5355\u5143\u4E0E\u5DF2\u6709\u914D\u7F6E\u4E0D\u540C\uFF1B\u4EBA\u5DE5\u6BD4\u8F83\u540E\u4EC5\u505A\u589E\u91CF\u7F16\u8F91\uFF0C\u4E0D\u91CD\u65B0\u751F\u6210\u3002"] : [],
|
|
20256
|
-
...unconfiguredGateCandidateIds.length > 0 ? ["\u5B58\u5728\u672A\u914D\u7F6E\u7684\u9A8C\u8BC1\u5019\u9009\uFF1B\u9010\u9879\u5BA1\u9605\u526F\u4F5C\u7528\u540E\u51B3\u5B9A\u662F\u5426\u4EBA\u5DE5\u6CE8\u518C\u3002"] : []
|
|
21158
|
+
...unconfiguredGateCandidateIds.length > 0 ? ["\u5B58\u5728\u672A\u914D\u7F6E\u7684\u9A8C\u8BC1\u5019\u9009\uFF1B\u9010\u9879\u5BA1\u9605\u526F\u4F5C\u7528\u540E\u51B3\u5B9A\u662F\u5426\u4EBA\u5DE5\u6CE8\u518C\u3002"] : [],
|
|
21159
|
+
...candidates2.units.some((unit) => (unit.npmEntryCandidates ?? []).some((candidate) => candidate.state !== "TRACKED_PRESENT") || (unit.npmEntryDiagnostics ?? []).length > 0) ? ["npm \u5165\u53E3\u5019\u9009\u5B58\u5728\u672A\u8DDF\u8E2A\u3001\u88AB\u5FFD\u7565\u3001\u7F3A\u5931\u3001\u975E\u666E\u901A\u6587\u4EF6\u6216\u58F0\u660E\u8986\u76D6\u7591\u70B9\uFF1B\u4EBA\u5DE5\u589E\u91CF\u4FEE\u590D\u914D\u7F6E\u6216\u6784\u5EFA\u6D41\u7A0B\uFF0Csetup \u4E0D\u81EA\u52A8\u6539\u5199\u3002"] : []
|
|
20257
21160
|
]
|
|
20258
21161
|
},
|
|
20259
21162
|
recommendedGateIds: [],
|
|
@@ -20272,7 +21175,7 @@ async function setupProject({ root, answersPath, write = false, confirmSetup, fa
|
|
|
20272
21175
|
)].sort();
|
|
20273
21176
|
let answers = null;
|
|
20274
21177
|
if (answersPath) {
|
|
20275
|
-
const resolvedAnswers =
|
|
21178
|
+
const resolvedAnswers = isAbsolute6(answersPath) ? answersPath : resolve9(rootReal, answersPath);
|
|
20276
21179
|
answers = await readJsonBounded(resolvedAnswers, "setup answers");
|
|
20277
21180
|
validateAnswers(answers, candidates.gates);
|
|
20278
21181
|
}
|
|
@@ -20326,7 +21229,7 @@ async function setupProject({ root, answersPath, write = false, confirmSetup, fa
|
|
|
20326
21229
|
if (faultInjector) await faultInjector("before-config-commit");
|
|
20327
21230
|
const lockedFacts = await discoverFacts(rootReal);
|
|
20328
21231
|
const lockedCandidates = buildCandidates(lockedFacts);
|
|
20329
|
-
const resolvedAnswers =
|
|
21232
|
+
const resolvedAnswers = isAbsolute6(answersPath) ? answersPath : resolve9(rootReal, answersPath);
|
|
20330
21233
|
const lockedAnswers = await readJsonBounded(resolvedAnswers, "setup answers");
|
|
20331
21234
|
validateAnswers(lockedAnswers, lockedCandidates.gates);
|
|
20332
21235
|
const lockedAuthority = {
|
|
@@ -20399,7 +21302,7 @@ async function setupProject({ root, answersPath, write = false, confirmSetup, fa
|
|
|
20399
21302
|
createdReport.compactSummary = buildCompactSummary(createdReport);
|
|
20400
21303
|
return createdReport;
|
|
20401
21304
|
}
|
|
20402
|
-
var import_yaml, import_ajv2, import_ajv_formats2,
|
|
21305
|
+
var import_yaml, import_ajv2, import_ajv_formats2, execFile2, SKIP_DIRS, DISCOVERY_MANIFEST_SUFFIXES, MANIFEST_SUFFIX_HOST_MAP, MAX_JSON_BYTES, schema, ajv2, validateProjectConfig;
|
|
20403
21306
|
var init_setup = __esm({
|
|
20404
21307
|
async "src/commands/setup.mjs"() {
|
|
20405
21308
|
import_yaml = __toESM(require_dist(), 1);
|
|
@@ -20408,9 +21311,10 @@ var init_setup = __esm({
|
|
|
20408
21311
|
init_digest();
|
|
20409
21312
|
init_project_lock();
|
|
20410
21313
|
await init_registry();
|
|
21314
|
+
init_npm_entry_closure();
|
|
20411
21315
|
init_errors();
|
|
20412
21316
|
init_trusted_resource();
|
|
20413
|
-
|
|
21317
|
+
execFile2 = promisify2(execFileCb2);
|
|
20414
21318
|
SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
20415
21319
|
".git",
|
|
20416
21320
|
".release-skill",
|
|
@@ -20500,6 +21404,9 @@ var init_setup = __esm({
|
|
|
20500
21404
|
__name(classifyScript, "classifyScript");
|
|
20501
21405
|
__name(discoverGit, "discoverGit");
|
|
20502
21406
|
__name(discoverUnitGit, "discoverUnitGit");
|
|
21407
|
+
__name(pathIsGitIgnored, "pathIsGitIgnored");
|
|
21408
|
+
__name(classifyNpmEntryCandidate, "classifyNpmEntryCandidate");
|
|
21409
|
+
__name(discoverNpmEntryCandidates, "discoverNpmEntryCandidates");
|
|
20503
21410
|
__name(discoverFacts, "discoverFacts");
|
|
20504
21411
|
__name(packagePatternMatches, "packagePatternMatches");
|
|
20505
21412
|
__name(buildPublicFileMappingCandidates, "buildPublicFileMappingCandidates");
|
|
@@ -20561,7 +21468,7 @@ var init_path_key = __esm({
|
|
|
20561
21468
|
});
|
|
20562
21469
|
|
|
20563
21470
|
// src/snapshot/public-path.mjs
|
|
20564
|
-
import { isAbsolute as
|
|
21471
|
+
import { isAbsolute as isAbsolute7, relative as relative7, resolve as resolve10 } from "node:path";
|
|
20565
21472
|
function canonicalPublicPath(raw, { allowDot = false } = {}) {
|
|
20566
21473
|
if (allowDot && raw === ".") {
|
|
20567
21474
|
return { path: "." };
|
|
@@ -20591,11 +21498,11 @@ function resolveUnitScopedPath(baseDir, unitId, { suffix = "" } = {}) {
|
|
|
20591
21498
|
{ unitId }
|
|
20592
21499
|
);
|
|
20593
21500
|
}
|
|
20594
|
-
const base =
|
|
20595
|
-
const candidate =
|
|
20596
|
-
const rel =
|
|
21501
|
+
const base = resolve10(baseDir);
|
|
21502
|
+
const candidate = resolve10(base, `${unitId}${suffix}`);
|
|
21503
|
+
const rel = relative7(base, candidate);
|
|
20597
21504
|
const separator = process.platform === "win32" ? "\\" : "/";
|
|
20598
|
-
if (rel === "" ||
|
|
21505
|
+
if (rel === "" || isAbsolute7(rel) || rel === ".." || rel.startsWith(`..${separator}`)) {
|
|
20599
21506
|
throw new ReleaseError(
|
|
20600
21507
|
PUBLIC_PATH_FORBIDDEN,
|
|
20601
21508
|
`release unit path escapes its trusted base: "${unitId}"`,
|
|
@@ -20617,11 +21524,11 @@ var init_public_path = __esm({
|
|
|
20617
21524
|
});
|
|
20618
21525
|
|
|
20619
21526
|
// src/core/baseline.mjs
|
|
20620
|
-
import { execFile as
|
|
20621
|
-
import { promisify as
|
|
20622
|
-
import { createHash as
|
|
20623
|
-
import { lstat as
|
|
20624
|
-
import { constants as
|
|
21527
|
+
import { execFile as execFileCb3 } from "node:child_process";
|
|
21528
|
+
import { promisify as promisify3 } from "node:util";
|
|
21529
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
21530
|
+
import { lstat as lstat6, open as open4, readlink } from "node:fs/promises";
|
|
21531
|
+
import { constants as fsConstants2 } from "node:fs";
|
|
20625
21532
|
import path from "node:path";
|
|
20626
21533
|
function isControlPlanePath(p) {
|
|
20627
21534
|
const normalized = p;
|
|
@@ -20667,9 +21574,9 @@ async function computeWorkspaceDigest(root) {
|
|
|
20667
21574
|
{ stdout: changedOut },
|
|
20668
21575
|
{ stdout: untrackedOut }
|
|
20669
21576
|
] = await Promise.all([
|
|
20670
|
-
|
|
20671
|
-
|
|
20672
|
-
|
|
21577
|
+
execFile3("git", ["ls-files", "-s", "-z"], opts),
|
|
21578
|
+
execFile3("git", ["diff", "--name-only", "-z"], opts),
|
|
21579
|
+
execFile3("git", ["ls-files", "-o", "--exclude-standard", "-z"], opts)
|
|
20673
21580
|
]);
|
|
20674
21581
|
const parts = [];
|
|
20675
21582
|
for (const record of splitNul(stagedOut)) {
|
|
@@ -20682,7 +21589,7 @@ async function computeWorkspaceDigest(root) {
|
|
|
20682
21589
|
const diffOpts = { ...opts, maxBuffer: DIFF_MAX_BUFFER };
|
|
20683
21590
|
const changedPaths = splitNul(changedOut).filter((p) => !isControlPlanePath(p));
|
|
20684
21591
|
for (const changedPath of changedPaths) {
|
|
20685
|
-
const { stdout: patch } = await
|
|
21592
|
+
const { stdout: patch } = await execFile3(
|
|
20686
21593
|
"git",
|
|
20687
21594
|
["diff", "--no-ext-diff", "--no-textconv", "--binary", "--no-color", "--", changedPath],
|
|
20688
21595
|
diffOpts
|
|
@@ -20699,7 +21606,7 @@ async function computeWorkspaceDigest(root) {
|
|
|
20699
21606
|
`Refusing to read path outside repository root: ${relPath} resolves to ${absPath}`
|
|
20700
21607
|
);
|
|
20701
21608
|
}
|
|
20702
|
-
const stat9 = await
|
|
21609
|
+
const stat9 = await lstat6(absPath);
|
|
20703
21610
|
let type;
|
|
20704
21611
|
if (stat9.isSymbolicLink()) {
|
|
20705
21612
|
type = "symlink";
|
|
@@ -20713,7 +21620,7 @@ async function computeWorkspaceDigest(root) {
|
|
|
20713
21620
|
const typeMarker = `TYPE:${type}`;
|
|
20714
21621
|
let content;
|
|
20715
21622
|
if (stat9.isFile()) {
|
|
20716
|
-
const handle = await
|
|
21623
|
+
const handle = await open4(absPath, fsConstants2.O_RDONLY | fsConstants2.O_NOFOLLOW);
|
|
20717
21624
|
try {
|
|
20718
21625
|
const fdStat = await handle.stat();
|
|
20719
21626
|
if (fdStat.ino !== stat9.ino) {
|
|
@@ -20739,21 +21646,21 @@ async function computeWorkspaceDigest(root) {
|
|
|
20739
21646
|
return [
|
|
20740
21647
|
relPath,
|
|
20741
21648
|
typeMarker,
|
|
20742
|
-
|
|
21649
|
+
createHash5("sha256").update(content).digest("hex")
|
|
20743
21650
|
].join(" ");
|
|
20744
21651
|
})
|
|
20745
21652
|
);
|
|
20746
21653
|
parts.sort();
|
|
20747
21654
|
untrackedParts.sort();
|
|
20748
21655
|
const combined = parts.join("\n") + (parts.length && untrackedParts.length ? "\n" : "") + untrackedParts.join("\n");
|
|
20749
|
-
return
|
|
21656
|
+
return createHash5("sha256").update(combined).digest("hex");
|
|
20750
21657
|
}
|
|
20751
21658
|
async function captureBaseline(root) {
|
|
20752
21659
|
const opts = { cwd: root, shell: false, encoding: "utf8" };
|
|
20753
21660
|
const [{ stdout: headOut }, { stdout: treeOut }, { stdout: statusOut }] = await Promise.all([
|
|
20754
|
-
|
|
20755
|
-
|
|
20756
|
-
|
|
21661
|
+
execFile3("git", ["rev-parse", "HEAD"], opts),
|
|
21662
|
+
execFile3("git", ["rev-parse", "HEAD^{tree}"], opts),
|
|
21663
|
+
execFile3("git", ["status", "--porcelain=v1", "-z", "--untracked-files=all"], opts)
|
|
20757
21664
|
]);
|
|
20758
21665
|
const gitHead = headOut.trim();
|
|
20759
21666
|
const gitTreeHash = treeOut.trim();
|
|
@@ -20768,10 +21675,10 @@ async function captureBaseline(root) {
|
|
|
20768
21675
|
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
20769
21676
|
};
|
|
20770
21677
|
}
|
|
20771
|
-
var
|
|
21678
|
+
var execFile3, WORKSPACE_DIGEST_ALGORITHM, CONTROL_PLANE_PREFIXES, RESERVED_CONTROL_PREFIXES, CONTROL_PLANE_EXACT, DIGEST, IMMUTABLE_PLAN, IMMUTABLE_APPROVAL;
|
|
20772
21679
|
var init_baseline = __esm({
|
|
20773
21680
|
"src/core/baseline.mjs"() {
|
|
20774
|
-
|
|
21681
|
+
execFile3 = promisify3(execFileCb3);
|
|
20775
21682
|
WORKSPACE_DIGEST_ALGORITHM = "git-workspace-v2";
|
|
20776
21683
|
CONTROL_PLANE_PREFIXES = [
|
|
20777
21684
|
".release-skill/lock",
|
|
@@ -20823,7 +21730,7 @@ var init_baseline = __esm({
|
|
|
20823
21730
|
|
|
20824
21731
|
// src/core/config.mjs
|
|
20825
21732
|
import { readFile as readFile8 } from "node:fs/promises";
|
|
20826
|
-
import { resolve as
|
|
21733
|
+
import { resolve as resolve11, isAbsolute as isAbsolute8, relative as relative8, normalize as normalize2 } from "node:path";
|
|
20827
21734
|
function containsAlias(node) {
|
|
20828
21735
|
if (!node || typeof node !== "object") {
|
|
20829
21736
|
return false;
|
|
@@ -20848,9 +21755,9 @@ function containsAlias(node) {
|
|
|
20848
21755
|
}
|
|
20849
21756
|
function resolveConfigPath(root, configPath) {
|
|
20850
21757
|
const rootNorm = normalize2(root);
|
|
20851
|
-
const resolved =
|
|
20852
|
-
const rel =
|
|
20853
|
-
if (rel.startsWith("..") || rel === ".." ||
|
|
21758
|
+
const resolved = isAbsolute8(configPath) ? normalize2(configPath) : resolve11(rootNorm, configPath);
|
|
21759
|
+
const rel = relative8(rootNorm, resolved);
|
|
21760
|
+
if (rel.startsWith("..") || rel === ".." || isAbsolute8(rel)) {
|
|
20854
21761
|
throw new ReleaseError(
|
|
20855
21762
|
CONFIG_INVALID,
|
|
20856
21763
|
`config path "${configPath}" resolves outside project root "${root}"`,
|
|
@@ -21147,10 +22054,10 @@ __export(assess_exports, {
|
|
|
21147
22054
|
Severity: () => Severity,
|
|
21148
22055
|
assessProject: () => assessProject
|
|
21149
22056
|
});
|
|
21150
|
-
import { readFile as readFile9, stat as stat2, writeFile as writeFile4, mkdir as
|
|
21151
|
-
import { resolve as
|
|
21152
|
-
import { execFile as
|
|
21153
|
-
import { promisify as
|
|
22057
|
+
import { readFile as readFile9, stat as stat2, writeFile as writeFile4, mkdir as mkdir4 } from "node:fs/promises";
|
|
22058
|
+
import { resolve as resolve12, dirname as dirname5, relative as relative9 } from "node:path";
|
|
22059
|
+
import { execFile as execFileCb4 } from "node:child_process";
|
|
22060
|
+
import { promisify as promisify4 } from "node:util";
|
|
21154
22061
|
async function fileExists(filePath) {
|
|
21155
22062
|
try {
|
|
21156
22063
|
await stat2(filePath);
|
|
@@ -21249,9 +22156,9 @@ async function checkCommonDocs(root, config) {
|
|
|
21249
22156
|
{ file: "LICENSE", code: "LICENSE_MISSING", message: "\u7F3A\u5C11 LICENSE \u6587\u4EF6" }
|
|
21250
22157
|
];
|
|
21251
22158
|
for (const unit of units) {
|
|
21252
|
-
const unitRoot =
|
|
22159
|
+
const unitRoot = resolve12(root, unit.source);
|
|
21253
22160
|
for (const doc of requiredDocs) {
|
|
21254
|
-
const exists = await fileExists(
|
|
22161
|
+
const exists = await fileExists(resolve12(unitRoot, doc.file));
|
|
21255
22162
|
if (!exists) {
|
|
21256
22163
|
gaps.push(
|
|
21257
22164
|
createGap({
|
|
@@ -21272,9 +22179,9 @@ async function checkCommonDocs(root, config) {
|
|
|
21272
22179
|
{ file: "CONTRIBUTING.md", code: "CONTRIBUTING_MISSING", message: "\u5EFA\u8BAE\u6DFB\u52A0 CONTRIBUTING.md" }
|
|
21273
22180
|
];
|
|
21274
22181
|
for (const unit of units) {
|
|
21275
|
-
const unitRoot =
|
|
22182
|
+
const unitRoot = resolve12(root, unit.source);
|
|
21276
22183
|
for (const doc of recommendedDocs) {
|
|
21277
|
-
const exists = await fileExists(
|
|
22184
|
+
const exists = await fileExists(resolve12(unitRoot, doc.file));
|
|
21278
22185
|
if (!exists) {
|
|
21279
22186
|
gaps.push(
|
|
21280
22187
|
createGap({
|
|
@@ -21296,9 +22203,9 @@ async function checkPluginManifests(root, config) {
|
|
|
21296
22203
|
const units = config.releaseUnits ?? [];
|
|
21297
22204
|
for (const unit of units) {
|
|
21298
22205
|
const distributionTypes = new Set((unit.distributions ?? []).map((dist) => dist.type));
|
|
21299
|
-
const unitRoot =
|
|
22206
|
+
const unitRoot = resolve12(root, unit.source);
|
|
21300
22207
|
if (distributionTypes.has("claude-plugin")) {
|
|
21301
|
-
const manifestPath =
|
|
22208
|
+
const manifestPath = resolve12(unitRoot, ".claude-plugin", "plugin.json");
|
|
21302
22209
|
const displayPath = unitFile(unit, ".claude-plugin/plugin.json");
|
|
21303
22210
|
const exists = await fileExists(manifestPath);
|
|
21304
22211
|
if (!exists) {
|
|
@@ -21345,7 +22252,7 @@ async function checkPluginManifests(root, config) {
|
|
|
21345
22252
|
}
|
|
21346
22253
|
}
|
|
21347
22254
|
if (distributionTypes.has("codex-plugin")) {
|
|
21348
|
-
const manifestPath =
|
|
22255
|
+
const manifestPath = resolve12(unitRoot, ".codex-plugin", "plugin.json");
|
|
21349
22256
|
const displayPath = unitFile(unit, ".codex-plugin/plugin.json");
|
|
21350
22257
|
const exists = await fileExists(manifestPath);
|
|
21351
22258
|
if (!exists) {
|
|
@@ -21392,7 +22299,7 @@ async function checkPluginManifests(root, config) {
|
|
|
21392
22299
|
}
|
|
21393
22300
|
}
|
|
21394
22301
|
if (distributionTypes.has("kimi-plugin")) {
|
|
21395
|
-
const manifestPath =
|
|
22302
|
+
const manifestPath = resolve12(unitRoot, ".kimi-plugin", "plugin.json");
|
|
21396
22303
|
const displayPath = unitFile(unit, ".kimi-plugin/plugin.json");
|
|
21397
22304
|
const exists = await fileExists(manifestPath);
|
|
21398
22305
|
if (!exists) {
|
|
@@ -21439,7 +22346,7 @@ async function checkPluginManifests(root, config) {
|
|
|
21439
22346
|
}
|
|
21440
22347
|
}
|
|
21441
22348
|
if (distributionTypes.has("codebuddy-plugin")) {
|
|
21442
|
-
const manifestPath =
|
|
22349
|
+
const manifestPath = resolve12(unitRoot, ".codebuddy-plugin", "plugin.json");
|
|
21443
22350
|
const displayPath = unitFile(unit, ".codebuddy-plugin/plugin.json");
|
|
21444
22351
|
const exists = await fileExists(manifestPath);
|
|
21445
22352
|
if (!exists) {
|
|
@@ -21505,8 +22412,8 @@ async function checkPackageMetadata(root, config) {
|
|
|
21505
22412
|
for (const unit of units) {
|
|
21506
22413
|
const hasNpmDist = (unit.distributions ?? []).some((d) => d.type === "npm");
|
|
21507
22414
|
if (!hasNpmDist) continue;
|
|
21508
|
-
const unitRoot =
|
|
21509
|
-
const pkgPath =
|
|
22415
|
+
const unitRoot = resolve12(root, unit.source);
|
|
22416
|
+
const pkgPath = resolve12(unitRoot, "package.json");
|
|
21510
22417
|
const exists = await fileExists(pkgPath);
|
|
21511
22418
|
if (!exists) {
|
|
21512
22419
|
gaps.push(
|
|
@@ -21558,7 +22465,7 @@ async function checkRemotePrerequisites(root, config, offline) {
|
|
|
21558
22465
|
const gaps = [];
|
|
21559
22466
|
const units = config.releaseUnits ?? [];
|
|
21560
22467
|
try {
|
|
21561
|
-
const { stdout } = await
|
|
22468
|
+
const { stdout } = await execFile4("git", ["remote", "get-url", "origin"], {
|
|
21562
22469
|
cwd: root,
|
|
21563
22470
|
shell: false,
|
|
21564
22471
|
encoding: "utf8",
|
|
@@ -21589,7 +22496,7 @@ async function checkRemotePrerequisites(root, config, offline) {
|
|
|
21589
22496
|
for (const unit of units) {
|
|
21590
22497
|
const npmDist = (unit.distributions ?? []).find((d) => d.type === "npm");
|
|
21591
22498
|
if (!npmDist?.package) continue;
|
|
21592
|
-
const pkgPath =
|
|
22499
|
+
const pkgPath = resolve12(root, unit.source, "package.json");
|
|
21593
22500
|
let version;
|
|
21594
22501
|
try {
|
|
21595
22502
|
const content = await readFile9(pkgPath, "utf8");
|
|
@@ -21600,7 +22507,7 @@ async function checkRemotePrerequisites(root, config, offline) {
|
|
|
21600
22507
|
}
|
|
21601
22508
|
if (!version) continue;
|
|
21602
22509
|
try {
|
|
21603
|
-
await
|
|
22510
|
+
await execFile4(
|
|
21604
22511
|
"npm",
|
|
21605
22512
|
["view", `${npmDist.package}@${version}`, "version"],
|
|
21606
22513
|
{ cwd: root, shell: false, encoding: "utf8", timeout: 15e3 }
|
|
@@ -21623,7 +22530,7 @@ async function checkReadmeStructure(root, config) {
|
|
|
21623
22530
|
const gaps = [];
|
|
21624
22531
|
const units = config.releaseUnits ?? [];
|
|
21625
22532
|
for (const unit of units) {
|
|
21626
|
-
const readmePath =
|
|
22533
|
+
const readmePath = resolve12(root, unit.source, "README.md");
|
|
21627
22534
|
const displayPath = unitFile(unit, "README.md");
|
|
21628
22535
|
const exists = await fileExists(readmePath);
|
|
21629
22536
|
if (!exists) continue;
|
|
@@ -21800,15 +22707,15 @@ async function assessProject(options) {
|
|
|
21800
22707
|
}
|
|
21801
22708
|
async function writeReport(outputPath, report) {
|
|
21802
22709
|
const dir = dirname5(outputPath);
|
|
21803
|
-
await
|
|
22710
|
+
await mkdir4(dir, { recursive: true });
|
|
21804
22711
|
await writeFile4(outputPath, JSON.stringify(report, null, 2), "utf8");
|
|
21805
22712
|
}
|
|
21806
|
-
var
|
|
22713
|
+
var execFile4, Severity, GapScope, GapCategory;
|
|
21807
22714
|
var init_assess = __esm({
|
|
21808
22715
|
async "src/commands/assess.mjs"() {
|
|
21809
22716
|
await init_config();
|
|
21810
22717
|
init_errors();
|
|
21811
|
-
|
|
22718
|
+
execFile4 = promisify4(execFileCb4);
|
|
21812
22719
|
Severity = Object.freeze({
|
|
21813
22720
|
ERROR: "error",
|
|
21814
22721
|
WARNING: "warning"
|
|
@@ -21844,10 +22751,10 @@ var init_assess = __esm({
|
|
|
21844
22751
|
});
|
|
21845
22752
|
|
|
21846
22753
|
// src/core/hooks.mjs
|
|
21847
|
-
import { execFile as
|
|
21848
|
-
import { promisify as
|
|
21849
|
-
import { resolve as
|
|
21850
|
-
import { realpath as
|
|
22754
|
+
import { execFile as execFileCb5 } from "node:child_process";
|
|
22755
|
+
import { promisify as promisify5 } from "node:util";
|
|
22756
|
+
import { resolve as resolve13, relative as relative10, isAbsolute as isAbsolute9 } from "node:path";
|
|
22757
|
+
import { realpath as realpath6 } from "node:fs/promises";
|
|
21851
22758
|
function validateHook(hook) {
|
|
21852
22759
|
if (!hook || typeof hook !== "object" || Array.isArray(hook)) {
|
|
21853
22760
|
throw new ReleaseError("INVALID_HOOK", "hook must be a non-null object");
|
|
@@ -21858,858 +22765,286 @@ function validateHook(hook) {
|
|
|
21858
22765
|
}
|
|
21859
22766
|
for (const c of command2) {
|
|
21860
22767
|
if (typeof c !== "string") {
|
|
21861
|
-
throw new ReleaseError("INVALID_HOOK", "every element of hook.command must be a string");
|
|
21862
|
-
}
|
|
21863
|
-
}
|
|
21864
|
-
if (cwd !== void 0 && typeof cwd !== "string") {
|
|
21865
|
-
throw new ReleaseError("INVALID_HOOK", "hook.cwd must be a string when provided");
|
|
21866
|
-
}
|
|
21867
|
-
if (timeoutMs !== void 0) {
|
|
21868
|
-
if (typeof timeoutMs !== "number" || !Number.isInteger(timeoutMs) || timeoutMs < 1) {
|
|
21869
|
-
throw new ReleaseError("INVALID_HOOK", "hook.timeoutMs must be a positive integer");
|
|
21870
|
-
}
|
|
21871
|
-
}
|
|
21872
|
-
if (envAllowlist !== void 0) {
|
|
21873
|
-
if (!Array.isArray(envAllowlist)) {
|
|
21874
|
-
throw new ReleaseError("INVALID_HOOK", "hook.envAllowlist must be an array");
|
|
21875
|
-
}
|
|
21876
|
-
for (const key of envAllowlist) {
|
|
21877
|
-
if (typeof key !== "string" || !ENV_KEY_PATTERN.test(key)) {
|
|
21878
|
-
throw new ReleaseError(
|
|
21879
|
-
"INVALID_HOOK",
|
|
21880
|
-
`hook.envAllowlist key "${key}" must match /^[A-Z_][A-Z0-9_]*$/`
|
|
21881
|
-
);
|
|
21882
|
-
}
|
|
21883
|
-
}
|
|
21884
|
-
}
|
|
21885
|
-
if (cacheable !== void 0 && typeof cacheable !== "boolean") {
|
|
21886
|
-
throw new ReleaseError("INVALID_HOOK", "hook.cacheable must be a boolean when provided");
|
|
21887
|
-
}
|
|
21888
|
-
if (cacheInputs !== void 0) {
|
|
21889
|
-
if (!Array.isArray(cacheInputs) || cacheInputs.length === 0) {
|
|
21890
|
-
throw new ReleaseError(
|
|
21891
|
-
"INVALID_HOOK",
|
|
21892
|
-
"hook.cacheInputs must be a non-empty array of glob strings when provided"
|
|
21893
|
-
);
|
|
21894
|
-
}
|
|
21895
|
-
for (const glob of cacheInputs) {
|
|
21896
|
-
if (typeof glob !== "string" || glob.length === 0) {
|
|
21897
|
-
throw new ReleaseError(
|
|
21898
|
-
"INVALID_HOOK",
|
|
21899
|
-
"every element of hook.cacheInputs must be a non-empty string"
|
|
21900
|
-
);
|
|
21901
|
-
}
|
|
21902
|
-
}
|
|
21903
|
-
}
|
|
21904
|
-
if (cacheable === true && (!Array.isArray(cacheInputs) || cacheInputs.length === 0)) {
|
|
21905
|
-
throw new ReleaseError(
|
|
21906
|
-
"INVALID_HOOK",
|
|
21907
|
-
"hook.cacheable=true requires a non-empty hook.cacheInputs"
|
|
21908
|
-
);
|
|
21909
|
-
}
|
|
21910
|
-
}
|
|
21911
|
-
function buildFilteredEnv(envAllowlist, contextEnv) {
|
|
21912
|
-
const env = {};
|
|
21913
|
-
for (const key of PLATFORM_REQUIRED_VARS) {
|
|
21914
|
-
const val = process.env[key];
|
|
21915
|
-
if (val !== void 0) {
|
|
21916
|
-
env[key] = val;
|
|
21917
|
-
}
|
|
21918
|
-
}
|
|
21919
|
-
if (contextEnv && envAllowlist) {
|
|
21920
|
-
for (const key of envAllowlist) {
|
|
21921
|
-
if (key in contextEnv) {
|
|
21922
|
-
env[key] = contextEnv[key];
|
|
21923
|
-
}
|
|
21924
|
-
}
|
|
21925
|
-
}
|
|
21926
|
-
return env;
|
|
21927
|
-
}
|
|
21928
|
-
async function runHook(hook, context) {
|
|
21929
|
-
validateHook(hook);
|
|
21930
|
-
if (!context || typeof context !== "object" || Array.isArray(context)) {
|
|
21931
|
-
throw new ReleaseError("INVALID_HOOK", "context must be a non-null object");
|
|
21932
|
-
}
|
|
21933
|
-
if (typeof context.root !== "string" || !isAbsolute8(context.root)) {
|
|
21934
|
-
throw new ReleaseError("INVALID_HOOK", "context.root must be an absolute path");
|
|
21935
|
-
}
|
|
21936
|
-
const { command: command2, cwd, timeoutMs, envAllowlist = [] } = hook;
|
|
21937
|
-
const rootReal = await realpath5(context.root);
|
|
21938
|
-
const resolvedCwd = cwd ? await realpath5(resolve12(rootReal, cwd)) : rootReal;
|
|
21939
|
-
const rel = relative9(rootReal, resolvedCwd);
|
|
21940
|
-
if (rel.startsWith("..") || rel === "..") {
|
|
21941
|
-
throw new ReleaseError(
|
|
21942
|
-
"INVALID_HOOK",
|
|
21943
|
-
`hook.cwd "${cwd}" resolves outside project root`
|
|
21944
|
-
);
|
|
21945
|
-
}
|
|
21946
|
-
const env = buildFilteredEnv(envAllowlist, context.env);
|
|
21947
|
-
const executable = command2[0];
|
|
21948
|
-
const args2 = command2.slice(1);
|
|
21949
|
-
let timeoutController;
|
|
21950
|
-
let timeoutHandle;
|
|
21951
|
-
if (timeoutMs && timeoutMs > 0) {
|
|
21952
|
-
timeoutController = new AbortController();
|
|
21953
|
-
timeoutHandle = setTimeout(() => timeoutController.abort(), timeoutMs);
|
|
21954
|
-
}
|
|
21955
|
-
try {
|
|
21956
|
-
const { stdout, stderr } = await execFileAsync(executable, args2, {
|
|
21957
|
-
cwd: resolvedCwd,
|
|
21958
|
-
env,
|
|
21959
|
-
shell: false,
|
|
21960
|
-
maxBuffer: 10 * 1024 * 1024,
|
|
21961
|
-
// 10 MiB
|
|
21962
|
-
signal: timeoutController?.signal
|
|
21963
|
-
});
|
|
21964
|
-
return { exitCode: 0, stdout: stdout ?? "", stderr: stderr ?? "" };
|
|
21965
|
-
} catch (err) {
|
|
21966
|
-
if (err.name === "AbortError" || err.killed && err.signal === "SIGTERM") {
|
|
21967
|
-
throw new ReleaseError(
|
|
21968
|
-
"HOOK_TIMEOUT",
|
|
21969
|
-
`hook timed out after ${timeoutMs}ms: ${executable} ${args2.join(" ")}`,
|
|
21970
|
-
{ command: command2, timeoutMs }
|
|
21971
|
-
);
|
|
21972
|
-
}
|
|
21973
|
-
if ("stdout" in err) {
|
|
21974
|
-
return {
|
|
21975
|
-
exitCode: typeof err.code === "number" ? err.code : 1,
|
|
21976
|
-
stdout: err.stdout ?? "",
|
|
21977
|
-
stderr: err.stderr ?? ""
|
|
21978
|
-
};
|
|
21979
|
-
}
|
|
21980
|
-
throw err;
|
|
21981
|
-
} finally {
|
|
21982
|
-
if (timeoutHandle) {
|
|
21983
|
-
clearTimeout(timeoutHandle);
|
|
21984
|
-
}
|
|
21985
|
-
}
|
|
21986
|
-
}
|
|
21987
|
-
var execFileAsync, PLATFORM_REQUIRED_VARS, ENV_KEY_PATTERN;
|
|
21988
|
-
var init_hooks = __esm({
|
|
21989
|
-
"src/core/hooks.mjs"() {
|
|
21990
|
-
init_errors();
|
|
21991
|
-
execFileAsync = promisify4(execFileCb4);
|
|
21992
|
-
PLATFORM_REQUIRED_VARS = (() => {
|
|
21993
|
-
const posix3 = ["PATH", "HOME", "USER", "LANG", "LC_ALL", "LC_CTYPE", "TERM"];
|
|
21994
|
-
const win322 = ["PATH", "HOME", "USER", "SystemRoot", "SYSTEMROOT", "COMSPEC", "PATHEXT", "TEMP", "TMP"];
|
|
21995
|
-
return new Set(process.platform === "win32" ? win322 : posix3);
|
|
21996
|
-
})();
|
|
21997
|
-
ENV_KEY_PATTERN = /^[A-Z_][A-Z0-9_]*$/;
|
|
21998
|
-
__name(validateHook, "validateHook");
|
|
21999
|
-
__name(buildFilteredEnv, "buildFilteredEnv");
|
|
22000
|
-
__name(runHook, "runHook");
|
|
22001
|
-
}
|
|
22002
|
-
});
|
|
22003
|
-
|
|
22004
|
-
// src/core/hook-cache.mjs
|
|
22005
|
-
import { readdir as readdir3, readFile as readFile10, mkdir as mkdir4, writeFile as writeFile5 } from "node:fs/promises";
|
|
22006
|
-
import { join as join8 } from "node:path";
|
|
22007
|
-
function globToRegExp(glob) {
|
|
22008
|
-
let source = "";
|
|
22009
|
-
let i = 0;
|
|
22010
|
-
while (i < glob.length) {
|
|
22011
|
-
const c = glob[i];
|
|
22012
|
-
if (c === "*") {
|
|
22013
|
-
if (glob[i + 1] === "*") {
|
|
22014
|
-
source += ".*";
|
|
22015
|
-
i += 2;
|
|
22016
|
-
} else {
|
|
22017
|
-
source += "[^/]*";
|
|
22018
|
-
i += 1;
|
|
22019
|
-
}
|
|
22020
|
-
} else if (c === "?") {
|
|
22021
|
-
source += "[^/]";
|
|
22022
|
-
i += 1;
|
|
22023
|
-
} else {
|
|
22024
|
-
source += c.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
22025
|
-
i += 1;
|
|
22026
|
-
}
|
|
22027
|
-
}
|
|
22028
|
-
return new RegExp(`^${source}$`);
|
|
22029
|
-
}
|
|
22030
|
-
async function listInputFiles(root) {
|
|
22031
|
-
const out = [];
|
|
22032
|
-
async function walk(dirAbs, dirRel) {
|
|
22033
|
-
let entries;
|
|
22034
|
-
try {
|
|
22035
|
-
entries = await readdir3(dirAbs, { withFileTypes: true });
|
|
22036
|
-
} catch {
|
|
22037
|
-
return;
|
|
22038
|
-
}
|
|
22039
|
-
for (const entry of entries) {
|
|
22040
|
-
if (entry.isDirectory()) {
|
|
22041
|
-
if (SKIPPED_DIRS.has(entry.name)) continue;
|
|
22042
|
-
const rel = dirRel ? `${dirRel}/${entry.name}` : entry.name;
|
|
22043
|
-
await walk(join8(dirAbs, entry.name), rel);
|
|
22044
|
-
} else if (entry.isFile()) {
|
|
22045
|
-
out.push(dirRel ? `${dirRel}/${entry.name}` : entry.name);
|
|
22046
|
-
}
|
|
22047
|
-
}
|
|
22048
|
-
}
|
|
22049
|
-
__name(walk, "walk");
|
|
22050
|
-
await walk(root, "");
|
|
22051
|
-
out.sort();
|
|
22052
|
-
return out;
|
|
22053
|
-
}
|
|
22054
|
-
async function computeHookCacheKey(hook, root) {
|
|
22055
|
-
const globs = Array.isArray(hook.cacheInputs) ? hook.cacheInputs : [];
|
|
22056
|
-
const matchers = globs.map((glob) => ({ glob, re: globToRegExp(glob) }));
|
|
22057
|
-
const allFiles = await listInputFiles(root);
|
|
22058
|
-
const matched = [];
|
|
22059
|
-
const hitPerGlob = matchers.map(() => false);
|
|
22060
|
-
for (const relPath of allFiles) {
|
|
22061
|
-
let hit = false;
|
|
22062
|
-
for (let i = 0; i < matchers.length; i += 1) {
|
|
22063
|
-
if (matchers[i].re.test(relPath)) {
|
|
22064
|
-
hitPerGlob[i] = true;
|
|
22065
|
-
hit = true;
|
|
22066
|
-
}
|
|
22067
|
-
}
|
|
22068
|
-
if (hit) matched.push(relPath);
|
|
22069
|
-
}
|
|
22070
|
-
for (let i = 0; i < matchers.length; i += 1) {
|
|
22071
|
-
if (!hitPerGlob[i]) {
|
|
22072
|
-
throw new ReleaseError(
|
|
22073
|
-
GATE_FAILED,
|
|
22074
|
-
`hook cacheInputs glob "${matchers[i].glob}" matched no files; refusing to cache`,
|
|
22075
|
-
{ glob: matchers[i].glob }
|
|
22076
|
-
);
|
|
22077
|
-
}
|
|
22078
|
-
}
|
|
22079
|
-
matched.sort();
|
|
22080
|
-
const fileEntries = [];
|
|
22081
|
-
for (const relPath of matched) {
|
|
22082
|
-
const content = await readFile10(join8(root, relPath));
|
|
22083
|
-
fileEntries.push({ path: relPath, sha256: sha256Hex(content) });
|
|
22084
|
-
}
|
|
22085
|
-
const cacheKey = sha256Hex(canonicalJson(hook) + canonicalJson(fileEntries));
|
|
22086
|
-
return { cacheKey, matchedFiles: matched };
|
|
22087
|
-
}
|
|
22088
|
-
function hookCacheDir(root, hookName) {
|
|
22089
|
-
return join8(root, ...CACHE_BASE, hookName);
|
|
22090
|
-
}
|
|
22091
|
-
function hookCachePath(root, hookName, cacheKey) {
|
|
22092
|
-
return join8(hookCacheDir(root, hookName), `${cacheKey}.json`);
|
|
22093
|
-
}
|
|
22094
|
-
async function readHookCache(root, hookName, cacheKey) {
|
|
22095
|
-
try {
|
|
22096
|
-
const raw = await readFile10(hookCachePath(root, hookName, cacheKey), "utf8");
|
|
22097
|
-
const record = JSON.parse(raw);
|
|
22098
|
-
if (record && record.cacheKey === cacheKey && record.exitCode === 0) {
|
|
22099
|
-
return record;
|
|
22100
|
-
}
|
|
22101
|
-
return null;
|
|
22102
|
-
} catch {
|
|
22103
|
-
return null;
|
|
22104
|
-
}
|
|
22105
|
-
}
|
|
22106
|
-
async function writeHookCache(root, hookName, cacheKey, result) {
|
|
22107
|
-
if (!result || result.exitCode !== 0) {
|
|
22108
|
-
return { ok: false, error: "refusing to cache a non-zero exit result" };
|
|
22109
|
-
}
|
|
22110
|
-
try {
|
|
22111
|
-
await mkdir4(hookCacheDir(root, hookName), { recursive: true });
|
|
22112
|
-
const record = {
|
|
22113
|
-
cacheKey,
|
|
22114
|
-
exitCode: 0,
|
|
22115
|
-
stdoutTail: String(result.stdoutTail ?? "").slice(-TAIL_LENGTH),
|
|
22116
|
-
stderrTail: String(result.stderrTail ?? "").slice(-TAIL_LENGTH),
|
|
22117
|
-
createdAt: result.createdAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
22118
|
-
};
|
|
22119
|
-
await writeFile5(hookCachePath(root, hookName, cacheKey), `${JSON.stringify(record, null, 2)}
|
|
22120
|
-
`, "utf8");
|
|
22121
|
-
return { ok: true };
|
|
22122
|
-
} catch (err) {
|
|
22123
|
-
return { ok: false, error: err.message };
|
|
22124
|
-
}
|
|
22125
|
-
}
|
|
22126
|
-
var CACHE_BASE, TAIL_LENGTH, SKIPPED_DIRS;
|
|
22127
|
-
var init_hook_cache = __esm({
|
|
22128
|
-
"src/core/hook-cache.mjs"() {
|
|
22129
|
-
init_digest();
|
|
22130
|
-
init_errors();
|
|
22131
|
-
CACHE_BASE = [".release-skill", "cache", "hooks"];
|
|
22132
|
-
TAIL_LENGTH = 4e3;
|
|
22133
|
-
SKIPPED_DIRS = /* @__PURE__ */ new Set([".git", "node_modules", ".release-skill"]);
|
|
22134
|
-
__name(globToRegExp, "globToRegExp");
|
|
22135
|
-
__name(listInputFiles, "listInputFiles");
|
|
22136
|
-
__name(computeHookCacheKey, "computeHookCacheKey");
|
|
22137
|
-
__name(hookCacheDir, "hookCacheDir");
|
|
22138
|
-
__name(hookCachePath, "hookCachePath");
|
|
22139
|
-
__name(readHookCache, "readHookCache");
|
|
22140
|
-
__name(writeHookCache, "writeHookCache");
|
|
22141
|
-
}
|
|
22142
|
-
});
|
|
22143
|
-
|
|
22144
|
-
// src/snapshot/frozen.mjs
|
|
22145
|
-
import { createHash as createHash5 } from "node:crypto";
|
|
22146
|
-
import { execFile as execFileCb5, spawn } from "node:child_process";
|
|
22147
|
-
import { promisify as promisify5 } from "node:util";
|
|
22148
|
-
import {
|
|
22149
|
-
chmod,
|
|
22150
|
-
lstat as lstat6,
|
|
22151
|
-
mkdir as mkdir5,
|
|
22152
|
-
mkdtemp,
|
|
22153
|
-
open as open4,
|
|
22154
|
-
readdir as readdir4,
|
|
22155
|
-
realpath as realpath6,
|
|
22156
|
-
rm as rm3,
|
|
22157
|
-
unlink as unlink2
|
|
22158
|
-
} from "node:fs/promises";
|
|
22159
|
-
import { constants as fsConstants2 } from "node:fs";
|
|
22160
|
-
import { isAbsolute as isAbsolute9, join as join9, relative as relative10, resolve as resolve13 } from "node:path";
|
|
22161
|
-
function frozenError(message, details = {}) {
|
|
22162
|
-
return new ReleaseError(GATE_FAILED, message, details);
|
|
22163
|
-
}
|
|
22164
|
-
function isInside(parent, candidate) {
|
|
22165
|
-
const rel = relative10(parent, candidate);
|
|
22166
|
-
return rel === "" || !isAbsolute9(rel) && rel !== ".." && !rel.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`);
|
|
22167
|
-
}
|
|
22168
|
-
async function resolveFrozenPath(root, relativePath, label = "frozen path") {
|
|
22169
|
-
if (!relativePath || typeof relativePath !== "string" || isAbsolute9(relativePath)) {
|
|
22170
|
-
throw frozenError(`${label} must be a non-empty project-relative path`);
|
|
22171
|
-
}
|
|
22172
|
-
const rootReal = await realpath6(root);
|
|
22173
|
-
const lexical = resolve13(rootReal, relativePath);
|
|
22174
|
-
if (!isInside(rootReal, lexical)) {
|
|
22175
|
-
throw frozenError(`${label} escapes project root`, { relativePath });
|
|
22176
|
-
}
|
|
22177
|
-
const lexicalStat = await lstat6(lexical).catch((err) => {
|
|
22178
|
-
throw frozenError(`${label} is missing`, { relativePath, cause: err.code });
|
|
22179
|
-
});
|
|
22180
|
-
if (lexicalStat.isSymbolicLink()) {
|
|
22181
|
-
throw frozenError(`${label} must not be a symlink`, { relativePath });
|
|
22182
|
-
}
|
|
22183
|
-
const physical = await realpath6(lexical).catch((err) => {
|
|
22184
|
-
throw frozenError(`${label} is missing`, { relativePath, cause: err.code });
|
|
22185
|
-
});
|
|
22186
|
-
if (!isInside(rootReal, physical)) {
|
|
22187
|
-
throw frozenError(`${label} resolves outside project root`, { relativePath });
|
|
22188
|
-
}
|
|
22189
|
-
return physical;
|
|
22190
|
-
}
|
|
22191
|
-
async function readStableRegularFile(filePath, displayPath) {
|
|
22192
|
-
const before = await lstat6(filePath);
|
|
22193
|
-
if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1) {
|
|
22194
|
-
throw frozenError(`frozen snapshot entry is not a single-link regular file: ${displayPath}`);
|
|
22195
|
-
}
|
|
22196
|
-
const handle = await open4(filePath, fsConstants2.O_RDONLY | fsConstants2.O_NOFOLLOW);
|
|
22197
|
-
try {
|
|
22198
|
-
const opened = await handle.stat();
|
|
22199
|
-
if (!opened.isFile() || opened.nlink !== 1 || opened.dev !== before.dev || opened.ino !== before.ino) {
|
|
22200
|
-
throw frozenError(`frozen snapshot entry changed before read: ${displayPath}`);
|
|
22201
|
-
}
|
|
22202
|
-
const bytes = await handle.readFile();
|
|
22203
|
-
const after = await handle.stat();
|
|
22204
|
-
if (after.dev !== opened.dev || after.ino !== opened.ino || after.nlink !== 1 || after.size !== opened.size || after.mtimeMs !== opened.mtimeMs || after.ctimeMs !== opened.ctimeMs) {
|
|
22205
|
-
throw frozenError(`frozen snapshot entry changed during read: ${displayPath}`);
|
|
22206
|
-
}
|
|
22207
|
-
return { bytes, mode: opened.mode };
|
|
22208
|
-
} finally {
|
|
22209
|
-
await handle.close();
|
|
22210
|
-
}
|
|
22211
|
-
}
|
|
22212
|
-
async function computeFrozenSnapshot(snapshotDir, { excludeRootEntries = [] } = {}) {
|
|
22213
|
-
const root = await realpath6(snapshotDir);
|
|
22214
|
-
const rootStat = await lstat6(root);
|
|
22215
|
-
if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) {
|
|
22216
|
-
throw frozenError("frozen snapshot root must be a real directory");
|
|
22217
|
-
}
|
|
22218
|
-
const entries = [];
|
|
22219
|
-
const excludedRootNames = /* @__PURE__ */ new Set();
|
|
22220
|
-
const excludedPathPrefixes = [];
|
|
22221
|
-
for (const entry of excludeRootEntries) {
|
|
22222
|
-
if (typeof entry !== "string") {
|
|
22223
|
-
throw frozenError("frozen snapshot exclusion entries must be strings");
|
|
22224
|
-
}
|
|
22225
|
-
const segments = entry.split("/").filter((segment) => segment !== "" && segment !== ".");
|
|
22226
|
-
if (segments.length === 0 || entry.startsWith("/") || entry.includes("\\") || segments.some((segment) => segment === "..")) {
|
|
22227
|
-
throw frozenError(`frozen snapshot exclusion entry is not a safe relative path: ${JSON.stringify(entry)}`);
|
|
22228
|
-
}
|
|
22229
|
-
if (segments.length === 1) {
|
|
22230
|
-
excludedRootNames.add(segments[0]);
|
|
22231
|
-
} else {
|
|
22232
|
-
excludedPathPrefixes.push(segments.join("/"));
|
|
22233
|
-
}
|
|
22234
|
-
}
|
|
22235
|
-
async function walk(dir) {
|
|
22236
|
-
const children = await readdir4(dir, { withFileTypes: true });
|
|
22237
|
-
children.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
22238
|
-
for (const child of children) {
|
|
22239
|
-
if (dir === root && excludedRootNames.has(child.name)) continue;
|
|
22240
|
-
const absolute = join9(dir, child.name);
|
|
22241
|
-
const rel = relative10(root, absolute).split("\\").join("/");
|
|
22242
|
-
if (excludedPathPrefixes.some((prefix) => rel === prefix || rel.startsWith(`${prefix}/`))) continue;
|
|
22243
|
-
const st = await lstat6(absolute);
|
|
22244
|
-
if (st.isSymbolicLink()) {
|
|
22245
|
-
throw frozenError(`frozen snapshot contains symlink: ${rel}`);
|
|
22246
|
-
}
|
|
22247
|
-
if (st.isDirectory()) {
|
|
22248
|
-
await walk(absolute);
|
|
22249
|
-
continue;
|
|
22250
|
-
}
|
|
22251
|
-
const { bytes, mode } = await readStableRegularFile(absolute, rel);
|
|
22252
|
-
entries.push({
|
|
22253
|
-
path: rel,
|
|
22254
|
-
type: "file",
|
|
22255
|
-
mode,
|
|
22256
|
-
size: bytes.length,
|
|
22257
|
-
contentDigest: createHash5("sha256").update(bytes).digest("hex")
|
|
22258
|
-
});
|
|
22259
|
-
}
|
|
22260
|
-
}
|
|
22261
|
-
__name(walk, "walk");
|
|
22262
|
-
await walk(root);
|
|
22263
|
-
entries.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
22264
|
-
const digest = createHash5("sha256").update(JSON.stringify(entries)).digest("hex");
|
|
22265
|
-
return { digest, entries };
|
|
22266
|
-
}
|
|
22267
|
-
async function verifyFrozenSnapshot({ root, snapshotPath, expectedDigest }) {
|
|
22268
|
-
const snapshotDir = await resolveFrozenPath(root, snapshotPath, "frozen snapshot path");
|
|
22269
|
-
const observed = await computeFrozenSnapshot(snapshotDir);
|
|
22270
|
-
if (!expectedDigest || observed.digest !== expectedDigest) {
|
|
22271
|
-
throw frozenError("frozen snapshot digest mismatch", {
|
|
22272
|
-
expectedDigest,
|
|
22273
|
-
observedDigest: observed.digest
|
|
22274
|
-
});
|
|
22275
|
-
}
|
|
22276
|
-
return { snapshotDir, ...observed };
|
|
22277
|
-
}
|
|
22278
|
-
async function verifyFrozenFile({ root, filePath, expectedSha256, label = "frozen file" }) {
|
|
22279
|
-
const physical = await resolveFrozenPath(root, filePath, label);
|
|
22280
|
-
const { bytes } = await readStableRegularFile(physical, filePath);
|
|
22281
|
-
const observedSha256 = createHash5("sha256").update(bytes).digest("hex");
|
|
22282
|
-
if (!expectedSha256 || observedSha256 !== expectedSha256) {
|
|
22283
|
-
throw frozenError(`${label} SHA-256 mismatch`, { expectedSha256, observedSha256 });
|
|
22284
|
-
}
|
|
22285
|
-
return { physical, observedSha256, size: bytes.length };
|
|
22286
|
-
}
|
|
22287
|
-
async function verifyFrozenDirectoryStructure(directory, label = "frozen directory") {
|
|
22288
|
-
async function walk(current) {
|
|
22289
|
-
const children = await readdir4(current, { withFileTypes: true });
|
|
22290
|
-
for (const child of children) {
|
|
22291
|
-
const absolute = join9(current, child.name);
|
|
22292
|
-
const st = await lstat6(absolute);
|
|
22293
|
-
if (st.isSymbolicLink()) {
|
|
22294
|
-
throw frozenError(`${label} contains a symlink`);
|
|
22295
|
-
}
|
|
22296
|
-
if (st.isDirectory()) {
|
|
22297
|
-
await walk(absolute);
|
|
22298
|
-
} else if (!st.isFile() || st.nlink !== 1) {
|
|
22299
|
-
throw frozenError(`${label} contains an unsafe non-regular or hardlinked entry`);
|
|
22300
|
-
}
|
|
22301
|
-
}
|
|
22302
|
-
}
|
|
22303
|
-
__name(walk, "walk");
|
|
22304
|
-
await walk(directory);
|
|
22305
|
-
}
|
|
22306
|
-
async function verifyFrozenGitRepository({
|
|
22307
|
-
root,
|
|
22308
|
-
gitObjectDir,
|
|
22309
|
-
commit,
|
|
22310
|
-
tree,
|
|
22311
|
-
parentCommit,
|
|
22312
|
-
commitTimestamp,
|
|
22313
|
-
exec = execFile4
|
|
22314
|
-
}) {
|
|
22315
|
-
const gitDir = await resolveFrozenPath(root, gitObjectDir, "frozen git object directory");
|
|
22316
|
-
await verifyFrozenDirectoryStructure(gitDir, "frozen git object directory");
|
|
22317
|
-
const { stdout } = await exec("git", ["--git-dir", gitDir, "rev-parse", `${commit}^{tree}`], { shell: false });
|
|
22318
|
-
if (stdout.trim() !== tree) {
|
|
22319
|
-
throw frozenError("frozen git object tree mismatch", { commit, expectedTree: tree, observedTree: stdout.trim() });
|
|
22320
|
-
}
|
|
22321
|
-
if (commitTimestamp !== void 0) {
|
|
22322
|
-
const expectedTimestamp = normalizeGitTimestamp(commitTimestamp, "frozen commit timestamp");
|
|
22323
|
-
const { stdout: datesOut } = await exec(
|
|
22324
|
-
"git",
|
|
22325
|
-
["--git-dir", gitDir, "show", "-s", "--format=%aI%n%cI", commit],
|
|
22326
|
-
{ shell: false }
|
|
22327
|
-
);
|
|
22328
|
-
const [authorDate, committerDate] = datesOut.trim().split("\n");
|
|
22329
|
-
if (authorDate !== expectedTimestamp || committerDate !== expectedTimestamp) {
|
|
22330
|
-
throw frozenError("frozen git commit dates do not match the plan freeze timestamp", {
|
|
22331
|
-
commit,
|
|
22332
|
-
expectedTimestamp,
|
|
22333
|
-
observedAuthorDate: authorDate,
|
|
22334
|
-
observedCommitterDate: committerDate
|
|
22335
|
-
});
|
|
22336
|
-
}
|
|
22337
|
-
}
|
|
22338
|
-
if (parentCommit) {
|
|
22339
|
-
if (!/^[a-f0-9]{40,64}$/.test(parentCommit)) {
|
|
22340
|
-
throw frozenError("frozen Git parent must be a full hexadecimal object id");
|
|
22341
|
-
}
|
|
22342
|
-
const { stdout: parentsOut } = await exec(
|
|
22343
|
-
"git",
|
|
22344
|
-
["--git-dir", gitDir, "rev-list", "--parents", "-n", "1", commit],
|
|
22345
|
-
{ shell: false }
|
|
22346
|
-
);
|
|
22347
|
-
const [observedCommit, ...parents] = parentsOut.trim().split(/\s+/);
|
|
22348
|
-
if (observedCommit !== commit || parents.length !== 1 || parents[0] !== parentCommit) {
|
|
22349
|
-
throw frozenError("frozen Git commit parent mismatch", {
|
|
22350
|
-
commit,
|
|
22351
|
-
expectedParent: parentCommit,
|
|
22352
|
-
observedParents: parents
|
|
22353
|
-
});
|
|
22354
|
-
}
|
|
22355
|
-
}
|
|
22356
|
-
return { gitDir, commit, tree, ...parentCommit ? { parentCommit } : {} };
|
|
22357
|
-
}
|
|
22358
|
-
async function verifyGitTreeContent({ snapshotDir, repositoryDir, commit, expectedSnapshotDigest, exec }) {
|
|
22359
|
-
const snapshot = await computeFrozenSnapshot(snapshotDir);
|
|
22360
|
-
if (snapshot.digest !== expectedSnapshotDigest) {
|
|
22361
|
-
throw frozenError("frozen snapshot changed while deriving Git objects", {
|
|
22362
|
-
expectedDigest: expectedSnapshotDigest,
|
|
22363
|
-
observedDigest: snapshot.digest
|
|
22364
|
-
});
|
|
22365
|
-
}
|
|
22366
|
-
const { stdout: treeOut } = await exec(
|
|
22367
|
-
"git",
|
|
22368
|
-
["--git-dir", repositoryDir, "ls-tree", "-rz", commit],
|
|
22369
|
-
{ shell: false, encoding: "buffer", maxBuffer: 64 * 1024 * 1024 }
|
|
22370
|
-
);
|
|
22371
|
-
const treeEntries = Buffer.from(treeOut).toString("utf8").split("\0").filter(Boolean).map((record) => {
|
|
22372
|
-
const separator = record.indexOf(" ");
|
|
22373
|
-
const [mode, type] = record.slice(0, separator).split(" ");
|
|
22374
|
-
return { path: record.slice(separator + 1), mode, type };
|
|
22375
|
-
}).sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
22376
|
-
const names = treeEntries.map((entry) => entry.path);
|
|
22377
|
-
const expectedNames = snapshot.entries.map((entry) => entry.path);
|
|
22378
|
-
if (JSON.stringify(names) !== JSON.stringify(expectedNames)) {
|
|
22379
|
-
throw frozenError("frozen Git tree paths do not match the sealed public snapshot");
|
|
22380
|
-
}
|
|
22381
|
-
for (const [index, entry] of snapshot.entries.entries()) {
|
|
22382
|
-
const gitEntry = treeEntries[index];
|
|
22383
|
-
const expectedMode = entry.mode & 73 ? "100755" : "100644";
|
|
22384
|
-
if (gitEntry.type !== "blob" || gitEntry.mode !== expectedMode) {
|
|
22385
|
-
throw frozenError(`frozen Git tree mode does not match the sealed public snapshot: ${entry.path}`, {
|
|
22386
|
-
expectedMode,
|
|
22387
|
-
observedMode: gitEntry.mode,
|
|
22388
|
-
observedType: gitEntry.type
|
|
22389
|
-
});
|
|
22390
|
-
}
|
|
22391
|
-
const { stdout } = await exec(
|
|
22392
|
-
"git",
|
|
22393
|
-
["--git-dir", repositoryDir, "show", `${commit}:${entry.path}`],
|
|
22394
|
-
{ shell: false, encoding: "buffer", maxBuffer: Math.max(64 * 1024 * 1024, entry.size + 1024) }
|
|
22395
|
-
);
|
|
22396
|
-
const digest = createHash5("sha256").update(Buffer.from(stdout)).digest("hex");
|
|
22397
|
-
if (digest !== entry.contentDigest) {
|
|
22398
|
-
throw frozenError(`frozen Git tree bytes do not match the sealed public snapshot: ${entry.path}`);
|
|
22399
|
-
}
|
|
22400
|
-
}
|
|
22401
|
-
}
|
|
22402
|
-
function normalizeGitTimestamp(value, label = "freeze timestamp") {
|
|
22403
|
-
if (typeof value !== "string" || value.trim() === "") {
|
|
22404
|
-
throw frozenError(`${label} must be a non-empty strict ISO 8601 timestamp string`, {
|
|
22405
|
-
received: typeof value === "string" ? value : typeof value
|
|
22406
|
-
});
|
|
22407
|
-
}
|
|
22408
|
-
const trimmed = value.trim();
|
|
22409
|
-
const match = GIT_ISO8601_STRICT_RE.exec(trimmed);
|
|
22410
|
-
if (!match) {
|
|
22411
|
-
throw frozenError(`${label} must be strict ISO 8601 with an explicit UTC offset`, {
|
|
22412
|
-
received: trimmed
|
|
22413
|
-
});
|
|
22414
|
-
}
|
|
22415
|
-
const [, yearStr, monthStr, dayStr, hourStr, minuteStr, secondStr, zone] = match;
|
|
22416
|
-
const inputYear = Number(yearStr);
|
|
22417
|
-
const inputMonth = Number(monthStr);
|
|
22418
|
-
const inputDay = Number(dayStr);
|
|
22419
|
-
const inputHour = Number(hourStr);
|
|
22420
|
-
const inputMinute = Number(minuteStr);
|
|
22421
|
-
const inputSecond = Number(secondStr);
|
|
22422
|
-
if (inputYear < 1e3 || inputYear > 9999) {
|
|
22423
|
-
throw frozenError(`${label} year must be a four-digit ISO year`, { received: trimmed });
|
|
22424
|
-
}
|
|
22425
|
-
const probeMs = Date.UTC(inputYear, inputMonth - 1, inputDay, inputHour, inputMinute, inputSecond);
|
|
22426
|
-
const probe = new Date(probeMs);
|
|
22427
|
-
if (probe.getUTCFullYear() !== inputYear || probe.getUTCMonth() !== inputMonth - 1 || probe.getUTCDate() !== inputDay || probe.getUTCHours() !== inputHour || probe.getUTCMinutes() !== inputMinute || probe.getUTCSeconds() !== inputSecond) {
|
|
22428
|
-
throw frozenError(`${label} is not a valid calendar date`, { received: trimmed });
|
|
22429
|
-
}
|
|
22430
|
-
let offsetMs = 0;
|
|
22431
|
-
if (zone !== "Z") {
|
|
22432
|
-
const sign = zone.startsWith("-") ? -1 : 1;
|
|
22433
|
-
const zoneHours = Number(zone.slice(1, 3));
|
|
22434
|
-
const zoneMinutes = Number(zone.slice(4, 6));
|
|
22435
|
-
if (zoneHours > 23 || zoneMinutes > 59) {
|
|
22436
|
-
throw frozenError(`${label} has an invalid UTC offset`, { received: trimmed });
|
|
22768
|
+
throw new ReleaseError("INVALID_HOOK", "every element of hook.command must be a string");
|
|
22437
22769
|
}
|
|
22438
|
-
offsetMs = sign * (zoneHours * 60 + zoneMinutes) * 6e4;
|
|
22439
22770
|
}
|
|
22440
|
-
|
|
22441
|
-
|
|
22442
|
-
throw frozenError(`${label} normalizes outside the four-digit ISO year range`, { received: trimmed });
|
|
22771
|
+
if (cwd !== void 0 && typeof cwd !== "string") {
|
|
22772
|
+
throw new ReleaseError("INVALID_HOOK", "hook.cwd must be a string when provided");
|
|
22443
22773
|
}
|
|
22444
|
-
|
|
22445
|
-
|
|
22446
|
-
|
|
22447
|
-
|
|
22448
|
-
if (typeof repo !== "string" || !/^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/.test(repo)) {
|
|
22449
|
-
throw frozenError("Git parent repo must use owner/name format");
|
|
22774
|
+
if (timeoutMs !== void 0) {
|
|
22775
|
+
if (typeof timeoutMs !== "number" || !Number.isInteger(timeoutMs) || timeoutMs < 1) {
|
|
22776
|
+
throw new ReleaseError("INVALID_HOOK", "hook.timeoutMs must be a positive integer");
|
|
22777
|
+
}
|
|
22450
22778
|
}
|
|
22451
|
-
if (
|
|
22452
|
-
|
|
22779
|
+
if (envAllowlist !== void 0) {
|
|
22780
|
+
if (!Array.isArray(envAllowlist)) {
|
|
22781
|
+
throw new ReleaseError("INVALID_HOOK", "hook.envAllowlist must be an array");
|
|
22782
|
+
}
|
|
22783
|
+
for (const key of envAllowlist) {
|
|
22784
|
+
if (typeof key !== "string" || !ENV_KEY_PATTERN.test(key)) {
|
|
22785
|
+
throw new ReleaseError(
|
|
22786
|
+
"INVALID_HOOK",
|
|
22787
|
+
`hook.envAllowlist key "${key}" must match /^[A-Z_][A-Z0-9_]*$/`
|
|
22788
|
+
);
|
|
22789
|
+
}
|
|
22790
|
+
}
|
|
22453
22791
|
}
|
|
22454
|
-
|
|
22455
|
-
|
|
22456
|
-
|
|
22457
|
-
|
|
22458
|
-
|
|
22459
|
-
|
|
22460
|
-
|
|
22461
|
-
|
|
22462
|
-
|
|
22463
|
-
exec = execFile4
|
|
22464
|
-
}) {
|
|
22465
|
-
const canonicalTimestamp = normalizeGitTimestamp(commitTimestamp, "frozen commit timestamp");
|
|
22466
|
-
if (!expectedSnapshotDigest) throw frozenError("Git object build requires the sealed snapshot digest");
|
|
22467
|
-
await mkdir5(repositoryDir, { recursive: true });
|
|
22468
|
-
await exec("git", ["init", "--bare", repositoryDir], { shell: false });
|
|
22469
|
-
let parentCommit = null;
|
|
22470
|
-
if (parent) {
|
|
22471
|
-
if (!parent.ref || typeof parent.ref !== "string") {
|
|
22472
|
-
throw frozenError("Git parent ref must be a non-empty string");
|
|
22792
|
+
if (cacheable !== void 0 && typeof cacheable !== "boolean") {
|
|
22793
|
+
throw new ReleaseError("INVALID_HOOK", "hook.cacheable must be a boolean when provided");
|
|
22794
|
+
}
|
|
22795
|
+
if (cacheInputs !== void 0) {
|
|
22796
|
+
if (!Array.isArray(cacheInputs) || cacheInputs.length === 0) {
|
|
22797
|
+
throw new ReleaseError(
|
|
22798
|
+
"INVALID_HOOK",
|
|
22799
|
+
"hook.cacheInputs must be a non-empty array of glob strings when provided"
|
|
22800
|
+
);
|
|
22473
22801
|
}
|
|
22474
|
-
|
|
22475
|
-
|
|
22802
|
+
for (const glob of cacheInputs) {
|
|
22803
|
+
if (typeof glob !== "string" || glob.length === 0) {
|
|
22804
|
+
throw new ReleaseError(
|
|
22805
|
+
"INVALID_HOOK",
|
|
22806
|
+
"every element of hook.cacheInputs must be a non-empty string"
|
|
22807
|
+
);
|
|
22808
|
+
}
|
|
22476
22809
|
}
|
|
22477
|
-
|
|
22478
|
-
|
|
22479
|
-
|
|
22480
|
-
|
|
22481
|
-
|
|
22482
|
-
);
|
|
22483
|
-
const { stdout: fetchedOut } = await exec(
|
|
22484
|
-
"git",
|
|
22485
|
-
["--git-dir", repositoryDir, "rev-parse", "FETCH_HEAD^{commit}"],
|
|
22486
|
-
{ shell: false }
|
|
22810
|
+
}
|
|
22811
|
+
if (cacheable === true && (!Array.isArray(cacheInputs) || cacheInputs.length === 0)) {
|
|
22812
|
+
throw new ReleaseError(
|
|
22813
|
+
"INVALID_HOOK",
|
|
22814
|
+
"hook.cacheable=true requires a non-empty hook.cacheInputs"
|
|
22487
22815
|
);
|
|
22488
|
-
|
|
22489
|
-
|
|
22490
|
-
|
|
22491
|
-
|
|
22492
|
-
|
|
22493
|
-
|
|
22816
|
+
}
|
|
22817
|
+
}
|
|
22818
|
+
function buildFilteredEnv(envAllowlist, contextEnv) {
|
|
22819
|
+
const env = {};
|
|
22820
|
+
for (const key of PLATFORM_REQUIRED_VARS) {
|
|
22821
|
+
const val = process.env[key];
|
|
22822
|
+
if (val !== void 0) {
|
|
22823
|
+
env[key] = val;
|
|
22494
22824
|
}
|
|
22495
|
-
parentCommit = parent.commit;
|
|
22496
22825
|
}
|
|
22497
|
-
|
|
22498
|
-
|
|
22499
|
-
|
|
22500
|
-
|
|
22501
|
-
|
|
22502
|
-
"--work-tree",
|
|
22503
|
-
snapshotDir,
|
|
22504
|
-
"add",
|
|
22505
|
-
"--all",
|
|
22506
|
-
"--force",
|
|
22507
|
-
"--",
|
|
22508
|
-
"."
|
|
22509
|
-
], { cwd: snapshotDir, env, shell: false });
|
|
22510
|
-
const { stdout: treeOut } = await exec("git", ["--git-dir", repositoryDir, "write-tree"], {
|
|
22511
|
-
env,
|
|
22512
|
-
shell: false
|
|
22513
|
-
});
|
|
22514
|
-
const tree = treeOut.trim();
|
|
22515
|
-
const commitEnv = {
|
|
22516
|
-
...env,
|
|
22517
|
-
GIT_AUTHOR_NAME: "release-skill",
|
|
22518
|
-
GIT_AUTHOR_EMAIL: "release-skill@localhost",
|
|
22519
|
-
GIT_COMMITTER_NAME: "release-skill",
|
|
22520
|
-
GIT_COMMITTER_EMAIL: "release-skill@localhost",
|
|
22521
|
-
GIT_AUTHOR_DATE: canonicalTimestamp,
|
|
22522
|
-
GIT_COMMITTER_DATE: canonicalTimestamp
|
|
22523
|
-
};
|
|
22524
|
-
const commitArgs = ["--git-dir", repositoryDir, "commit-tree", tree];
|
|
22525
|
-
if (parentCommit) commitArgs.push("-p", parentCommit);
|
|
22526
|
-
commitArgs.push("-m", `Release ${version}`);
|
|
22527
|
-
const { stdout: commitOut } = await exec(
|
|
22528
|
-
"git",
|
|
22529
|
-
commitArgs,
|
|
22530
|
-
{ env: commitEnv, shell: false }
|
|
22531
|
-
);
|
|
22532
|
-
const commit = commitOut.trim();
|
|
22533
|
-
await verifyGitTreeContent({ snapshotDir, repositoryDir, commit, expectedSnapshotDigest, exec });
|
|
22534
|
-
if (parentCommit) {
|
|
22535
|
-
const { stdout: parentsOut } = await exec(
|
|
22536
|
-
"git",
|
|
22537
|
-
["--git-dir", repositoryDir, "rev-list", "--parents", "-n", "1", commit],
|
|
22538
|
-
{ shell: false }
|
|
22539
|
-
);
|
|
22540
|
-
const [observedCommit, ...parents] = parentsOut.trim().split(/\s+/);
|
|
22541
|
-
if (observedCommit !== commit || parents.length !== 1 || parents[0] !== parentCommit) {
|
|
22542
|
-
throw frozenError("derived Git commit does not have the exact planned parent", {
|
|
22543
|
-
commit,
|
|
22544
|
-
parentCommit,
|
|
22545
|
-
observedParents: parents
|
|
22546
|
-
});
|
|
22826
|
+
if (contextEnv && envAllowlist) {
|
|
22827
|
+
for (const key of envAllowlist) {
|
|
22828
|
+
if (key in contextEnv) {
|
|
22829
|
+
env[key] = contextEnv[key];
|
|
22830
|
+
}
|
|
22547
22831
|
}
|
|
22548
22832
|
}
|
|
22549
|
-
return
|
|
22550
|
-
}
|
|
22551
|
-
function contentEntries(entries) {
|
|
22552
|
-
return entries.map(({ path: path3, size, contentDigest }) => ({ path: path3, size, contentDigest }));
|
|
22833
|
+
return env;
|
|
22553
22834
|
}
|
|
22554
|
-
async function
|
|
22555
|
-
|
|
22556
|
-
|
|
22557
|
-
|
|
22835
|
+
async function runHook(hook, context) {
|
|
22836
|
+
validateHook(hook);
|
|
22837
|
+
if (!context || typeof context !== "object" || Array.isArray(context)) {
|
|
22838
|
+
throw new ReleaseError("INVALID_HOOK", "context must be a non-null object");
|
|
22839
|
+
}
|
|
22840
|
+
if (typeof context.root !== "string" || !isAbsolute9(context.root)) {
|
|
22841
|
+
throw new ReleaseError("INVALID_HOOK", "context.root must be an absolute path");
|
|
22842
|
+
}
|
|
22843
|
+
const { command: command2, cwd, timeoutMs, envAllowlist = [] } = hook;
|
|
22844
|
+
const rootReal = await realpath6(context.root);
|
|
22845
|
+
const resolvedCwd = cwd ? await realpath6(resolve13(rootReal, cwd)) : rootReal;
|
|
22846
|
+
const rel = relative10(rootReal, resolvedCwd);
|
|
22847
|
+
if (rel.startsWith("..") || rel === "..") {
|
|
22848
|
+
throw new ReleaseError(
|
|
22849
|
+
"INVALID_HOOK",
|
|
22850
|
+
`hook.cwd "${cwd}" resolves outside project root`
|
|
22851
|
+
);
|
|
22852
|
+
}
|
|
22853
|
+
const env = buildFilteredEnv(envAllowlist, context.env);
|
|
22854
|
+
const executable = command2[0];
|
|
22855
|
+
const args2 = command2.slice(1);
|
|
22856
|
+
let timeoutController;
|
|
22857
|
+
let timeoutHandle;
|
|
22858
|
+
if (timeoutMs && timeoutMs > 0) {
|
|
22859
|
+
timeoutController = new AbortController();
|
|
22860
|
+
timeoutHandle = setTimeout(() => timeoutController.abort(), timeoutMs);
|
|
22861
|
+
}
|
|
22558
22862
|
try {
|
|
22559
|
-
const
|
|
22560
|
-
|
|
22561
|
-
|
|
22562
|
-
|
|
22563
|
-
|
|
22564
|
-
|
|
22565
|
-
|
|
22566
|
-
handle = await open4(tempPath, fsConstants2.O_RDONLY | fsConstants2.O_NOFOLLOW);
|
|
22567
|
-
await unlink2(tempPath);
|
|
22568
|
-
await rm3(tempDir, { recursive: true, force: true });
|
|
22569
|
-
return handle;
|
|
22570
|
-
} catch (err) {
|
|
22571
|
-
await handle?.close().catch(() => {
|
|
22863
|
+
const { stdout, stderr } = await execFileAsync(executable, args2, {
|
|
22864
|
+
cwd: resolvedCwd,
|
|
22865
|
+
env,
|
|
22866
|
+
shell: false,
|
|
22867
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
22868
|
+
// 10 MiB
|
|
22869
|
+
signal: timeoutController?.signal
|
|
22572
22870
|
});
|
|
22573
|
-
|
|
22871
|
+
return { exitCode: 0, stdout: stdout ?? "", stderr: stderr ?? "" };
|
|
22872
|
+
} catch (err) {
|
|
22873
|
+
if (err.name === "AbortError" || err.killed && err.signal === "SIGTERM") {
|
|
22874
|
+
throw new ReleaseError(
|
|
22875
|
+
"HOOK_TIMEOUT",
|
|
22876
|
+
`hook timed out after ${timeoutMs}ms: ${executable} ${args2.join(" ")}`,
|
|
22877
|
+
{ command: command2, timeoutMs }
|
|
22878
|
+
);
|
|
22879
|
+
}
|
|
22880
|
+
if ("stdout" in err) {
|
|
22881
|
+
return {
|
|
22882
|
+
exitCode: typeof err.code === "number" ? err.code : 1,
|
|
22883
|
+
stdout: err.stdout ?? "",
|
|
22884
|
+
stderr: err.stderr ?? ""
|
|
22885
|
+
};
|
|
22886
|
+
}
|
|
22574
22887
|
throw err;
|
|
22888
|
+
} finally {
|
|
22889
|
+
if (timeoutHandle) {
|
|
22890
|
+
clearTimeout(timeoutHandle);
|
|
22891
|
+
}
|
|
22575
22892
|
}
|
|
22576
22893
|
}
|
|
22577
|
-
|
|
22578
|
-
|
|
22579
|
-
|
|
22580
|
-
|
|
22581
|
-
|
|
22582
|
-
|
|
22583
|
-
|
|
22584
|
-
|
|
22585
|
-
|
|
22586
|
-
|
|
22587
|
-
|
|
22588
|
-
|
|
22589
|
-
|
|
22590
|
-
|
|
22591
|
-
});
|
|
22592
|
-
child.stderr.on("data", (chunk) => {
|
|
22593
|
-
stderr += chunk;
|
|
22594
|
-
});
|
|
22595
|
-
child.on("error", rejectPromise);
|
|
22596
|
-
child.on("close", (code) => {
|
|
22597
|
-
if (code === 0) resolvePromise({ stdout, stderr });
|
|
22598
|
-
else rejectPromise(new Error(`tar exited with code ${code}: ${stderr.trim()}`));
|
|
22599
|
-
});
|
|
22600
|
-
});
|
|
22601
|
-
}
|
|
22602
|
-
async function verifyNpmTarballContent({ snapshotDir, tarballBytes, tarballDir, expectedSnapshotDigest }) {
|
|
22603
|
-
const listHandle = await createDetachedReadHandle(tarballBytes, tarballDir);
|
|
22604
|
-
let listOut;
|
|
22605
|
-
try {
|
|
22606
|
-
({ stdout: listOut } = await runTarFromHandle(listHandle, (fdPath) => ["-tzf", fdPath]));
|
|
22607
|
-
} finally {
|
|
22608
|
-
await listHandle.close();
|
|
22894
|
+
var execFileAsync, PLATFORM_REQUIRED_VARS, ENV_KEY_PATTERN;
|
|
22895
|
+
var init_hooks = __esm({
|
|
22896
|
+
"src/core/hooks.mjs"() {
|
|
22897
|
+
init_errors();
|
|
22898
|
+
execFileAsync = promisify5(execFileCb5);
|
|
22899
|
+
PLATFORM_REQUIRED_VARS = (() => {
|
|
22900
|
+
const posix3 = ["PATH", "HOME", "USER", "LANG", "LC_ALL", "LC_CTYPE", "TERM"];
|
|
22901
|
+
const win322 = ["PATH", "HOME", "USER", "SystemRoot", "SYSTEMROOT", "COMSPEC", "PATHEXT", "TEMP", "TMP"];
|
|
22902
|
+
return new Set(process.platform === "win32" ? win322 : posix3);
|
|
22903
|
+
})();
|
|
22904
|
+
ENV_KEY_PATTERN = /^[A-Z_][A-Z0-9_]*$/;
|
|
22905
|
+
__name(validateHook, "validateHook");
|
|
22906
|
+
__name(buildFilteredEnv, "buildFilteredEnv");
|
|
22907
|
+
__name(runHook, "runHook");
|
|
22609
22908
|
}
|
|
22610
|
-
|
|
22611
|
-
|
|
22612
|
-
|
|
22909
|
+
});
|
|
22910
|
+
|
|
22911
|
+
// src/core/hook-cache.mjs
|
|
22912
|
+
import { readdir as readdir4, readFile as readFile10, mkdir as mkdir5, writeFile as writeFile5 } from "node:fs/promises";
|
|
22913
|
+
import { join as join9 } from "node:path";
|
|
22914
|
+
function globToRegExp(glob) {
|
|
22915
|
+
let source = "";
|
|
22916
|
+
let i = 0;
|
|
22917
|
+
while (i < glob.length) {
|
|
22918
|
+
const c = glob[i];
|
|
22919
|
+
if (c === "*") {
|
|
22920
|
+
if (glob[i + 1] === "*") {
|
|
22921
|
+
source += ".*";
|
|
22922
|
+
i += 2;
|
|
22923
|
+
} else {
|
|
22924
|
+
source += "[^/]*";
|
|
22925
|
+
i += 1;
|
|
22926
|
+
}
|
|
22927
|
+
} else if (c === "?") {
|
|
22928
|
+
source += "[^/]";
|
|
22929
|
+
i += 1;
|
|
22930
|
+
} else {
|
|
22931
|
+
source += c.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
22932
|
+
i += 1;
|
|
22933
|
+
}
|
|
22613
22934
|
}
|
|
22614
|
-
|
|
22615
|
-
|
|
22616
|
-
|
|
22935
|
+
return new RegExp(`^${source}$`);
|
|
22936
|
+
}
|
|
22937
|
+
async function listInputFiles(root) {
|
|
22938
|
+
const out = [];
|
|
22939
|
+
async function walk(dirAbs, dirRel) {
|
|
22940
|
+
let entries;
|
|
22617
22941
|
try {
|
|
22618
|
-
await
|
|
22619
|
-
}
|
|
22620
|
-
|
|
22942
|
+
entries = await readdir4(dirAbs, { withFileTypes: true });
|
|
22943
|
+
} catch {
|
|
22944
|
+
return;
|
|
22621
22945
|
}
|
|
22622
|
-
const
|
|
22623
|
-
|
|
22624
|
-
|
|
22625
|
-
|
|
22626
|
-
|
|
22627
|
-
})
|
|
22946
|
+
for (const entry of entries) {
|
|
22947
|
+
if (entry.isDirectory()) {
|
|
22948
|
+
if (SKIPPED_DIRS.has(entry.name)) continue;
|
|
22949
|
+
const rel = dirRel ? `${dirRel}/${entry.name}` : entry.name;
|
|
22950
|
+
await walk(join9(dirAbs, entry.name), rel);
|
|
22951
|
+
} else if (entry.isFile()) {
|
|
22952
|
+
out.push(dirRel ? `${dirRel}/${entry.name}` : entry.name);
|
|
22953
|
+
}
|
|
22954
|
+
}
|
|
22955
|
+
}
|
|
22956
|
+
__name(walk, "walk");
|
|
22957
|
+
await walk(root, "");
|
|
22958
|
+
out.sort();
|
|
22959
|
+
return out;
|
|
22960
|
+
}
|
|
22961
|
+
async function computeHookCacheKey(hook, root) {
|
|
22962
|
+
const globs = Array.isArray(hook.cacheInputs) ? hook.cacheInputs : [];
|
|
22963
|
+
const matchers = globs.map((glob) => ({ glob, re: globToRegExp(glob) }));
|
|
22964
|
+
const allFiles = await listInputFiles(root);
|
|
22965
|
+
const matched = [];
|
|
22966
|
+
const hitPerGlob = matchers.map(() => false);
|
|
22967
|
+
for (const relPath of allFiles) {
|
|
22968
|
+
let hit = false;
|
|
22969
|
+
for (let i = 0; i < matchers.length; i += 1) {
|
|
22970
|
+
if (matchers[i].re.test(relPath)) {
|
|
22971
|
+
hitPerGlob[i] = true;
|
|
22972
|
+
hit = true;
|
|
22973
|
+
}
|
|
22628
22974
|
}
|
|
22629
|
-
|
|
22630
|
-
|
|
22631
|
-
|
|
22975
|
+
if (hit) matched.push(relPath);
|
|
22976
|
+
}
|
|
22977
|
+
for (let i = 0; i < matchers.length; i += 1) {
|
|
22978
|
+
if (!hitPerGlob[i]) {
|
|
22979
|
+
throw new ReleaseError(
|
|
22980
|
+
GATE_FAILED,
|
|
22981
|
+
`hook cacheInputs glob "${matchers[i].glob}" matched no files; refusing to cache`,
|
|
22982
|
+
{ glob: matchers[i].glob }
|
|
22983
|
+
);
|
|
22632
22984
|
}
|
|
22633
|
-
} finally {
|
|
22634
|
-
await rm3(verifyDir, { recursive: true, force: true });
|
|
22635
22985
|
}
|
|
22986
|
+
matched.sort();
|
|
22987
|
+
const fileEntries = [];
|
|
22988
|
+
for (const relPath of matched) {
|
|
22989
|
+
const content = await readFile10(join9(root, relPath));
|
|
22990
|
+
fileEntries.push({ path: relPath, sha256: sha256Hex(content) });
|
|
22991
|
+
}
|
|
22992
|
+
const cacheKey = sha256Hex(canonicalJson(hook) + canonicalJson(fileEntries));
|
|
22993
|
+
return { cacheKey, matchedFiles: matched };
|
|
22636
22994
|
}
|
|
22637
|
-
|
|
22638
|
-
|
|
22639
|
-
|
|
22640
|
-
|
|
22641
|
-
|
|
22642
|
-
|
|
22643
|
-
|
|
22644
|
-
let parsed;
|
|
22995
|
+
function hookCacheDir(root, hookName) {
|
|
22996
|
+
return join9(root, ...CACHE_BASE, hookName);
|
|
22997
|
+
}
|
|
22998
|
+
function hookCachePath(root, hookName, cacheKey) {
|
|
22999
|
+
return join9(hookCacheDir(root, hookName), `${cacheKey}.json`);
|
|
23000
|
+
}
|
|
23001
|
+
async function readHookCache(root, hookName, cacheKey) {
|
|
22645
23002
|
try {
|
|
22646
|
-
|
|
23003
|
+
const raw = await readFile10(hookCachePath(root, hookName, cacheKey), "utf8");
|
|
23004
|
+
const record = JSON.parse(raw);
|
|
23005
|
+
if (record && record.cacheKey === cacheKey && record.exitCode === 0) {
|
|
23006
|
+
return record;
|
|
23007
|
+
}
|
|
23008
|
+
return null;
|
|
22647
23009
|
} catch {
|
|
22648
|
-
|
|
22649
|
-
}
|
|
22650
|
-
const info = Array.isArray(parsed) ? parsed[0] : parsed;
|
|
22651
|
-
if (!info?.filename || !info?.integrity) {
|
|
22652
|
-
throw frozenError("npm pack did not return filename and integrity");
|
|
23010
|
+
return null;
|
|
22653
23011
|
}
|
|
22654
|
-
const tarballPath = join9(tarballDir, info.filename);
|
|
22655
|
-
const { bytes } = await readStableRegularFile(tarballPath, info.filename);
|
|
22656
|
-
if (!expectedSnapshotDigest) throw frozenError("npm tarball build requires the sealed snapshot digest");
|
|
22657
|
-
await verifyNpmTarballContent({
|
|
22658
|
-
snapshotDir,
|
|
22659
|
-
tarballBytes: bytes,
|
|
22660
|
-
tarballDir,
|
|
22661
|
-
expectedSnapshotDigest
|
|
22662
|
-
});
|
|
22663
|
-
return {
|
|
22664
|
-
tarballPath,
|
|
22665
|
-
integrity: info.integrity,
|
|
22666
|
-
sha256: createHash5("sha256").update(bytes).digest("hex"),
|
|
22667
|
-
size: bytes.length
|
|
22668
|
-
};
|
|
22669
23012
|
}
|
|
22670
|
-
async function
|
|
22671
|
-
|
|
22672
|
-
|
|
22673
|
-
|
|
22674
|
-
|
|
22675
|
-
|
|
22676
|
-
|
|
22677
|
-
|
|
22678
|
-
|
|
22679
|
-
|
|
22680
|
-
|
|
22681
|
-
|
|
22682
|
-
}
|
|
23013
|
+
async function writeHookCache(root, hookName, cacheKey, result) {
|
|
23014
|
+
if (!result || result.exitCode !== 0) {
|
|
23015
|
+
return { ok: false, error: "refusing to cache a non-zero exit result" };
|
|
23016
|
+
}
|
|
23017
|
+
try {
|
|
23018
|
+
await mkdir5(hookCacheDir(root, hookName), { recursive: true });
|
|
23019
|
+
const record = {
|
|
23020
|
+
cacheKey,
|
|
23021
|
+
exitCode: 0,
|
|
23022
|
+
stdoutTail: String(result.stdoutTail ?? "").slice(-TAIL_LENGTH),
|
|
23023
|
+
stderrTail: String(result.stderrTail ?? "").slice(-TAIL_LENGTH),
|
|
23024
|
+
createdAt: result.createdAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
23025
|
+
};
|
|
23026
|
+
await writeFile5(hookCachePath(root, hookName, cacheKey), `${JSON.stringify(record, null, 2)}
|
|
23027
|
+
`, "utf8");
|
|
23028
|
+
return { ok: true };
|
|
23029
|
+
} catch (err) {
|
|
23030
|
+
return { ok: false, error: err.message };
|
|
22683
23031
|
}
|
|
22684
|
-
__name(walk, "walk");
|
|
22685
|
-
await walk(snapshotDir);
|
|
22686
|
-
await chmod(snapshotDir, 365);
|
|
22687
23032
|
}
|
|
22688
|
-
var
|
|
22689
|
-
var
|
|
22690
|
-
"src/
|
|
23033
|
+
var CACHE_BASE, TAIL_LENGTH, SKIPPED_DIRS;
|
|
23034
|
+
var init_hook_cache = __esm({
|
|
23035
|
+
"src/core/hook-cache.mjs"() {
|
|
23036
|
+
init_digest();
|
|
22691
23037
|
init_errors();
|
|
22692
|
-
|
|
22693
|
-
|
|
22694
|
-
|
|
22695
|
-
__name(
|
|
22696
|
-
__name(
|
|
22697
|
-
__name(
|
|
22698
|
-
__name(
|
|
22699
|
-
__name(
|
|
22700
|
-
__name(
|
|
22701
|
-
__name(
|
|
22702
|
-
__name(verifyGitTreeContent, "verifyGitTreeContent");
|
|
22703
|
-
GIT_ISO8601_STRICT_RE = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d{1,9})?(Z|[+-]\d{2}:\d{2})$/;
|
|
22704
|
-
__name(normalizeGitTimestamp, "normalizeGitTimestamp");
|
|
22705
|
-
__name(publicGitRemote, "publicGitRemote");
|
|
22706
|
-
__name(buildFrozenGitRepository, "buildFrozenGitRepository");
|
|
22707
|
-
__name(contentEntries, "contentEntries");
|
|
22708
|
-
__name(createDetachedReadHandle, "createDetachedReadHandle");
|
|
22709
|
-
__name(runTarFromHandle, "runTarFromHandle");
|
|
22710
|
-
__name(verifyNpmTarballContent, "verifyNpmTarballContent");
|
|
22711
|
-
__name(buildFrozenNpmTarball, "buildFrozenNpmTarball");
|
|
22712
|
-
__name(sealFrozenSnapshot, "sealFrozenSnapshot");
|
|
23038
|
+
CACHE_BASE = [".release-skill", "cache", "hooks"];
|
|
23039
|
+
TAIL_LENGTH = 4e3;
|
|
23040
|
+
SKIPPED_DIRS = /* @__PURE__ */ new Set([".git", "node_modules", ".release-skill"]);
|
|
23041
|
+
__name(globToRegExp, "globToRegExp");
|
|
23042
|
+
__name(listInputFiles, "listInputFiles");
|
|
23043
|
+
__name(computeHookCacheKey, "computeHookCacheKey");
|
|
23044
|
+
__name(hookCacheDir, "hookCacheDir");
|
|
23045
|
+
__name(hookCachePath, "hookCachePath");
|
|
23046
|
+
__name(readHookCache, "readHookCache");
|
|
23047
|
+
__name(writeHookCache, "writeHookCache");
|
|
22713
23048
|
}
|
|
22714
23049
|
});
|
|
22715
23050
|
|
|
@@ -25188,7 +25523,7 @@ import { execFile as execFileCb6 } from "node:child_process";
|
|
|
25188
25523
|
import { promisify as promisify6 } from "node:util";
|
|
25189
25524
|
import { lstat as lstat10, mkdtemp as mkdtemp2, readFile as readFile15, readdir as readdir9, realpath as realpath9, rm as rm5 } from "node:fs/promises";
|
|
25190
25525
|
import { join as join13, relative as relative15, resolve as resolve17, sep as sep4 } from "node:path";
|
|
25191
|
-
import { tmpdir as
|
|
25526
|
+
import { tmpdir as tmpdir3 } from "node:os";
|
|
25192
25527
|
async function computeSourceInputClosure({ units, root }) {
|
|
25193
25528
|
const realRoot = await realpath9(resolve17(root));
|
|
25194
25529
|
const entriesByPath = /* @__PURE__ */ new Map();
|
|
@@ -25502,7 +25837,7 @@ async function verifyWithTemporaryGit({
|
|
|
25502
25837
|
if (!branchLine) {
|
|
25503
25838
|
return failure(REF_MISSING, `remote ref "refs/heads/${defaultBranch}" does not exist`);
|
|
25504
25839
|
}
|
|
25505
|
-
const tempRoot = await mkdtemp2(join13(
|
|
25840
|
+
const tempRoot = await mkdtemp2(join13(tmpdir3(), "release-skill-source-authority-"));
|
|
25506
25841
|
try {
|
|
25507
25842
|
await execFn("git", ["init", "--bare", tempRoot], {
|
|
25508
25843
|
encoding: "utf8",
|
|
@@ -70128,6 +70463,7 @@ __export(npm_exports, {
|
|
|
70128
70463
|
registryTokenKey: () => registryTokenKey,
|
|
70129
70464
|
resolveNpmRegistryAuthToken: () => resolveNpmRegistryAuthToken,
|
|
70130
70465
|
resolvePackageCwd: () => resolvePackageCwd,
|
|
70466
|
+
verifyFrozenNpmTarballContract: () => verifyFrozenNpmTarballContract,
|
|
70131
70467
|
verifyFrozenNpmTarballIdentity: () => verifyFrozenNpmTarballIdentity
|
|
70132
70468
|
});
|
|
70133
70469
|
import { createHash as createHash9, randomUUID } from "node:crypto";
|
|
@@ -70377,6 +70713,33 @@ async function verifyFrozenNpmTarballIdentity(action, root) {
|
|
|
70377
70713
|
version: action.version
|
|
70378
70714
|
});
|
|
70379
70715
|
}
|
|
70716
|
+
async function verifyNpmTarballBufferContract(buffer, action, tarballDir) {
|
|
70717
|
+
const manifest = extractManifestFromTarball(buffer, {
|
|
70718
|
+
name: action.package,
|
|
70719
|
+
version: action.version
|
|
70720
|
+
});
|
|
70721
|
+
const fileIndex = await buildTarballFileIndex(buffer, tarballDir);
|
|
70722
|
+
const closureResult = checkNpmEntryClosure(manifest, fileIndex);
|
|
70723
|
+
if (closureResult.errors.length > 0) {
|
|
70724
|
+
throw new ReleaseError(
|
|
70725
|
+
GATE_FAILED,
|
|
70726
|
+
`npm entry closure check failed for ${action.package}@${action.version}: ${closureResult.errors.length} error(s)`,
|
|
70727
|
+
{
|
|
70728
|
+
gate: "npm-entry-closure",
|
|
70729
|
+
package: action.package,
|
|
70730
|
+
version: action.version,
|
|
70731
|
+
entries: closureResult.entries,
|
|
70732
|
+
errors: closureResult.errors,
|
|
70733
|
+
diagnostics: closureResult.diagnostics
|
|
70734
|
+
}
|
|
70735
|
+
);
|
|
70736
|
+
}
|
|
70737
|
+
return { manifest, closureResult };
|
|
70738
|
+
}
|
|
70739
|
+
async function verifyFrozenNpmTarballContract(action, root, tarballDir) {
|
|
70740
|
+
const buffer = await readVerifiedTarballBytes(action, root);
|
|
70741
|
+
return verifyNpmTarballBufferContract(buffer, action, tarballDir);
|
|
70742
|
+
}
|
|
70380
70743
|
async function spawnNpmPublishTarball({ args: args2, cwd }) {
|
|
70381
70744
|
return new Promise((resolvePromise, rejectPromise) => {
|
|
70382
70745
|
const child = spawn3("npm", args2, {
|
|
@@ -70460,7 +70823,7 @@ function createNpmAdapter(deps = {}) {
|
|
|
70460
70823
|
}
|
|
70461
70824
|
const cwd = resolvePackageCwd(action.cwd, context.root);
|
|
70462
70825
|
if (action.tarballPath) {
|
|
70463
|
-
await
|
|
70826
|
+
await verifyFrozenNpmTarballContract(action, context.root);
|
|
70464
70827
|
}
|
|
70465
70828
|
let whoamiUser;
|
|
70466
70829
|
if (action.tarballPath) {
|
|
@@ -70520,7 +70883,7 @@ function createNpmAdapter(deps = {}) {
|
|
|
70520
70883
|
throw new Error("frozen npm tarball requires a stable Buffer publish capability");
|
|
70521
70884
|
}
|
|
70522
70885
|
const buffer = await readVerifiedTarballBytes(action, context.root);
|
|
70523
|
-
const manifest =
|
|
70886
|
+
const { manifest } = await verifyNpmTarballBufferContract(buffer, action);
|
|
70524
70887
|
let token;
|
|
70525
70888
|
try {
|
|
70526
70889
|
token = await resolveAuthToken({ registry: normalizedRegistry, cwd, exec, env: authEnv });
|
|
@@ -70669,6 +71032,8 @@ var init_npm = __esm({
|
|
|
70669
71032
|
import_libnpmpublish = __toESM(require_lib25(), 1);
|
|
70670
71033
|
import_npm_registry_fetch = __toESM(require_lib15(), 1);
|
|
70671
71034
|
init_contract();
|
|
71035
|
+
init_npm_entry_closure();
|
|
71036
|
+
init_errors();
|
|
70672
71037
|
execFile6 = promisify7(execFileCb7);
|
|
70673
71038
|
NAME = "npm";
|
|
70674
71039
|
SAFE_PACKAGE_NAME = /^(?:@[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*|[a-z0-9][a-z0-9._-]*)$/;
|
|
@@ -70687,6 +71052,8 @@ var init_npm = __esm({
|
|
|
70687
71052
|
__name(readVerifiedTarballBytes, "readVerifiedTarballBytes");
|
|
70688
71053
|
__name(extractManifestFromTarball, "extractManifestFromTarball");
|
|
70689
71054
|
__name(verifyFrozenNpmTarballIdentity, "verifyFrozenNpmTarballIdentity");
|
|
71055
|
+
__name(verifyNpmTarballBufferContract, "verifyNpmTarballBufferContract");
|
|
71056
|
+
__name(verifyFrozenNpmTarballContract, "verifyFrozenNpmTarballContract");
|
|
70690
71057
|
__name(spawnNpmPublishTarball, "spawnNpmPublishTarball");
|
|
70691
71058
|
__name(queryVersion, "queryVersion");
|
|
70692
71059
|
__name(createNpmAdapter, "createNpmAdapter");
|
|
@@ -73253,19 +73620,7 @@ function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
73253
73620
|
}
|
|
73254
73621
|
let verifiedInstallPath = null;
|
|
73255
73622
|
if (normalizedAttestation.installPath) {
|
|
73256
|
-
const managedRoot = resolve20(attestationDir, "kimi-home", "plugins", "managed");
|
|
73257
73623
|
const installPathAbs = resolve20(normalizedAttestation.installPath);
|
|
73258
|
-
const managedRootReal = await realpath11(managedRoot).catch(() => null);
|
|
73259
|
-
if (!managedRootReal) {
|
|
73260
|
-
return createResult({
|
|
73261
|
-
actionType,
|
|
73262
|
-
status: ActionStatus.OBSERVED,
|
|
73263
|
-
observation: {
|
|
73264
|
-
installed: false,
|
|
73265
|
-
error: "KIMI_CODE_HOME does not exist"
|
|
73266
|
-
}
|
|
73267
|
-
});
|
|
73268
|
-
}
|
|
73269
73624
|
let lexicalStat;
|
|
73270
73625
|
try {
|
|
73271
73626
|
lexicalStat = await lstat13(installPathAbs);
|
|
@@ -73300,18 +73655,6 @@ function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
73300
73655
|
}
|
|
73301
73656
|
});
|
|
73302
73657
|
}
|
|
73303
|
-
const rel = relative18(managedRootReal, installPathReal2);
|
|
73304
|
-
const sep7 = process.platform === "win32" ? "\\" : "/";
|
|
73305
|
-
if (rel === "" || rel === ".." || isAbsolute15(rel) || rel.startsWith(`..${sep7}`)) {
|
|
73306
|
-
return createResult({
|
|
73307
|
-
actionType,
|
|
73308
|
-
status: ActionStatus.OBSERVED,
|
|
73309
|
-
observation: {
|
|
73310
|
-
installed: false,
|
|
73311
|
-
error: `kimi attestation installPath escapes the managed root: ${normalizedAttestation.installPath}`
|
|
73312
|
-
}
|
|
73313
|
-
});
|
|
73314
|
-
}
|
|
73315
73658
|
verifiedInstallPath = installPathReal2;
|
|
73316
73659
|
}
|
|
73317
73660
|
let manifestDigest2 = action.manifestDigest;
|
|
@@ -79771,13 +80114,13 @@ async function buildProductionAssets(unitResults, resolvedVersions, root, runDir
|
|
|
79771
80114
|
tarballDir: resolveUnitScopedPath(resolve22(runDir, "tarballs"), unit.id),
|
|
79772
80115
|
expectedSnapshotDigest: sealed.digest
|
|
79773
80116
|
});
|
|
79774
|
-
await
|
|
80117
|
+
await verifyFrozenNpmTarballContract({
|
|
79775
80118
|
package: npmDistribution.package,
|
|
79776
80119
|
version,
|
|
79777
80120
|
tarballPath: relative21(root, npm.tarballPath),
|
|
79778
80121
|
tarballSha256: npm.sha256,
|
|
79779
80122
|
integrity: npm.integrity
|
|
79780
|
-
}, root);
|
|
80123
|
+
}, root, resolveUnitScopedPath(resolve22(runDir, "tarballs"), unit.id));
|
|
79781
80124
|
}
|
|
79782
80125
|
assets.push({
|
|
79783
80126
|
snapshotPath,
|
|
@@ -82159,6 +82502,37 @@ async function reconcileRelease(options) {
|
|
|
82159
82502
|
);
|
|
82160
82503
|
}
|
|
82161
82504
|
await evidence.append({ phase: "safety-gate", gate: "action-completeness", status: "passed" });
|
|
82505
|
+
for (const unit of plan.units ?? []) {
|
|
82506
|
+
const frozen = unit.frozenSnapshot;
|
|
82507
|
+
if (!frozen?.npm) continue;
|
|
82508
|
+
const npmDistribution = (unit.distributions ?? []).find((distribution) => distribution.type === "npm");
|
|
82509
|
+
if (!npmDistribution) {
|
|
82510
|
+
throw new ReleaseError(
|
|
82511
|
+
GATE_FAILED,
|
|
82512
|
+
`unit "${unit.id}" has a frozen npm tarball but no npm distribution`,
|
|
82513
|
+
{ gate: "npm-entry-closure", unitId: unit.id }
|
|
82514
|
+
);
|
|
82515
|
+
}
|
|
82516
|
+
await evidence.append({
|
|
82517
|
+
phase: "safety-gate",
|
|
82518
|
+
gate: "npm-entry-closure",
|
|
82519
|
+
unitId: unit.id,
|
|
82520
|
+
status: "started"
|
|
82521
|
+
});
|
|
82522
|
+
await verifyFrozenNpmTarballContract({
|
|
82523
|
+
package: npmDistribution.package,
|
|
82524
|
+
version: unit.targetVersion,
|
|
82525
|
+
tarballPath: frozen.npm.tarballPath,
|
|
82526
|
+
tarballSha256: frozen.npm.tarballSha256,
|
|
82527
|
+
integrity: frozen.npm.integrity
|
|
82528
|
+
}, root);
|
|
82529
|
+
await evidence.append({
|
|
82530
|
+
phase: "safety-gate",
|
|
82531
|
+
gate: "npm-entry-closure",
|
|
82532
|
+
unitId: unit.id,
|
|
82533
|
+
status: "passed"
|
|
82534
|
+
});
|
|
82535
|
+
}
|
|
82162
82536
|
await evidence.append({ phase: "safety-gate", gate: "source-run-load", status: "started" });
|
|
82163
82537
|
const sourceRun = await loadRun(sourceRunPath, {
|
|
82164
82538
|
requireDigest: Boolean(plan.production),
|
|
@@ -83070,6 +83444,7 @@ var init_reconcile = __esm({
|
|
|
83070
83444
|
init_source_authority();
|
|
83071
83445
|
init_contract();
|
|
83072
83446
|
init_observe_retry();
|
|
83447
|
+
init_npm();
|
|
83073
83448
|
__name(defaultClock3, "defaultClock");
|
|
83074
83449
|
__name(reconcileRelease, "reconcileRelease");
|
|
83075
83450
|
}
|
|
@@ -83666,7 +84041,7 @@ __export(verify_exports, {
|
|
|
83666
84041
|
import { readFile as readFile24, writeFile as writeFile8, mkdtemp as mkdtemp4, rm as rm8, mkdir as mkdir13, lstat as lstat14, realpath as realpath13, readdir as readdir12 } from "node:fs/promises";
|
|
83667
84042
|
import { realpathSync as realpathSync4 } from "node:fs";
|
|
83668
84043
|
import { dirname as dirname13, join as join20, relative as relative22, isAbsolute as isAbsolute18, resolve as resolve25, basename as basename8 } from "node:path";
|
|
83669
|
-
import { tmpdir as
|
|
84044
|
+
import { tmpdir as tmpdir4 } from "node:os";
|
|
83670
84045
|
import { execFile as execFileCb12 } from "node:child_process";
|
|
83671
84046
|
import { promisify as promisify12 } from "node:util";
|
|
83672
84047
|
function defaultClock4() {
|
|
@@ -83693,7 +84068,7 @@ function matchesSubset2(actual, expected) {
|
|
|
83693
84068
|
return true;
|
|
83694
84069
|
}
|
|
83695
84070
|
async function runSmokeTest(plan, root, options = {}) {
|
|
83696
|
-
const baseDir = options.baseDir ??
|
|
84071
|
+
const baseDir = options.baseDir ?? tmpdir4();
|
|
83697
84072
|
await mkdir13(baseDir, { recursive: true });
|
|
83698
84073
|
const tmpDir = await mkdtemp4(join20(baseDir, "verify-smoke-"));
|
|
83699
84074
|
const npmExec = options.npmExecutor ?? defaultNpmExecutor;
|
|
@@ -83796,6 +84171,24 @@ async function runSmokeTest(plan, root, options = {}) {
|
|
|
83796
84171
|
};
|
|
83797
84172
|
}
|
|
83798
84173
|
const pkgRoot = join20(installDir, "node_modules", pkgName);
|
|
84174
|
+
{
|
|
84175
|
+
const dirIndex = await buildDirectoryFileIndex(pkgRoot);
|
|
84176
|
+
const closureResult = checkNpmEntryClosure(installedPkg, dirIndex);
|
|
84177
|
+
if (closureResult.errors.length > 0) {
|
|
84178
|
+
return {
|
|
84179
|
+
passed: false,
|
|
84180
|
+
details: {
|
|
84181
|
+
gate: "npm-entry-closure",
|
|
84182
|
+
error: `npm entry closure check failed for ${packageAtVersion}: ${closureResult.errors.map((e) => e.message).join("; ")}`,
|
|
84183
|
+
packageAtVersion,
|
|
84184
|
+
unitId,
|
|
84185
|
+
entries: closureResult.entries,
|
|
84186
|
+
errors: closureResult.errors,
|
|
84187
|
+
diagnostics: closureResult.diagnostics
|
|
84188
|
+
}
|
|
84189
|
+
};
|
|
84190
|
+
}
|
|
84191
|
+
}
|
|
83799
84192
|
if (plan.skillResourceClosure) {
|
|
83800
84193
|
const expectedUnitReceipt = plan.skillResourceClosure.unitReceipts.find((item) => item.unitId === unitId);
|
|
83801
84194
|
if (!expectedUnitReceipt) {
|
|
@@ -84450,8 +84843,7 @@ async function verifyRelease(options) {
|
|
|
84450
84843
|
} : action.type === "codebuddy-marketplace-install" ? {
|
|
84451
84844
|
HOME: resolve25(runDir, "consumers", `codebuddy-${action.parameters.plugin}`)
|
|
84452
84845
|
} : {
|
|
84453
|
-
HOME: resolve25(runDir, "consumers", `kimi-${action.parameters.plugin}`)
|
|
84454
|
-
KIMI_CODE_HOME: resolve25(runDir, "consumers", `kimi-${action.parameters.plugin}`)
|
|
84846
|
+
HOME: resolve25(runDir, "consumers", `kimi-${action.parameters.plugin}`)
|
|
84455
84847
|
}
|
|
84456
84848
|
}));
|
|
84457
84849
|
} else {
|
|
@@ -84710,6 +85102,7 @@ var init_verify = __esm({
|
|
|
84710
85102
|
init_skill_resource_closure();
|
|
84711
85103
|
init_checkpoints();
|
|
84712
85104
|
init_installation_contract();
|
|
85105
|
+
init_npm_entry_closure();
|
|
84713
85106
|
execFile11 = promisify12(execFileCb12);
|
|
84714
85107
|
VERIFICATION_RESOLVED_TYPES = Object.freeze({
|
|
84715
85108
|
PASSED_AUTOMATIC: "PASSED_AUTOMATIC",
|
|
@@ -85140,7 +85533,7 @@ async function publishRelease(options) {
|
|
|
85140
85533
|
if (!npmDistribution) {
|
|
85141
85534
|
throw new ReleaseError(GATE_FAILED, `unit "${unit.id}" has a frozen npm tarball but no npm distribution`);
|
|
85142
85535
|
}
|
|
85143
|
-
await
|
|
85536
|
+
await verifyFrozenNpmTarballContract({
|
|
85144
85537
|
package: npmDistribution.package,
|
|
85145
85538
|
version: unit.targetVersion,
|
|
85146
85539
|
tarballPath: frozen.npm.tarballPath,
|
|
@@ -86368,7 +86761,7 @@ var init_graph = __esm({
|
|
|
86368
86761
|
import { readFile as readFile28, readdir as readdir14, stat as stat6, mkdir as mkdir15, writeFile as writeFile9, rm as rm9 } from "node:fs/promises";
|
|
86369
86762
|
import { join as join24 } from "node:path";
|
|
86370
86763
|
import { mkdtemp as mkdtemp5 } from "node:fs/promises";
|
|
86371
|
-
import { tmpdir as
|
|
86764
|
+
import { tmpdir as tmpdir5 } from "node:os";
|
|
86372
86765
|
import { createHash as createHash12 } from "node:crypto";
|
|
86373
86766
|
import { readFileSync as readFileSync4 } from "node:fs";
|
|
86374
86767
|
function collectStaticImports(filePath, visited = /* @__PURE__ */ new Set()) {
|
|
@@ -86506,7 +86899,7 @@ async function runProducerClosure({
|
|
|
86506
86899
|
graph,
|
|
86507
86900
|
inputSnapshot,
|
|
86508
86901
|
artifactIds,
|
|
86509
|
-
tempRootFactory = /* @__PURE__ */ __name(async () => mkdtemp5(join24(
|
|
86902
|
+
tempRootFactory = /* @__PURE__ */ __name(async () => mkdtemp5(join24(tmpdir5(), "producer-")), "tempRootFactory")
|
|
86510
86903
|
} = {}) {
|
|
86511
86904
|
const generatedSet = new Set(graph.topologicalOrder);
|
|
86512
86905
|
for (const id of artifactIds) {
|