@openthink/stamp 1.9.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +163 -46
- package/dist/{chunk-NWZ7AJ2Y.js → chunk-4PFD2DSY.js} +99 -6
- package/dist/chunk-4PFD2DSY.js.map +1 -0
- package/dist/hooks/pre-receive.cjs +920 -104
- package/dist/hooks/pre-receive.cjs.map +1 -1
- package/dist/index.js +7037 -2918
- package/dist/index.js.map +1 -1
- package/dist/server/bootstrap-review-key.cjs +196 -0
- package/dist/server/bootstrap-review-key.cjs.map +1 -0
- package/dist/server/stamp-review.cjs +8354 -0
- package/dist/server/stamp-review.cjs.map +1 -0
- package/dist/{ui-KOLYLKT2.js → ui-P5DRAT3P.js} +2 -2
- package/package.json +2 -1
- package/dist/chunk-NWZ7AJ2Y.js.map +0 -1
- /package/dist/{ui-KOLYLKT2.js.map → ui-P5DRAT3P.js.map} +0 -0
|
@@ -9,6 +9,10 @@ var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
|
9
9
|
var __commonJS = (cb, mod) => function __require() {
|
|
10
10
|
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
11
11
|
};
|
|
12
|
+
var __export = (target, all) => {
|
|
13
|
+
for (var name in all)
|
|
14
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
15
|
+
};
|
|
12
16
|
var __copyProps = (to, from, except, desc) => {
|
|
13
17
|
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
18
|
for (let key of __getOwnPropNames(from))
|
|
@@ -25,6 +29,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
25
29
|
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
30
|
mod
|
|
27
31
|
));
|
|
32
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
28
33
|
|
|
29
34
|
// node_modules/yaml/dist/nodes/identity.js
|
|
30
35
|
var require_identity = __commonJS({
|
|
@@ -7329,8 +7334,24 @@ var require_dist = __commonJS({
|
|
|
7329
7334
|
});
|
|
7330
7335
|
|
|
7331
7336
|
// src/hooks/pre-receive.ts
|
|
7332
|
-
var
|
|
7333
|
-
|
|
7337
|
+
var pre_receive_exports = {};
|
|
7338
|
+
__export(pre_receive_exports, {
|
|
7339
|
+
parsePathRules: () => parsePathRules,
|
|
7340
|
+
verifyV4ApprovalSignatures: () => verifyV4ApprovalSignatures,
|
|
7341
|
+
verifyV4Approvals: () => verifyV4Approvals,
|
|
7342
|
+
verifyV4Checks: () => verifyV4Checks,
|
|
7343
|
+
verifyV4DiffHash: () => verifyV4DiffHash,
|
|
7344
|
+
verifyV4MergeStructure: () => verifyV4MergeStructure,
|
|
7345
|
+
verifyV4OuterSignature: () => verifyV4OuterSignature,
|
|
7346
|
+
verifyV4SignerTrust: () => verifyV4SignerTrust,
|
|
7347
|
+
verifyV4StampPathsGuard: () => verifyV4StampPathsGuard,
|
|
7348
|
+
verifyV4TargetBranch: () => verifyV4TargetBranch,
|
|
7349
|
+
verifyV4TrustAnchorSignatures: () => verifyV4TrustAnchorSignatures
|
|
7350
|
+
});
|
|
7351
|
+
module.exports = __toCommonJS(pre_receive_exports);
|
|
7352
|
+
var import_node_child_process3 = require("child_process");
|
|
7353
|
+
var import_node_url = require("url");
|
|
7354
|
+
var import_yaml5 = __toESM(require_dist(), 1);
|
|
7334
7355
|
|
|
7335
7356
|
// src/lib/attestation.ts
|
|
7336
7357
|
var MIN_ACCEPTED_PAYLOAD_VERSION = 3;
|
|
@@ -7358,6 +7379,26 @@ function parseCommitAttestation(commitMessage) {
|
|
|
7358
7379
|
return { payload, payloadBytes, signatureBase64: b64Sig };
|
|
7359
7380
|
}
|
|
7360
7381
|
|
|
7382
|
+
// src/lib/attestationV4.ts
|
|
7383
|
+
var MIN_ACCEPTED_V4_SCHEMA_VERSION = 4;
|
|
7384
|
+
var MAX_V4_ENVELOPE_BYTES = 64 * 1024;
|
|
7385
|
+
function sortKeysDeep(value) {
|
|
7386
|
+
if (value === null || typeof value !== "object") return value;
|
|
7387
|
+
if (Array.isArray(value)) return value.map(sortKeysDeep);
|
|
7388
|
+
const obj = value;
|
|
7389
|
+
const out = {};
|
|
7390
|
+
for (const key of Object.keys(obj).sort()) {
|
|
7391
|
+
out[key] = sortKeysDeep(obj[key]);
|
|
7392
|
+
}
|
|
7393
|
+
return out;
|
|
7394
|
+
}
|
|
7395
|
+
function canonicalSerializeApproval(a) {
|
|
7396
|
+
return Buffer.from(JSON.stringify(sortKeysDeep(a)), "utf8");
|
|
7397
|
+
}
|
|
7398
|
+
function canonicalSerializePayload(p) {
|
|
7399
|
+
return Buffer.from(JSON.stringify(sortKeysDeep(p)), "utf8");
|
|
7400
|
+
}
|
|
7401
|
+
|
|
7361
7402
|
// src/lib/keys.ts
|
|
7362
7403
|
var import_node_crypto = require("crypto");
|
|
7363
7404
|
var import_node_fs2 = require("fs");
|
|
@@ -7488,7 +7529,622 @@ function verifyBytes(publicKeyPem, data, signatureBase64) {
|
|
|
7488
7529
|
return (0, import_node_crypto3.verify)(null, data, key, sig);
|
|
7489
7530
|
}
|
|
7490
7531
|
|
|
7532
|
+
// src/lib/trustedKeysManifest.ts
|
|
7533
|
+
var import_node_crypto4 = require("crypto");
|
|
7534
|
+
var import_yaml4 = __toESM(require_dist(), 1);
|
|
7535
|
+
var KNOWN_CAPABILITIES = ["admin", "operator", "server"];
|
|
7536
|
+
var MAX_MANIFEST_BYTES = 256 * 1024;
|
|
7537
|
+
var MAX_MANIFEST_ENTRIES = 1e4;
|
|
7538
|
+
var FINGERPRINT_PATTERN = /^sha256:[0-9a-f]{64}$/;
|
|
7539
|
+
var NAME_PATTERN = /^[A-Za-z0-9_.-]+$/;
|
|
7540
|
+
function parseManifest(yamlText) {
|
|
7541
|
+
if (typeof yamlText !== "string") return null;
|
|
7542
|
+
if (Buffer.byteLength(yamlText, "utf8") > MAX_MANIFEST_BYTES) return null;
|
|
7543
|
+
let parsed;
|
|
7544
|
+
try {
|
|
7545
|
+
parsed = (0, import_yaml4.parse)(yamlText);
|
|
7546
|
+
} catch {
|
|
7547
|
+
return null;
|
|
7548
|
+
}
|
|
7549
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
7550
|
+
return null;
|
|
7551
|
+
}
|
|
7552
|
+
const top = parsed;
|
|
7553
|
+
const rawKeys = top.keys;
|
|
7554
|
+
if (!rawKeys || typeof rawKeys !== "object" || Array.isArray(rawKeys)) {
|
|
7555
|
+
return null;
|
|
7556
|
+
}
|
|
7557
|
+
const names = Object.keys(rawKeys);
|
|
7558
|
+
if (names.length === 0) return null;
|
|
7559
|
+
if (names.length > MAX_MANIFEST_ENTRIES) return null;
|
|
7560
|
+
const fingerprintsSeen = /* @__PURE__ */ new Set();
|
|
7561
|
+
const entries = [];
|
|
7562
|
+
for (const name of names) {
|
|
7563
|
+
if (!NAME_PATTERN.test(name)) return null;
|
|
7564
|
+
const def = rawKeys[name];
|
|
7565
|
+
if (!def || typeof def !== "object" || Array.isArray(def)) return null;
|
|
7566
|
+
const d = def;
|
|
7567
|
+
if (typeof d.fingerprint !== "string") return null;
|
|
7568
|
+
if (!FINGERPRINT_PATTERN.test(d.fingerprint)) return null;
|
|
7569
|
+
if (fingerprintsSeen.has(d.fingerprint)) return null;
|
|
7570
|
+
fingerprintsSeen.add(d.fingerprint);
|
|
7571
|
+
if (!Array.isArray(d.capabilities)) return null;
|
|
7572
|
+
if (d.capabilities.length === 0) return null;
|
|
7573
|
+
const capSet = /* @__PURE__ */ new Set();
|
|
7574
|
+
for (const cap of d.capabilities) {
|
|
7575
|
+
if (typeof cap !== "string") return null;
|
|
7576
|
+
if (!isKnownCapability(cap)) return null;
|
|
7577
|
+
capSet.add(cap);
|
|
7578
|
+
}
|
|
7579
|
+
const capabilities = [...capSet].sort();
|
|
7580
|
+
let role_source;
|
|
7581
|
+
if (d.role_source !== void 0) {
|
|
7582
|
+
if (typeof d.role_source !== "string" || d.role_source.length === 0) {
|
|
7583
|
+
return null;
|
|
7584
|
+
}
|
|
7585
|
+
role_source = d.role_source;
|
|
7586
|
+
}
|
|
7587
|
+
entries.push({
|
|
7588
|
+
name,
|
|
7589
|
+
fingerprint: d.fingerprint,
|
|
7590
|
+
capabilities,
|
|
7591
|
+
...role_source !== void 0 ? { role_source } : {}
|
|
7592
|
+
});
|
|
7593
|
+
}
|
|
7594
|
+
entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
7595
|
+
return { entries };
|
|
7596
|
+
}
|
|
7597
|
+
function isKnownCapability(s) {
|
|
7598
|
+
return KNOWN_CAPABILITIES.includes(s);
|
|
7599
|
+
}
|
|
7600
|
+
function serializeManifestCanonical(manifest) {
|
|
7601
|
+
const sortedEntries = [...manifest.entries].sort(
|
|
7602
|
+
(a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0
|
|
7603
|
+
);
|
|
7604
|
+
const keys = {};
|
|
7605
|
+
for (const e of sortedEntries) {
|
|
7606
|
+
const entry = {
|
|
7607
|
+
capabilities: [...e.capabilities].sort(),
|
|
7608
|
+
fingerprint: e.fingerprint
|
|
7609
|
+
};
|
|
7610
|
+
if (e.role_source !== void 0) {
|
|
7611
|
+
entry.role_source = e.role_source;
|
|
7612
|
+
}
|
|
7613
|
+
keys[e.name] = entry;
|
|
7614
|
+
}
|
|
7615
|
+
const canonical = { keys };
|
|
7616
|
+
return Buffer.from(JSON.stringify(canonical), "utf8");
|
|
7617
|
+
}
|
|
7618
|
+
function snapshotSha256(manifest) {
|
|
7619
|
+
const bytes = serializeManifestCanonical(manifest);
|
|
7620
|
+
const hex = (0, import_node_crypto4.createHash)("sha256").update(bytes).digest("hex");
|
|
7621
|
+
return `sha256:${hex}`;
|
|
7622
|
+
}
|
|
7623
|
+
function resolveCapability(manifest, fingerprint) {
|
|
7624
|
+
for (const entry of manifest.entries) {
|
|
7625
|
+
if (entry.fingerprint === fingerprint) {
|
|
7626
|
+
return [...entry.capabilities];
|
|
7627
|
+
}
|
|
7628
|
+
}
|
|
7629
|
+
return null;
|
|
7630
|
+
}
|
|
7631
|
+
|
|
7632
|
+
// src/lib/v4Trust.ts
|
|
7633
|
+
var import_node_child_process2 = require("child_process");
|
|
7634
|
+
var import_node_crypto6 = require("crypto");
|
|
7635
|
+
|
|
7636
|
+
// src/lib/sshReviewClient.ts
|
|
7637
|
+
var import_node_crypto5 = require("crypto");
|
|
7638
|
+
var import_node_child_process = require("child_process");
|
|
7639
|
+
function buildPubkeyMap(pubFilenames, readAtBase) {
|
|
7640
|
+
const out = /* @__PURE__ */ new Map();
|
|
7641
|
+
for (const name of pubFilenames) {
|
|
7642
|
+
if (!name.endsWith(".pub")) continue;
|
|
7643
|
+
let pem;
|
|
7644
|
+
try {
|
|
7645
|
+
pem = readAtBase(`.stamp/trusted-keys/${name}`);
|
|
7646
|
+
} catch {
|
|
7647
|
+
continue;
|
|
7648
|
+
}
|
|
7649
|
+
let fp;
|
|
7650
|
+
try {
|
|
7651
|
+
fp = fingerprintFromPem(pem);
|
|
7652
|
+
} catch {
|
|
7653
|
+
continue;
|
|
7654
|
+
}
|
|
7655
|
+
out.set(fp, pem);
|
|
7656
|
+
}
|
|
7657
|
+
return out;
|
|
7658
|
+
}
|
|
7659
|
+
|
|
7660
|
+
// src/lib/v4Trust.ts
|
|
7661
|
+
var COMMIT_PHASES_V4 = [
|
|
7662
|
+
{ name: "verifyV4MergeStructure", fn: verifyV4MergeStructure },
|
|
7663
|
+
{ name: "verifyV4TargetBranch", fn: verifyV4TargetBranch },
|
|
7664
|
+
{ name: "verifyV4SignerTrust", fn: verifyV4SignerTrust },
|
|
7665
|
+
{ name: "verifyV4OuterSignature", fn: verifyV4OuterSignature },
|
|
7666
|
+
{ name: "verifyV4Approvals", fn: verifyV4Approvals },
|
|
7667
|
+
{ name: "verifyV4DiffHash", fn: verifyV4DiffHash },
|
|
7668
|
+
{ name: "verifyV4ApprovalSignatures", fn: verifyV4ApprovalSignatures },
|
|
7669
|
+
{ name: "verifyV4Checks", fn: verifyV4Checks },
|
|
7670
|
+
// Runs before verifyV4StampPathsGuard for UX (clearer error message
|
|
7671
|
+
// on forged sigs); the guard is correct out-of-order too.
|
|
7672
|
+
{ name: "verifyV4TrustAnchorSignatures", fn: verifyV4TrustAnchorSignatures },
|
|
7673
|
+
// Independently re-verifies trust-anchor signatures — see the
|
|
7674
|
+
// ORDERING note above. Phase ordering is not security-load-bearing.
|
|
7675
|
+
{ name: "verifyV4StampPathsGuard", fn: verifyV4StampPathsGuard }
|
|
7676
|
+
];
|
|
7677
|
+
var PR_MODE_PHASES_V4 = COMMIT_PHASES_V4.filter((p) => p.name !== "verifyV4MergeStructure");
|
|
7678
|
+
function verifyV4MergeStructure(input) {
|
|
7679
|
+
const { sha, branch, payload } = input;
|
|
7680
|
+
const parents = run(["rev-list", "--parents", "-n", "1", sha]).trim().split(/\s+/).slice(1);
|
|
7681
|
+
if (parents.length !== 2) {
|
|
7682
|
+
return {
|
|
7683
|
+
ok: false,
|
|
7684
|
+
reason: `commit ${sha.slice(0, 8)} is not a merge commit (has ${parents.length} parent(s)). Every commit to '${branch}' must be a --no-ff merge.`
|
|
7685
|
+
};
|
|
7686
|
+
}
|
|
7687
|
+
const [parent0, parent1] = parents;
|
|
7688
|
+
if (parent1 !== payload.head_sha) {
|
|
7689
|
+
return {
|
|
7690
|
+
ok: false,
|
|
7691
|
+
reason: `commit ${sha.slice(0, 8)}: v4 second parent (${parent1.slice(0, 8)}) != payload.head_sha (${payload.head_sha.slice(0, 8)})`
|
|
7692
|
+
};
|
|
7693
|
+
}
|
|
7694
|
+
const mergeBase = run(["merge-base", parent0, parent1]).trim();
|
|
7695
|
+
if (mergeBase !== payload.base_sha) {
|
|
7696
|
+
return {
|
|
7697
|
+
ok: false,
|
|
7698
|
+
reason: `commit ${sha.slice(0, 8)}: v4 merge-base(${parent0.slice(0, 8)}, ${parent1.slice(0, 8)}) = ${mergeBase.slice(0, 8)} != payload.base_sha (${payload.base_sha.slice(0, 8)})`
|
|
7699
|
+
};
|
|
7700
|
+
}
|
|
7701
|
+
return { ok: true };
|
|
7702
|
+
}
|
|
7703
|
+
function verifyV4TargetBranch(input) {
|
|
7704
|
+
const { sha, branch, payload } = input;
|
|
7705
|
+
if (payload.target_branch !== branch) {
|
|
7706
|
+
return {
|
|
7707
|
+
ok: false,
|
|
7708
|
+
reason: `commit ${sha.slice(0, 8)}: v4 payload.target_branch ("${payload.target_branch}") does not match the branch being pushed ("${branch}")`
|
|
7709
|
+
};
|
|
7710
|
+
}
|
|
7711
|
+
return { ok: true };
|
|
7712
|
+
}
|
|
7713
|
+
function verifyV4SignerTrust(input) {
|
|
7714
|
+
const { sha, payload, manifest, pubkeyByFingerprint } = input;
|
|
7715
|
+
const caps = resolveCapability(manifest, payload.signer_key_id);
|
|
7716
|
+
if (caps === null) {
|
|
7717
|
+
return {
|
|
7718
|
+
ok: false,
|
|
7719
|
+
reason: `commit ${sha.slice(0, 8)}: v4 signer key ${payload.signer_key_id} is not listed in .stamp/trusted-keys/manifest.yml at base ${payload.base_sha.slice(0, 8)}`
|
|
7720
|
+
};
|
|
7721
|
+
}
|
|
7722
|
+
if (!caps.includes("admin") && !caps.includes("operator")) {
|
|
7723
|
+
return {
|
|
7724
|
+
ok: false,
|
|
7725
|
+
reason: `commit ${sha.slice(0, 8)}: v4 signer key ${payload.signer_key_id} has capabilities [${caps.join(", ")}] in the manifest at base ${payload.base_sha.slice(0, 8)} \u2014 needs 'admin' or 'operator' to sign a v4 envelope. Update the manifest entry and re-merge.`
|
|
7726
|
+
};
|
|
7727
|
+
}
|
|
7728
|
+
if (!pubkeyByFingerprint.has(payload.signer_key_id)) {
|
|
7729
|
+
return {
|
|
7730
|
+
ok: false,
|
|
7731
|
+
reason: `commit ${sha.slice(0, 8)}: v4 signer key ${payload.signer_key_id} is in the manifest but no matching .pub file exists in .stamp/trusted-keys/ at base ${payload.base_sha.slice(0, 8)}. Commit the public key alongside the manifest entry and re-merge.`
|
|
7732
|
+
};
|
|
7733
|
+
}
|
|
7734
|
+
return { ok: true };
|
|
7735
|
+
}
|
|
7736
|
+
function verifyV4OuterSignature(input) {
|
|
7737
|
+
const { sha, payload, payloadBytes, signatureBase64, pubkeyByFingerprint } = input;
|
|
7738
|
+
const pem = pubkeyByFingerprint.get(payload.signer_key_id);
|
|
7739
|
+
let sigValid = false;
|
|
7740
|
+
try {
|
|
7741
|
+
sigValid = verifyBytes(pem, payloadBytes, signatureBase64);
|
|
7742
|
+
} catch (err) {
|
|
7743
|
+
return {
|
|
7744
|
+
ok: false,
|
|
7745
|
+
reason: `commit ${sha.slice(0, 8)}: v4 outer signature verification threw \u2014 ${err instanceof Error ? err.message : String(err)}`
|
|
7746
|
+
};
|
|
7747
|
+
}
|
|
7748
|
+
if (!sigValid) {
|
|
7749
|
+
return {
|
|
7750
|
+
ok: false,
|
|
7751
|
+
reason: `commit ${sha.slice(0, 8)}: v4 outer Ed25519 signature does not verify against the operator's trusted key ${payload.signer_key_id}`
|
|
7752
|
+
};
|
|
7753
|
+
}
|
|
7754
|
+
return { ok: true };
|
|
7755
|
+
}
|
|
7756
|
+
function verifyV4Approvals(input) {
|
|
7757
|
+
const { sha, payload, rule } = input;
|
|
7758
|
+
const approvedReviewers = new Set(
|
|
7759
|
+
payload.approvals.filter((a) => a.approval.verdict === "approved").map((a) => a.approval.reviewer)
|
|
7760
|
+
);
|
|
7761
|
+
const missing = rule.required.filter((r) => !approvedReviewers.has(r));
|
|
7762
|
+
if (missing.length > 0) {
|
|
7763
|
+
return {
|
|
7764
|
+
ok: false,
|
|
7765
|
+
reason: `commit ${sha.slice(0, 8)}: v4 missing required approvals \u2014 ${missing.join(", ")}`
|
|
7766
|
+
};
|
|
7767
|
+
}
|
|
7768
|
+
return { ok: true };
|
|
7769
|
+
}
|
|
7770
|
+
function verifyV4DiffHash(input) {
|
|
7771
|
+
const { sha, payload } = input;
|
|
7772
|
+
let diffText;
|
|
7773
|
+
try {
|
|
7774
|
+
diffText = run(["diff", `${payload.base_sha}...${payload.head_sha}`]);
|
|
7775
|
+
} catch (err) {
|
|
7776
|
+
return {
|
|
7777
|
+
ok: false,
|
|
7778
|
+
reason: `commit ${sha.slice(0, 8)}: v4 unable to compute base...head diff \u2014 ${err instanceof Error ? err.message : String(err)}`
|
|
7779
|
+
};
|
|
7780
|
+
}
|
|
7781
|
+
const actualDiffSha256 = (0, import_node_crypto6.createHash)("sha256").update(Buffer.from(diffText, "utf8")).digest("hex");
|
|
7782
|
+
if (actualDiffSha256 !== payload.diff_sha256) {
|
|
7783
|
+
return {
|
|
7784
|
+
ok: false,
|
|
7785
|
+
reason: `commit ${sha.slice(0, 8)}: v4 diff_sha256 mismatch \u2014 payload claims ${payload.diff_sha256.slice(0, 12)}\u2026 but base...head hashes to ${actualDiffSha256.slice(0, 12)}\u2026. The operator signed against a different diff than what the commit actually merges.`
|
|
7786
|
+
};
|
|
7787
|
+
}
|
|
7788
|
+
for (const entry of payload.approvals) {
|
|
7789
|
+
if (entry.approval.diff_sha256 !== actualDiffSha256) {
|
|
7790
|
+
return {
|
|
7791
|
+
ok: false,
|
|
7792
|
+
reason: `commit ${sha.slice(0, 8)}: v4 approval for "${entry.approval.reviewer}" was server-signed against diff_sha256 ${entry.approval.diff_sha256.slice(0, 12)}\u2026 but base...head hashes to ${actualDiffSha256.slice(0, 12)}\u2026. The server's verdict is for a different diff.`
|
|
7793
|
+
};
|
|
7794
|
+
}
|
|
7795
|
+
}
|
|
7796
|
+
return { ok: true };
|
|
7797
|
+
}
|
|
7798
|
+
function verifyV4ApprovalSignatures(input) {
|
|
7799
|
+
const { sha, payload, manifest, pubkeyByFingerprint } = input;
|
|
7800
|
+
const manifestSnapshot = snapshotSha256(manifest);
|
|
7801
|
+
const reviewerDefs = readReviewerDefsAtRef(payload.base_sha);
|
|
7802
|
+
for (const entry of payload.approvals) {
|
|
7803
|
+
const a = entry.approval;
|
|
7804
|
+
const reviewerLabel = `"${a.reviewer}"`;
|
|
7805
|
+
if (a.base_sha !== payload.base_sha) {
|
|
7806
|
+
return {
|
|
7807
|
+
ok: false,
|
|
7808
|
+
reason: `commit ${sha.slice(0, 8)}: v4 approval ${reviewerLabel} was signed against base_sha ${a.base_sha.slice(0, 8)} but envelope's base_sha is ${payload.base_sha.slice(0, 8)}`
|
|
7809
|
+
};
|
|
7810
|
+
}
|
|
7811
|
+
if (a.head_sha !== payload.head_sha) {
|
|
7812
|
+
return {
|
|
7813
|
+
ok: false,
|
|
7814
|
+
reason: `commit ${sha.slice(0, 8)}: v4 approval ${reviewerLabel} was signed against head_sha ${a.head_sha.slice(0, 8)} but envelope's head_sha is ${payload.head_sha.slice(0, 8)}`
|
|
7815
|
+
};
|
|
7816
|
+
}
|
|
7817
|
+
if (entry.server_attestation.server_key_id !== a.server_key_id) {
|
|
7818
|
+
return {
|
|
7819
|
+
ok: false,
|
|
7820
|
+
reason: `commit ${sha.slice(0, 8)}: v4 approval ${reviewerLabel}: server_attestation.server_key_id (${entry.server_attestation.server_key_id}) does not match inner approval.server_key_id (${a.server_key_id}). The inner signed payload is authoritative; one of the two was tampered with after signing.`
|
|
7821
|
+
};
|
|
7822
|
+
}
|
|
7823
|
+
if (a.trusted_keys_snapshot_sha256 !== manifestSnapshot) {
|
|
7824
|
+
return {
|
|
7825
|
+
ok: false,
|
|
7826
|
+
reason: `commit ${sha.slice(0, 8)}: v4 approval ${reviewerLabel}: trusted_keys_snapshot_sha256 (${a.trusted_keys_snapshot_sha256.slice(0, 16)}\u2026) does not match the manifest at base ${payload.base_sha.slice(0, 8)} (${manifestSnapshot.slice(0, 16)}\u2026). The server signed against a different snapshot of the trust set than the one committed at the merge base.`
|
|
7827
|
+
};
|
|
7828
|
+
}
|
|
7829
|
+
const caps = resolveCapability(manifest, a.server_key_id);
|
|
7830
|
+
if (caps === null) {
|
|
7831
|
+
return {
|
|
7832
|
+
ok: false,
|
|
7833
|
+
reason: `commit ${sha.slice(0, 8)}: v4 approval ${reviewerLabel} was signed by ${a.server_key_id}, but that key is not in .stamp/trusted-keys/manifest.yml at base ${payload.base_sha.slice(0, 8)}`
|
|
7834
|
+
};
|
|
7835
|
+
}
|
|
7836
|
+
if (!caps.includes("server")) {
|
|
7837
|
+
return {
|
|
7838
|
+
ok: false,
|
|
7839
|
+
reason: `commit ${sha.slice(0, 8)}: v4 approval ${reviewerLabel} was signed by ${a.server_key_id}, but that key's capabilities [${caps.join(", ")}] don't include 'server' at base ${payload.base_sha.slice(0, 8)}`
|
|
7840
|
+
};
|
|
7841
|
+
}
|
|
7842
|
+
const serverPem = pubkeyByFingerprint.get(a.server_key_id);
|
|
7843
|
+
if (!serverPem) {
|
|
7844
|
+
return {
|
|
7845
|
+
ok: false,
|
|
7846
|
+
reason: `commit ${sha.slice(0, 8)}: v4 approval ${reviewerLabel}: no .pub file in .stamp/trusted-keys/ at base ${payload.base_sha.slice(0, 8)} matches fingerprint ${a.server_key_id}`
|
|
7847
|
+
};
|
|
7848
|
+
}
|
|
7849
|
+
let sigOk = false;
|
|
7850
|
+
try {
|
|
7851
|
+
sigOk = verifyBytes(
|
|
7852
|
+
serverPem,
|
|
7853
|
+
canonicalSerializeApproval(a),
|
|
7854
|
+
entry.server_attestation.signature
|
|
7855
|
+
);
|
|
7856
|
+
} catch (err) {
|
|
7857
|
+
return {
|
|
7858
|
+
ok: false,
|
|
7859
|
+
reason: `commit ${sha.slice(0, 8)}: v4 approval ${reviewerLabel}: server signature verification threw \u2014 ${err instanceof Error ? err.message : String(err)}`
|
|
7860
|
+
};
|
|
7861
|
+
}
|
|
7862
|
+
if (!sigOk) {
|
|
7863
|
+
return {
|
|
7864
|
+
ok: false,
|
|
7865
|
+
reason: `commit ${sha.slice(0, 8)}: v4 approval ${reviewerLabel}: server signature does not verify against ${a.server_key_id} over canonical approval bytes`
|
|
7866
|
+
};
|
|
7867
|
+
}
|
|
7868
|
+
const def = reviewerDefs[a.reviewer];
|
|
7869
|
+
if (!def) {
|
|
7870
|
+
return {
|
|
7871
|
+
ok: false,
|
|
7872
|
+
reason: `commit ${sha.slice(0, 8)}: v4 approval ${reviewerLabel}: reviewer is not defined in .stamp/config.yml at base ${payload.base_sha.slice(0, 8)}`
|
|
7873
|
+
};
|
|
7874
|
+
}
|
|
7875
|
+
let promptText;
|
|
7876
|
+
try {
|
|
7877
|
+
promptText = run(["show", `${payload.base_sha}:${def.prompt}`]);
|
|
7878
|
+
} catch {
|
|
7879
|
+
return {
|
|
7880
|
+
ok: false,
|
|
7881
|
+
reason: `commit ${sha.slice(0, 8)}: v4 approval ${reviewerLabel}: prompt "${def.prompt}" is unreadable at base ${payload.base_sha.slice(0, 8)}`
|
|
7882
|
+
};
|
|
7883
|
+
}
|
|
7884
|
+
const recomputedPromptSha = hashPromptBytes(Buffer.from(promptText, "utf8"));
|
|
7885
|
+
if (recomputedPromptSha !== a.prompt_sha256) {
|
|
7886
|
+
return {
|
|
7887
|
+
ok: false,
|
|
7888
|
+
reason: `commit ${sha.slice(0, 8)}: v4 approval ${reviewerLabel}: prompt_sha256 mismatch \u2014 server signed ${a.prompt_sha256.slice(0, 12)}\u2026 but prompt file at base hashes to ${recomputedPromptSha.slice(0, 12)}\u2026. The reviewer prompt the server reviewed differs from the one in the merge-base tree.`
|
|
7889
|
+
};
|
|
7890
|
+
}
|
|
7891
|
+
}
|
|
7892
|
+
return { ok: true };
|
|
7893
|
+
}
|
|
7894
|
+
function verifyV4Checks(input) {
|
|
7895
|
+
const { sha, payload, rule } = input;
|
|
7896
|
+
const requiredChecks = rule.required_checks ?? [];
|
|
7897
|
+
const attestedByName = new Map(payload.checks.map((c) => [c.name, c]));
|
|
7898
|
+
const missingChecks = [];
|
|
7899
|
+
const failingChecks = [];
|
|
7900
|
+
for (const req of requiredChecks) {
|
|
7901
|
+
const attested = attestedByName.get(req.name);
|
|
7902
|
+
if (!attested) {
|
|
7903
|
+
missingChecks.push(req.name);
|
|
7904
|
+
continue;
|
|
7905
|
+
}
|
|
7906
|
+
if (attested.exit_code !== 0) {
|
|
7907
|
+
failingChecks.push(`${req.name} (exit ${attested.exit_code})`);
|
|
7908
|
+
}
|
|
7909
|
+
}
|
|
7910
|
+
if (missingChecks.length > 0) {
|
|
7911
|
+
return {
|
|
7912
|
+
ok: false,
|
|
7913
|
+
reason: `commit ${sha.slice(0, 8)}: v4 attestation is missing required check(s) \u2014 ${missingChecks.join(", ")}`
|
|
7914
|
+
};
|
|
7915
|
+
}
|
|
7916
|
+
if (failingChecks.length > 0) {
|
|
7917
|
+
return {
|
|
7918
|
+
ok: false,
|
|
7919
|
+
reason: `commit ${sha.slice(0, 8)}: v4 attestation records failing check(s) \u2014 ${failingChecks.join(", ")}`
|
|
7920
|
+
};
|
|
7921
|
+
}
|
|
7922
|
+
return { ok: true };
|
|
7923
|
+
}
|
|
7924
|
+
function verifyV4TrustAnchorSignatures(input) {
|
|
7925
|
+
const { sha, payload, manifest, pubkeyByFingerprint } = input;
|
|
7926
|
+
if (payload.trust_anchor_signatures.length === 0) return { ok: true };
|
|
7927
|
+
const payloadForAdmins = {
|
|
7928
|
+
...payload,
|
|
7929
|
+
trust_anchor_signatures: []
|
|
7930
|
+
};
|
|
7931
|
+
const adminSigningBytes = canonicalSerializePayload(payloadForAdmins);
|
|
7932
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7933
|
+
for (const ts of payload.trust_anchor_signatures) {
|
|
7934
|
+
if (seen.has(ts.signer_key_id)) {
|
|
7935
|
+
return {
|
|
7936
|
+
ok: false,
|
|
7937
|
+
reason: `commit ${sha.slice(0, 8)}: v4 trust_anchor_signatures contains a duplicate entry for ${ts.signer_key_id}`
|
|
7938
|
+
};
|
|
7939
|
+
}
|
|
7940
|
+
seen.add(ts.signer_key_id);
|
|
7941
|
+
const caps = resolveCapability(manifest, ts.signer_key_id);
|
|
7942
|
+
if (caps === null) {
|
|
7943
|
+
return {
|
|
7944
|
+
ok: false,
|
|
7945
|
+
reason: `commit ${sha.slice(0, 8)}: v4 trust_anchor_signatures includes ${ts.signer_key_id}, which is not in the manifest at base ${payload.base_sha.slice(0, 8)}`
|
|
7946
|
+
};
|
|
7947
|
+
}
|
|
7948
|
+
if (!caps.includes("admin")) {
|
|
7949
|
+
return {
|
|
7950
|
+
ok: false,
|
|
7951
|
+
reason: `commit ${sha.slice(0, 8)}: v4 trust_anchor_signatures includes ${ts.signer_key_id} with capabilities [${caps.join(", ")}] \u2014 needs 'admin' to counter-sign at base ${payload.base_sha.slice(0, 8)}`
|
|
7952
|
+
};
|
|
7953
|
+
}
|
|
7954
|
+
const pem = pubkeyByFingerprint.get(ts.signer_key_id);
|
|
7955
|
+
if (!pem) {
|
|
7956
|
+
return {
|
|
7957
|
+
ok: false,
|
|
7958
|
+
reason: `commit ${sha.slice(0, 8)}: v4 trust_anchor_signatures includes ${ts.signer_key_id} but no matching .pub file is committed at base ${payload.base_sha.slice(0, 8)}`
|
|
7959
|
+
};
|
|
7960
|
+
}
|
|
7961
|
+
let ok = false;
|
|
7962
|
+
try {
|
|
7963
|
+
ok = verifyBytes(pem, adminSigningBytes, ts.signature);
|
|
7964
|
+
} catch (err) {
|
|
7965
|
+
return {
|
|
7966
|
+
ok: false,
|
|
7967
|
+
reason: `commit ${sha.slice(0, 8)}: v4 trust-anchor signature by ${ts.signer_key_id} threw on verify \u2014 ${err instanceof Error ? err.message : String(err)}`
|
|
7968
|
+
};
|
|
7969
|
+
}
|
|
7970
|
+
if (!ok) {
|
|
7971
|
+
return {
|
|
7972
|
+
ok: false,
|
|
7973
|
+
reason: `commit ${sha.slice(0, 8)}: v4 trust-anchor signature by ${ts.signer_key_id} does not verify`
|
|
7974
|
+
};
|
|
7975
|
+
}
|
|
7976
|
+
}
|
|
7977
|
+
return { ok: true };
|
|
7978
|
+
}
|
|
7979
|
+
function verifyV4StampPathsGuard(input) {
|
|
7980
|
+
const { sha, payload, manifest, pubkeyByFingerprint, pathRules, changedFiles } = input;
|
|
7981
|
+
if (pathRules.length === 0) return { ok: true };
|
|
7982
|
+
const payloadForAdmins = {
|
|
7983
|
+
...payload,
|
|
7984
|
+
trust_anchor_signatures: []
|
|
7985
|
+
};
|
|
7986
|
+
const adminSigningBytes = canonicalSerializePayload(payloadForAdmins);
|
|
7987
|
+
const validSigners = /* @__PURE__ */ new Set();
|
|
7988
|
+
for (const ts of payload.trust_anchor_signatures) {
|
|
7989
|
+
if (validSigners.has(ts.signer_key_id)) continue;
|
|
7990
|
+
const pem = pubkeyByFingerprint.get(ts.signer_key_id);
|
|
7991
|
+
if (!pem) continue;
|
|
7992
|
+
let ok = false;
|
|
7993
|
+
try {
|
|
7994
|
+
ok = verifyBytes(pem, adminSigningBytes, ts.signature);
|
|
7995
|
+
} catch {
|
|
7996
|
+
ok = false;
|
|
7997
|
+
}
|
|
7998
|
+
if (ok) validSigners.add(ts.signer_key_id);
|
|
7999
|
+
}
|
|
8000
|
+
for (const rule of pathRules) {
|
|
8001
|
+
const matched = changedFiles.filter((f) => pathMatchesAny(f, [rule.pattern]));
|
|
8002
|
+
if (matched.length === 0) continue;
|
|
8003
|
+
let qualifying = 0;
|
|
8004
|
+
for (const keyId of validSigners) {
|
|
8005
|
+
const caps = resolveCapability(manifest, keyId);
|
|
8006
|
+
if (caps !== null && caps.includes(rule.require_capability)) {
|
|
8007
|
+
qualifying++;
|
|
8008
|
+
}
|
|
8009
|
+
}
|
|
8010
|
+
if (qualifying < rule.minimum_signatures) {
|
|
8011
|
+
const sample = matched.slice(0, 3).join(", ");
|
|
8012
|
+
const moreSuffix = matched.length > 3 ? `, +${matched.length - 3} more` : "";
|
|
8013
|
+
return {
|
|
8014
|
+
ok: false,
|
|
8015
|
+
reason: `commit ${sha.slice(0, 8)}: v4 path_rules gate for pattern "${rule.pattern}" requires ${rule.minimum_signatures} signature(s) from keys with capability '${rule.require_capability}' (diff touches ${matched.length} matched path(s): ${sample}${moreSuffix}), but only ${qualifying} qualifying trust_anchor_signature(s) are present at base ${payload.base_sha.slice(0, 8)}. Re-run the merge after collecting the required admin counter-signatures.`
|
|
8016
|
+
};
|
|
8017
|
+
}
|
|
8018
|
+
if (!rule.bypass_review_cycle && payload.approvals.length === 0) {
|
|
8019
|
+
return {
|
|
8020
|
+
ok: false,
|
|
8021
|
+
reason: `commit ${sha.slice(0, 8)}: v4 path_rules gate for pattern "${rule.pattern}" has bypass_review_cycle=false (admin signatures + reviewer cycle required), but the envelope carries no approvals \u2014 the reviewer cycle did not run for this merge.`
|
|
8022
|
+
};
|
|
8023
|
+
}
|
|
8024
|
+
}
|
|
8025
|
+
return { ok: true };
|
|
8026
|
+
}
|
|
8027
|
+
function run(args) {
|
|
8028
|
+
try {
|
|
8029
|
+
return (0, import_node_child_process2.execFileSync)("git", args, {
|
|
8030
|
+
encoding: "utf8",
|
|
8031
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
8032
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
8033
|
+
});
|
|
8034
|
+
} catch (err) {
|
|
8035
|
+
throw new Error(
|
|
8036
|
+
`git ${args.join(" ")} failed: ${err instanceof Error ? err.message : String(err)}`
|
|
8037
|
+
);
|
|
8038
|
+
}
|
|
8039
|
+
}
|
|
8040
|
+
function readPubkeyMapAt(ref) {
|
|
8041
|
+
let lsOut;
|
|
8042
|
+
try {
|
|
8043
|
+
lsOut = run(["ls-tree", "--name-only", ref, ".stamp/trusted-keys/"]);
|
|
8044
|
+
} catch {
|
|
8045
|
+
return /* @__PURE__ */ new Map();
|
|
8046
|
+
}
|
|
8047
|
+
const names = [];
|
|
8048
|
+
for (const line of lsOut.split("\n")) {
|
|
8049
|
+
if (!line) continue;
|
|
8050
|
+
const prefix = ".stamp/trusted-keys/";
|
|
8051
|
+
const basename = line.startsWith(prefix) ? line.slice(prefix.length) : line;
|
|
8052
|
+
if (basename.endsWith(".pub")) names.push(basename);
|
|
8053
|
+
}
|
|
8054
|
+
return buildPubkeyMap(names, (relPath) => run(["show", `${ref}:${relPath}`]));
|
|
8055
|
+
}
|
|
8056
|
+
function readReviewerDefsAtRef(ref) {
|
|
8057
|
+
let yaml;
|
|
8058
|
+
try {
|
|
8059
|
+
yaml = run(["show", `${ref}:.stamp/config.yml`]);
|
|
8060
|
+
} catch {
|
|
8061
|
+
return {};
|
|
8062
|
+
}
|
|
8063
|
+
const defs = readReviewersFromYaml(yaml);
|
|
8064
|
+
const out = {};
|
|
8065
|
+
for (const [name, def] of Object.entries(defs)) {
|
|
8066
|
+
if (def && typeof def.prompt === "string") {
|
|
8067
|
+
out[name] = { prompt: def.prompt };
|
|
8068
|
+
}
|
|
8069
|
+
}
|
|
8070
|
+
return out;
|
|
8071
|
+
}
|
|
8072
|
+
function readChangedFilesAtRef(baseSha, headSha) {
|
|
8073
|
+
let out;
|
|
8074
|
+
try {
|
|
8075
|
+
out = run(["diff", "-z", "--name-only", `${baseSha}...${headSha}`]);
|
|
8076
|
+
} catch {
|
|
8077
|
+
return null;
|
|
8078
|
+
}
|
|
8079
|
+
return out.split("\0").filter((l) => l.length > 0);
|
|
8080
|
+
}
|
|
8081
|
+
function pathGlobToRegex(pattern) {
|
|
8082
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
8083
|
+
const DOUBLE = "\0DOUBLESTAR\0";
|
|
8084
|
+
const translated = escaped.replace(/\*\*/g, DOUBLE).replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]").split(DOUBLE).join(".*");
|
|
8085
|
+
return new RegExp(`^${translated}$`);
|
|
8086
|
+
}
|
|
8087
|
+
function pathMatchesAny(filePath, patterns) {
|
|
8088
|
+
for (const p of patterns) {
|
|
8089
|
+
if (pathGlobToRegex(p).test(filePath)) return true;
|
|
8090
|
+
}
|
|
8091
|
+
return false;
|
|
8092
|
+
}
|
|
8093
|
+
function parsePathRules(raw) {
|
|
8094
|
+
if (raw === void 0 || raw === null) return { rules: [], warnings: [] };
|
|
8095
|
+
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
8096
|
+
return {
|
|
8097
|
+
rules: [],
|
|
8098
|
+
warnings: [
|
|
8099
|
+
`path_rules: top-level value must be a YAML map (e.g. \`".stamp/**": { ... }\`). Got ${Array.isArray(raw) ? "an array" : typeof raw}; entire path_rules section ignored.`
|
|
8100
|
+
]
|
|
8101
|
+
};
|
|
8102
|
+
}
|
|
8103
|
+
const out = [];
|
|
8104
|
+
const warnings = [];
|
|
8105
|
+
for (const [pattern, rule] of Object.entries(raw)) {
|
|
8106
|
+
if (typeof pattern !== "string" || pattern.length === 0) {
|
|
8107
|
+
warnings.push(`path_rules: empty or non-string pattern key skipped.`);
|
|
8108
|
+
continue;
|
|
8109
|
+
}
|
|
8110
|
+
if (!rule || typeof rule !== "object" || Array.isArray(rule)) {
|
|
8111
|
+
warnings.push(
|
|
8112
|
+
`path_rules["${pattern}"]: rule body must be a YAML map with require_capability/minimum_signatures/bypass_review_cycle fields. Rule ignored \u2014 the path is NOT gated.`
|
|
8113
|
+
);
|
|
8114
|
+
continue;
|
|
8115
|
+
}
|
|
8116
|
+
const r = rule;
|
|
8117
|
+
if (typeof r.require_capability !== "string" || r.require_capability.length === 0) {
|
|
8118
|
+
warnings.push(
|
|
8119
|
+
`path_rules["${pattern}"]: require_capability must be a non-empty string (e.g. \`admin\`). Got ${typeof r.require_capability}; rule ignored \u2014 the path is NOT gated.`
|
|
8120
|
+
);
|
|
8121
|
+
continue;
|
|
8122
|
+
}
|
|
8123
|
+
if (typeof r.minimum_signatures !== "number" || !Number.isInteger(r.minimum_signatures) || r.minimum_signatures < 1) {
|
|
8124
|
+
warnings.push(
|
|
8125
|
+
`path_rules["${pattern}"]: minimum_signatures must be a positive integer (got ${JSON.stringify(r.minimum_signatures)}). Rule ignored \u2014 the path is NOT gated.`
|
|
8126
|
+
);
|
|
8127
|
+
continue;
|
|
8128
|
+
}
|
|
8129
|
+
if (typeof r.bypass_review_cycle !== "boolean") {
|
|
8130
|
+
warnings.push(
|
|
8131
|
+
`path_rules["${pattern}"]: bypass_review_cycle must be a YAML boolean (true or false; YAML's \`yes\`/\`no\`/\`on\`/\`off\` are NOT parsed as booleans here). Got ${JSON.stringify(r.bypass_review_cycle)}; rule ignored \u2014 the path is NOT gated.`
|
|
8132
|
+
);
|
|
8133
|
+
continue;
|
|
8134
|
+
}
|
|
8135
|
+
out.push({
|
|
8136
|
+
pattern,
|
|
8137
|
+
require_capability: r.require_capability,
|
|
8138
|
+
minimum_signatures: r.minimum_signatures,
|
|
8139
|
+
bypass_review_cycle: r.bypass_review_cycle
|
|
8140
|
+
});
|
|
8141
|
+
}
|
|
8142
|
+
out.sort((a, b) => a.pattern < b.pattern ? -1 : a.pattern > b.pattern ? 1 : 0);
|
|
8143
|
+
return { rules: out, warnings };
|
|
8144
|
+
}
|
|
8145
|
+
|
|
7491
8146
|
// src/hooks/pre-receive.ts
|
|
8147
|
+
var import_meta = {};
|
|
7492
8148
|
var ZERO_SHA = "0000000000000000000000000000000000000000";
|
|
7493
8149
|
function main() {
|
|
7494
8150
|
const stdin = readAllStdin();
|
|
@@ -7550,7 +8206,7 @@ function verifyRef(oldSha, newSha, refname) {
|
|
|
7550
8206
|
function verifyTagPush(newSha, refname) {
|
|
7551
8207
|
let pointedCommit;
|
|
7552
8208
|
try {
|
|
7553
|
-
pointedCommit =
|
|
8209
|
+
pointedCommit = run2(["rev-parse", `${newSha}^{commit}`]).trim();
|
|
7554
8210
|
} catch (err) {
|
|
7555
8211
|
reject(
|
|
7556
8212
|
refname,
|
|
@@ -7559,7 +8215,7 @@ function verifyTagPush(newSha, refname) {
|
|
|
7559
8215
|
}
|
|
7560
8216
|
let headRef;
|
|
7561
8217
|
try {
|
|
7562
|
-
headRef =
|
|
8218
|
+
headRef = run2(["symbolic-ref", "HEAD"]).trim();
|
|
7563
8219
|
} catch {
|
|
7564
8220
|
reject(
|
|
7565
8221
|
refname,
|
|
@@ -7568,7 +8224,7 @@ function verifyTagPush(newSha, refname) {
|
|
|
7568
8224
|
}
|
|
7569
8225
|
let defaultBranchTip;
|
|
7570
8226
|
try {
|
|
7571
|
-
defaultBranchTip =
|
|
8227
|
+
defaultBranchTip = run2(["rev-parse", headRef]).trim();
|
|
7572
8228
|
} catch {
|
|
7573
8229
|
reject(
|
|
7574
8230
|
refname,
|
|
@@ -7582,7 +8238,7 @@ function verifyTagPush(newSha, refname) {
|
|
|
7582
8238
|
`no readable .stamp/config.yml at ${defaultBranchTip.slice(0, 8)}; tag pushes require a bootstrapped repo`
|
|
7583
8239
|
);
|
|
7584
8240
|
}
|
|
7585
|
-
const branchListing =
|
|
8241
|
+
const branchListing = run2([
|
|
7586
8242
|
"for-each-ref",
|
|
7587
8243
|
"--format=%(refname:short)",
|
|
7588
8244
|
"refs/heads/"
|
|
@@ -7599,7 +8255,7 @@ function verifyTagPush(newSha, refname) {
|
|
|
7599
8255
|
);
|
|
7600
8256
|
}
|
|
7601
8257
|
for (const b of protectedBranches) {
|
|
7602
|
-
const tip =
|
|
8258
|
+
const tip = run2(["rev-parse", `refs/heads/${b}`]).trim();
|
|
7603
8259
|
if (isAncestor(pointedCommit, tip)) {
|
|
7604
8260
|
return;
|
|
7605
8261
|
}
|
|
@@ -7609,8 +8265,18 @@ function verifyTagPush(newSha, refname) {
|
|
|
7609
8265
|
`tag points at commit ${pointedCommit.slice(0, 8)} which is not reachable from any protected branch (${protectedBranches.join(", ")}). Tags can only point at commits that have already passed branch verification \u2014 merge to a protected branch first via the stamp flow, then create the tag from that commit.`
|
|
7610
8266
|
);
|
|
7611
8267
|
}
|
|
8268
|
+
var COMMIT_PHASES = [
|
|
8269
|
+
{ name: "verifyMergeStructure", fn: verifyMergeStructure },
|
|
8270
|
+
{ name: "verifyTargetBranch", fn: verifyTargetBranch },
|
|
8271
|
+
{ name: "verifySignerTrust", fn: verifySignerTrust },
|
|
8272
|
+
{ name: "verifyTrailerSignature", fn: verifyTrailerSignature },
|
|
8273
|
+
{ name: "verifyApprovals", fn: verifyApprovals },
|
|
8274
|
+
{ name: "verifyChecks", fn: verifyChecks },
|
|
8275
|
+
{ name: "verifySchemaVersion", fn: verifySchemaVersion },
|
|
8276
|
+
{ name: "verifyReviewerHashesAtMergeBase", fn: verifyReviewerHashesAtMergeBase }
|
|
8277
|
+
];
|
|
7612
8278
|
function verifyCommit(sha, branch, rule, trustedKeys, refname) {
|
|
7613
|
-
const commitMessage =
|
|
8279
|
+
const commitMessage = run2(["cat-file", "-p", sha]).split(/\n\n/s).slice(1).join("\n\n");
|
|
7614
8280
|
const parsed = parseCommitAttestation(commitMessage);
|
|
7615
8281
|
if (!parsed) {
|
|
7616
8282
|
reject(
|
|
@@ -7618,66 +8284,106 @@ function verifyCommit(sha, branch, rule, trustedKeys, refname) {
|
|
|
7618
8284
|
`commit ${sha.slice(0, 8)} has no Stamp-Payload / Stamp-Verified trailers. Every commit to '${branch}' must be a stamped merge.`
|
|
7619
8285
|
);
|
|
7620
8286
|
}
|
|
7621
|
-
const
|
|
7622
|
-
|
|
8287
|
+
const rawSchemaVersion = parsed.payload.schema_version;
|
|
8288
|
+
if (typeof rawSchemaVersion === "number" && rawSchemaVersion >= MIN_ACCEPTED_V4_SCHEMA_VERSION) {
|
|
8289
|
+
verifyCommitV4(sha, branch, rule, parsed.payloadBytes, parsed.signatureBase64, refname);
|
|
8290
|
+
return;
|
|
8291
|
+
}
|
|
8292
|
+
const input = {
|
|
8293
|
+
sha,
|
|
8294
|
+
branch,
|
|
8295
|
+
rule,
|
|
8296
|
+
trustedKeys,
|
|
8297
|
+
payload: parsed.payload,
|
|
8298
|
+
payloadBytes: parsed.payloadBytes,
|
|
8299
|
+
signatureBase64: parsed.signatureBase64
|
|
8300
|
+
};
|
|
8301
|
+
for (const phase of COMMIT_PHASES) {
|
|
8302
|
+
const result = phase.fn(input);
|
|
8303
|
+
if (!result.ok) reject(refname, result.reason);
|
|
8304
|
+
}
|
|
8305
|
+
}
|
|
8306
|
+
function verifyMergeStructure(input) {
|
|
8307
|
+
const { sha, branch, payload } = input;
|
|
8308
|
+
const parents = run2(["rev-list", "--parents", "-n", "1", sha]).trim().split(/\s+/).slice(1);
|
|
7623
8309
|
if (parents.length !== 2) {
|
|
7624
|
-
|
|
7625
|
-
|
|
7626
|
-
`commit ${sha.slice(0, 8)} is not a merge commit (has ${parents.length} parent(s)). Every commit to '${branch}' must be a --no-ff merge.`
|
|
7627
|
-
|
|
8310
|
+
return {
|
|
8311
|
+
ok: false,
|
|
8312
|
+
reason: `commit ${sha.slice(0, 8)} is not a merge commit (has ${parents.length} parent(s)). Every commit to '${branch}' must be a --no-ff merge.`
|
|
8313
|
+
};
|
|
7628
8314
|
}
|
|
7629
8315
|
const [parent0, parent1] = parents;
|
|
7630
8316
|
if (parent1 !== payload.head_sha) {
|
|
7631
|
-
|
|
7632
|
-
|
|
7633
|
-
`commit ${sha.slice(0, 8)}: second parent (${parent1.slice(0, 8)}) != payload.head_sha (${payload.head_sha.slice(0, 8)})`
|
|
7634
|
-
|
|
8317
|
+
return {
|
|
8318
|
+
ok: false,
|
|
8319
|
+
reason: `commit ${sha.slice(0, 8)}: second parent (${parent1.slice(0, 8)}) != payload.head_sha (${payload.head_sha.slice(0, 8)})`
|
|
8320
|
+
};
|
|
7635
8321
|
}
|
|
7636
|
-
const mergeBase =
|
|
8322
|
+
const mergeBase = run2(["merge-base", parent0, parent1]).trim();
|
|
7637
8323
|
if (mergeBase !== payload.base_sha) {
|
|
7638
|
-
|
|
7639
|
-
|
|
7640
|
-
`commit ${sha.slice(0, 8)}: merge-base(${parent0.slice(0, 8)}, ${parent1.slice(0, 8)}) = ${mergeBase.slice(0, 8)} != payload.base_sha (${payload.base_sha.slice(0, 8)})`
|
|
7641
|
-
|
|
8324
|
+
return {
|
|
8325
|
+
ok: false,
|
|
8326
|
+
reason: `commit ${sha.slice(0, 8)}: merge-base(${parent0.slice(0, 8)}, ${parent1.slice(0, 8)}) = ${mergeBase.slice(0, 8)} != payload.base_sha (${payload.base_sha.slice(0, 8)})`
|
|
8327
|
+
};
|
|
7642
8328
|
}
|
|
8329
|
+
return { ok: true };
|
|
8330
|
+
}
|
|
8331
|
+
function verifyTargetBranch(input) {
|
|
8332
|
+
const { sha, branch, payload } = input;
|
|
7643
8333
|
if (payload.target_branch !== branch) {
|
|
7644
|
-
|
|
7645
|
-
|
|
7646
|
-
`commit ${sha.slice(0, 8)}: payload.target_branch ("${payload.target_branch}") does not match the branch being pushed ("${branch}")`
|
|
7647
|
-
|
|
8334
|
+
return {
|
|
8335
|
+
ok: false,
|
|
8336
|
+
reason: `commit ${sha.slice(0, 8)}: payload.target_branch ("${payload.target_branch}") does not match the branch being pushed ("${branch}")`
|
|
8337
|
+
};
|
|
7648
8338
|
}
|
|
7649
|
-
|
|
7650
|
-
|
|
7651
|
-
|
|
7652
|
-
|
|
7653
|
-
|
|
7654
|
-
|
|
8339
|
+
return { ok: true };
|
|
8340
|
+
}
|
|
8341
|
+
function verifySignerTrust(input) {
|
|
8342
|
+
const { sha, payload, trustedKeys } = input;
|
|
8343
|
+
if (!trustedKeys.has(payload.signer_key_id)) {
|
|
8344
|
+
return {
|
|
8345
|
+
ok: false,
|
|
8346
|
+
reason: `commit ${sha.slice(0, 8)}: signer key ${payload.signer_key_id} is not in .stamp/trusted-keys/`
|
|
8347
|
+
};
|
|
7655
8348
|
}
|
|
8349
|
+
return { ok: true };
|
|
8350
|
+
}
|
|
8351
|
+
function verifyTrailerSignature(input) {
|
|
8352
|
+
const { sha, payload, payloadBytes, signatureBase64, trustedKeys } = input;
|
|
8353
|
+
const trustedPem = trustedKeys.get(payload.signer_key_id);
|
|
7656
8354
|
let sigValid = false;
|
|
7657
8355
|
try {
|
|
7658
8356
|
sigValid = verifyBytes(trustedPem, payloadBytes, signatureBase64);
|
|
7659
8357
|
} catch (err) {
|
|
7660
|
-
|
|
7661
|
-
|
|
7662
|
-
`commit ${sha.slice(0, 8)}: signature verification threw \u2014 ${err instanceof Error ? err.message : String(err)}`
|
|
7663
|
-
|
|
8358
|
+
return {
|
|
8359
|
+
ok: false,
|
|
8360
|
+
reason: `commit ${sha.slice(0, 8)}: signature verification threw \u2014 ${err instanceof Error ? err.message : String(err)}`
|
|
8361
|
+
};
|
|
7664
8362
|
}
|
|
7665
8363
|
if (!sigValid) {
|
|
7666
|
-
|
|
7667
|
-
|
|
7668
|
-
`commit ${sha.slice(0, 8)}: Ed25519 signature does not verify against the signer's trusted key`
|
|
7669
|
-
|
|
8364
|
+
return {
|
|
8365
|
+
ok: false,
|
|
8366
|
+
reason: `commit ${sha.slice(0, 8)}: Ed25519 signature does not verify against the signer's trusted key`
|
|
8367
|
+
};
|
|
7670
8368
|
}
|
|
8369
|
+
return { ok: true };
|
|
8370
|
+
}
|
|
8371
|
+
function verifyApprovals(input) {
|
|
8372
|
+
const { sha, payload, rule } = input;
|
|
7671
8373
|
const approvedReviewers = new Set(
|
|
7672
8374
|
payload.approvals.filter((a) => a.verdict === "approved").map((a) => a.reviewer)
|
|
7673
8375
|
);
|
|
7674
8376
|
const missing = rule.required.filter((r) => !approvedReviewers.has(r));
|
|
7675
8377
|
if (missing.length > 0) {
|
|
7676
|
-
|
|
7677
|
-
|
|
7678
|
-
`commit ${sha.slice(0, 8)}: missing required approvals \u2014 ${missing.join(", ")}`
|
|
7679
|
-
|
|
8378
|
+
return {
|
|
8379
|
+
ok: false,
|
|
8380
|
+
reason: `commit ${sha.slice(0, 8)}: missing required approvals \u2014 ${missing.join(", ")}`
|
|
8381
|
+
};
|
|
7680
8382
|
}
|
|
8383
|
+
return { ok: true };
|
|
8384
|
+
}
|
|
8385
|
+
function verifyChecks(input) {
|
|
8386
|
+
const { sha, payload, rule } = input;
|
|
7681
8387
|
const requiredChecks = rule.required_checks ?? [];
|
|
7682
8388
|
const attestedByName = new Map(
|
|
7683
8389
|
(payload.checks ?? []).map((c) => [c.name, c])
|
|
@@ -7695,36 +8401,42 @@ function verifyCommit(sha, branch, rule, trustedKeys, refname) {
|
|
|
7695
8401
|
}
|
|
7696
8402
|
}
|
|
7697
8403
|
if (missingChecks.length > 0) {
|
|
7698
|
-
|
|
7699
|
-
|
|
7700
|
-
`commit ${sha.slice(0, 8)}: attestation is missing required check(s) \u2014 ${missingChecks.join(", ")}`
|
|
7701
|
-
|
|
8404
|
+
return {
|
|
8405
|
+
ok: false,
|
|
8406
|
+
reason: `commit ${sha.slice(0, 8)}: attestation is missing required check(s) \u2014 ${missingChecks.join(", ")}`
|
|
8407
|
+
};
|
|
7702
8408
|
}
|
|
7703
8409
|
if (failingChecks.length > 0) {
|
|
7704
|
-
|
|
7705
|
-
|
|
7706
|
-
`commit ${sha.slice(0, 8)}: attestation records failing check(s) \u2014 ${failingChecks.join(", ")}`
|
|
7707
|
-
|
|
8410
|
+
return {
|
|
8411
|
+
ok: false,
|
|
8412
|
+
reason: `commit ${sha.slice(0, 8)}: attestation records failing check(s) \u2014 ${failingChecks.join(", ")}`
|
|
8413
|
+
};
|
|
7708
8414
|
}
|
|
8415
|
+
return { ok: true };
|
|
8416
|
+
}
|
|
8417
|
+
function verifySchemaVersion(input) {
|
|
8418
|
+
const { sha, payload } = input;
|
|
7709
8419
|
const version = payload.schema_version ?? 1;
|
|
7710
8420
|
if (version < MIN_ACCEPTED_PAYLOAD_VERSION) {
|
|
7711
|
-
|
|
7712
|
-
|
|
7713
|
-
`commit ${sha.slice(0, 8)}: attestation schema_version ${version} is no longer accepted (minimum supported is ${MIN_ACCEPTED_PAYLOAD_VERSION} \u2014 earlier versions are known-broken under the feature-branch self-review attack). Re-create the merge with a current stamp-cli build which produces v${MIN_ACCEPTED_PAYLOAD_VERSION} attestations bound to the merge-base tree.`
|
|
7714
|
-
|
|
8421
|
+
return {
|
|
8422
|
+
ok: false,
|
|
8423
|
+
reason: `commit ${sha.slice(0, 8)}: attestation schema_version ${version} is no longer accepted (minimum supported is ${MIN_ACCEPTED_PAYLOAD_VERSION} \u2014 earlier versions are known-broken under the feature-branch self-review attack). Re-create the merge with a current stamp-cli build which produces v${MIN_ACCEPTED_PAYLOAD_VERSION} attestations bound to the merge-base tree.`
|
|
8424
|
+
};
|
|
7715
8425
|
}
|
|
7716
|
-
|
|
8426
|
+
return { ok: true };
|
|
7717
8427
|
}
|
|
7718
|
-
function verifyReviewerHashesAtMergeBase(
|
|
8428
|
+
function verifyReviewerHashesAtMergeBase(input) {
|
|
8429
|
+
const { sha, payload } = input;
|
|
8430
|
+
const baseSha = payload.base_sha;
|
|
7719
8431
|
const prefix = `commit ${sha.slice(0, 8)}: v3 attestation:`;
|
|
7720
8432
|
let configYaml;
|
|
7721
8433
|
try {
|
|
7722
|
-
configYaml =
|
|
8434
|
+
configYaml = run2(["show", `${baseSha}:.stamp/config.yml`]);
|
|
7723
8435
|
} catch {
|
|
7724
|
-
|
|
7725
|
-
|
|
7726
|
-
`${prefix} .stamp/config.yml unreadable at merge-base ${baseSha.slice(0, 8)}`
|
|
7727
|
-
|
|
8436
|
+
return {
|
|
8437
|
+
ok: false,
|
|
8438
|
+
reason: `${prefix} .stamp/config.yml unreadable at merge-base ${baseSha.slice(0, 8)}`
|
|
8439
|
+
};
|
|
7728
8440
|
}
|
|
7729
8441
|
const reviewers = readReviewersFromYaml(configYaml);
|
|
7730
8442
|
for (const approval of payload.approvals) {
|
|
@@ -7733,42 +8445,111 @@ function verifyReviewerHashesAtMergeBase(sha, baseSha, payload, refname) {
|
|
|
7733
8445
|
if (!approval.tools_sha256) missing.push("tools_sha256");
|
|
7734
8446
|
if (!approval.mcp_sha256) missing.push("mcp_sha256");
|
|
7735
8447
|
if (missing.length > 0) {
|
|
7736
|
-
|
|
7737
|
-
|
|
7738
|
-
`${prefix} approval for "${approval.reviewer}" is missing ${missing.join(", ")}`
|
|
7739
|
-
|
|
8448
|
+
return {
|
|
8449
|
+
ok: false,
|
|
8450
|
+
reason: `${prefix} approval for "${approval.reviewer}" is missing ${missing.join(", ")}`
|
|
8451
|
+
};
|
|
7740
8452
|
}
|
|
7741
8453
|
const def = reviewers[approval.reviewer];
|
|
7742
8454
|
if (!def) {
|
|
7743
|
-
|
|
7744
|
-
|
|
7745
|
-
`${prefix} reviewer "${approval.reviewer}" not defined in .stamp/config.yml at merge-base`
|
|
7746
|
-
|
|
8455
|
+
return {
|
|
8456
|
+
ok: false,
|
|
8457
|
+
reason: `${prefix} reviewer "${approval.reviewer}" not defined in .stamp/config.yml at merge-base`
|
|
8458
|
+
};
|
|
7747
8459
|
}
|
|
7748
8460
|
let promptBytes;
|
|
7749
8461
|
try {
|
|
7750
|
-
promptBytes =
|
|
8462
|
+
promptBytes = run2(["show", `${baseSha}:${def.prompt}`]);
|
|
7751
8463
|
} catch {
|
|
7752
|
-
|
|
7753
|
-
|
|
7754
|
-
`${prefix} reviewer "${approval.reviewer}" prompt "${def.prompt}" unreadable at merge-base`
|
|
7755
|
-
|
|
8464
|
+
return {
|
|
8465
|
+
ok: false,
|
|
8466
|
+
reason: `${prefix} reviewer "${approval.reviewer}" prompt "${def.prompt}" unreadable at merge-base`
|
|
8467
|
+
};
|
|
8468
|
+
}
|
|
8469
|
+
const fields = [
|
|
8470
|
+
{ field: "prompt", computed: hashPromptBytes(Buffer.from(promptBytes, "utf8")), expected: approval.prompt_sha256 },
|
|
8471
|
+
{ field: "tools", computed: hashTools(def.tools), expected: approval.tools_sha256 },
|
|
8472
|
+
{ field: "mcp_servers", computed: hashMcpServers(def.mcp_servers), expected: approval.mcp_sha256 }
|
|
8473
|
+
];
|
|
8474
|
+
for (const f of fields) {
|
|
8475
|
+
if (f.computed === f.expected) continue;
|
|
8476
|
+
return {
|
|
8477
|
+
ok: false,
|
|
8478
|
+
reason: `${prefix} reviewer "${approval.reviewer}" ${f.field} hash mismatch (expected ${f.expected.slice(0, 16)}..., committed tree has ${f.computed.slice(0, 16)}...). The committed config differs from what the attestation claims; re-run stamp merge or revert the change.`
|
|
8479
|
+
};
|
|
7756
8480
|
}
|
|
7757
|
-
checkHashOrReject(prefix, refname, approval.reviewer, "prompt", hashPromptBytes(Buffer.from(promptBytes, "utf8")), approval.prompt_sha256);
|
|
7758
|
-
checkHashOrReject(prefix, refname, approval.reviewer, "tools", hashTools(def.tools), approval.tools_sha256);
|
|
7759
|
-
checkHashOrReject(prefix, refname, approval.reviewer, "mcp_servers", hashMcpServers(def.mcp_servers), approval.mcp_sha256);
|
|
7760
8481
|
}
|
|
8482
|
+
return { ok: true };
|
|
7761
8483
|
}
|
|
7762
|
-
function
|
|
7763
|
-
|
|
7764
|
-
|
|
7765
|
-
|
|
7766
|
-
|
|
7767
|
-
|
|
8484
|
+
function verifyCommitV4(sha, branch, rule, payloadBytes, signatureBase64, refname) {
|
|
8485
|
+
let payload;
|
|
8486
|
+
try {
|
|
8487
|
+
payload = JSON.parse(payloadBytes.toString("utf8"));
|
|
8488
|
+
} catch (err) {
|
|
8489
|
+
reject(
|
|
8490
|
+
refname,
|
|
8491
|
+
`commit ${sha.slice(0, 8)}: v4 payload is not valid JSON \u2014 ${err instanceof Error ? err.message : String(err)}`
|
|
8492
|
+
);
|
|
8493
|
+
}
|
|
8494
|
+
if (!payload || typeof payload !== "object" || typeof payload.schema_version !== "number" || typeof payload.base_sha !== "string" || typeof payload.head_sha !== "string" || typeof payload.target_branch !== "string" || typeof payload.diff_sha256 !== "string" || typeof payload.signer_key_id !== "string" || !Array.isArray(payload.approvals) || !Array.isArray(payload.checks) || !Array.isArray(payload.trust_anchor_signatures)) {
|
|
8495
|
+
reject(
|
|
8496
|
+
refname,
|
|
8497
|
+
`commit ${sha.slice(0, 8)}: v4 payload has invalid structure (missing or wrong-typed fields)`
|
|
8498
|
+
);
|
|
8499
|
+
}
|
|
8500
|
+
if (payload.schema_version < MIN_ACCEPTED_V4_SCHEMA_VERSION) {
|
|
8501
|
+
reject(
|
|
8502
|
+
refname,
|
|
8503
|
+
`commit ${sha.slice(0, 8)}: v4 attestation schema_version ${payload.schema_version} is below minimum ${MIN_ACCEPTED_V4_SCHEMA_VERSION}. Re-create the merge with a current stamp-cli build.`
|
|
8504
|
+
);
|
|
8505
|
+
}
|
|
8506
|
+
let manifestYaml;
|
|
8507
|
+
try {
|
|
8508
|
+
manifestYaml = run2(["show", `${payload.base_sha}:.stamp/trusted-keys/manifest.yml`]);
|
|
8509
|
+
} catch (err) {
|
|
8510
|
+
reject(
|
|
8511
|
+
refname,
|
|
8512
|
+
`commit ${sha.slice(0, 8)}: .stamp/trusted-keys/manifest.yml is missing at base ${payload.base_sha.slice(0, 8)} \u2014 v4 attestations require the manifest in the merge-base tree. (${err instanceof Error ? err.message : String(err)})`
|
|
8513
|
+
);
|
|
8514
|
+
}
|
|
8515
|
+
const manifest = parseManifest(manifestYaml);
|
|
8516
|
+
if (!manifest) {
|
|
8517
|
+
reject(
|
|
8518
|
+
refname,
|
|
8519
|
+
`commit ${sha.slice(0, 8)}: .stamp/trusted-keys/manifest.yml at base ${payload.base_sha.slice(0, 8)} failed to parse (bad YAML, duplicate fingerprint, unknown capability, etc.)`
|
|
8520
|
+
);
|
|
8521
|
+
}
|
|
8522
|
+
const pubkeyByFingerprint = readPubkeyMapAt(payload.base_sha);
|
|
8523
|
+
const configAtBase = readConfigAt(payload.base_sha);
|
|
8524
|
+
const pathRules = configAtBase?.path_rules ?? [];
|
|
8525
|
+
const changedFiles = readChangedFilesAtRef(payload.base_sha, payload.head_sha);
|
|
8526
|
+
if (changedFiles === null) {
|
|
8527
|
+
reject(
|
|
8528
|
+
refname,
|
|
8529
|
+
`commit ${sha.slice(0, 8)}: unable to enumerate changed files between base ${payload.base_sha.slice(0, 8)} and head ${payload.head_sha.slice(0, 8)} for path_rules evaluation. Run \`stamp review --diff <base>..<head>\` and re-attempt the merge.`
|
|
8530
|
+
);
|
|
8531
|
+
return;
|
|
8532
|
+
}
|
|
8533
|
+
const input = {
|
|
8534
|
+
sha,
|
|
8535
|
+
branch,
|
|
8536
|
+
rule,
|
|
8537
|
+
payload,
|
|
8538
|
+
payloadBytes,
|
|
8539
|
+
signatureBase64,
|
|
8540
|
+
manifest,
|
|
8541
|
+
pubkeyByFingerprint,
|
|
8542
|
+
pathRules,
|
|
8543
|
+
changedFiles
|
|
8544
|
+
};
|
|
8545
|
+
for (const phase of COMMIT_PHASES_V4) {
|
|
8546
|
+
const result = phase.fn(input);
|
|
8547
|
+
if (!result.ok) reject(refname, result.reason);
|
|
8548
|
+
}
|
|
7768
8549
|
}
|
|
7769
|
-
function
|
|
8550
|
+
function run2(args) {
|
|
7770
8551
|
try {
|
|
7771
|
-
return (0,
|
|
8552
|
+
return (0, import_node_child_process3.execFileSync)("git", args, {
|
|
7772
8553
|
encoding: "utf8",
|
|
7773
8554
|
maxBuffer: 16 * 1024 * 1024,
|
|
7774
8555
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -7797,8 +8578,8 @@ function resolveBranchRule(branches, branchName) {
|
|
|
7797
8578
|
}
|
|
7798
8579
|
function readConfigAt(sha) {
|
|
7799
8580
|
try {
|
|
7800
|
-
const raw =
|
|
7801
|
-
const parsed = (0,
|
|
8581
|
+
const raw = run2(["show", `${sha}:.stamp/config.yml`]);
|
|
8582
|
+
const parsed = (0, import_yaml5.parse)(raw);
|
|
7802
8583
|
if (!parsed || typeof parsed !== "object") return null;
|
|
7803
8584
|
const obj = parsed;
|
|
7804
8585
|
const branches = {};
|
|
@@ -7824,7 +8605,15 @@ function readConfigAt(sha) {
|
|
|
7824
8605
|
};
|
|
7825
8606
|
}
|
|
7826
8607
|
}
|
|
7827
|
-
|
|
8608
|
+
const parsedPathRules = parsePathRules(obj.path_rules);
|
|
8609
|
+
for (const warning of parsedPathRules.warnings) {
|
|
8610
|
+
process.stderr.write(`stamp-verify: ${warning}
|
|
8611
|
+
`);
|
|
8612
|
+
}
|
|
8613
|
+
return {
|
|
8614
|
+
branches,
|
|
8615
|
+
...parsedPathRules.rules.length > 0 ? { path_rules: parsedPathRules.rules } : {}
|
|
8616
|
+
};
|
|
7828
8617
|
} catch {
|
|
7829
8618
|
return null;
|
|
7830
8619
|
}
|
|
@@ -7833,14 +8622,14 @@ function readTrustedKeysAt(sha) {
|
|
|
7833
8622
|
const map = /* @__PURE__ */ new Map();
|
|
7834
8623
|
let lsOut;
|
|
7835
8624
|
try {
|
|
7836
|
-
lsOut =
|
|
8625
|
+
lsOut = run2(["ls-tree", "-r", "--name-only", sha, ".stamp/trusted-keys/"]);
|
|
7837
8626
|
} catch {
|
|
7838
8627
|
return map;
|
|
7839
8628
|
}
|
|
7840
8629
|
const files = lsOut.split("\n").filter((f) => f.endsWith(".pub"));
|
|
7841
8630
|
for (const path of files) {
|
|
7842
8631
|
try {
|
|
7843
|
-
const pem =
|
|
8632
|
+
const pem = run2(["show", `${sha}:${path}`]);
|
|
7844
8633
|
const fp = fingerprintFromPem(pem);
|
|
7845
8634
|
map.set(fp, pem);
|
|
7846
8635
|
} catch {
|
|
@@ -7850,7 +8639,7 @@ function readTrustedKeysAt(sha) {
|
|
|
7850
8639
|
}
|
|
7851
8640
|
function readLiveRef(refname) {
|
|
7852
8641
|
try {
|
|
7853
|
-
return (0,
|
|
8642
|
+
return (0, import_node_child_process3.execFileSync)("git", ["rev-parse", "--verify", refname], {
|
|
7854
8643
|
encoding: "utf8",
|
|
7855
8644
|
stdio: ["ignore", "pipe", "pipe"]
|
|
7856
8645
|
}).trim();
|
|
@@ -7860,7 +8649,7 @@ function readLiveRef(refname) {
|
|
|
7860
8649
|
}
|
|
7861
8650
|
function isAncestor(ancestor, descendant) {
|
|
7862
8651
|
try {
|
|
7863
|
-
(0,
|
|
8652
|
+
(0, import_node_child_process3.execFileSync)(
|
|
7864
8653
|
"git",
|
|
7865
8654
|
["merge-base", "--is-ancestor", ancestor, descendant],
|
|
7866
8655
|
{ stdio: "ignore" }
|
|
@@ -7871,7 +8660,7 @@ function isAncestor(ancestor, descendant) {
|
|
|
7871
8660
|
}
|
|
7872
8661
|
}
|
|
7873
8662
|
function listNewCommits(oldSha, newSha) {
|
|
7874
|
-
const out =
|
|
8663
|
+
const out = run2([
|
|
7875
8664
|
"rev-list",
|
|
7876
8665
|
"--first-parent",
|
|
7877
8666
|
`${oldSha}..${newSha}`
|
|
@@ -7896,14 +8685,41 @@ function reject(refname, reason) {
|
|
|
7896
8685
|
`);
|
|
7897
8686
|
process.exit(1);
|
|
7898
8687
|
}
|
|
7899
|
-
|
|
7900
|
-
|
|
7901
|
-
|
|
7902
|
-
|
|
7903
|
-
|
|
7904
|
-
|
|
8688
|
+
function isMainModule() {
|
|
8689
|
+
const argv1 = typeof process !== "undefined" ? process.argv?.[1] : void 0;
|
|
8690
|
+
if (!argv1) {
|
|
8691
|
+
return false;
|
|
8692
|
+
}
|
|
8693
|
+
try {
|
|
8694
|
+
return (0, import_node_url.fileURLToPath)(import_meta.url) === argv1;
|
|
8695
|
+
} catch {
|
|
8696
|
+
return true;
|
|
8697
|
+
}
|
|
8698
|
+
}
|
|
8699
|
+
if (isMainModule()) {
|
|
8700
|
+
try {
|
|
8701
|
+
main();
|
|
8702
|
+
process.exit(0);
|
|
8703
|
+
} catch (err) {
|
|
8704
|
+
process.stderr.write(
|
|
8705
|
+
`stamp-verify: internal error \u2014 ${err instanceof Error ? err.stack ?? err.message : String(err)}
|
|
7905
8706
|
`
|
|
7906
|
-
|
|
7907
|
-
|
|
8707
|
+
);
|
|
8708
|
+
process.exit(1);
|
|
8709
|
+
}
|
|
7908
8710
|
}
|
|
8711
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
8712
|
+
0 && (module.exports = {
|
|
8713
|
+
parsePathRules,
|
|
8714
|
+
verifyV4ApprovalSignatures,
|
|
8715
|
+
verifyV4Approvals,
|
|
8716
|
+
verifyV4Checks,
|
|
8717
|
+
verifyV4DiffHash,
|
|
8718
|
+
verifyV4MergeStructure,
|
|
8719
|
+
verifyV4OuterSignature,
|
|
8720
|
+
verifyV4SignerTrust,
|
|
8721
|
+
verifyV4StampPathsGuard,
|
|
8722
|
+
verifyV4TargetBranch,
|
|
8723
|
+
verifyV4TrustAnchorSignatures
|
|
8724
|
+
});
|
|
7909
8725
|
//# sourceMappingURL=pre-receive.cjs.map
|