@openthink/stamp 1.10.0 → 2.0.1
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 +162 -46
- package/dist/{chunk-V7W3CFNP.js → chunk-4PFD2DSY.js} +91 -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 +6213 -1865
- 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 +8357 -0
- package/dist/server/stamp-review.cjs.map +1 -0
- package/dist/{ui-BC7UWSJW.js → ui-P5DRAT3P.js} +2 -2
- package/package.json +2 -1
- package/dist/chunk-V7W3CFNP.js.map +0 -1
- /package/dist/{ui-BC7UWSJW.js.map → ui-P5DRAT3P.js.map} +0 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// src/lib/reviewSigningKey.ts
|
|
5
|
+
var import_node_crypto2 = require("crypto");
|
|
6
|
+
var import_node_fs3 = require("fs");
|
|
7
|
+
var import_node_path3 = require("path");
|
|
8
|
+
|
|
9
|
+
// src/lib/keys.ts
|
|
10
|
+
var import_node_crypto = require("crypto");
|
|
11
|
+
var import_node_fs2 = require("fs");
|
|
12
|
+
var import_node_path2 = require("path");
|
|
13
|
+
|
|
14
|
+
// src/lib/paths.ts
|
|
15
|
+
var import_node_fs = require("fs");
|
|
16
|
+
var import_node_os = require("os");
|
|
17
|
+
var import_node_path = require("path");
|
|
18
|
+
|
|
19
|
+
// src/lib/keys.ts
|
|
20
|
+
function fingerprintFromPem(publicKeyPem) {
|
|
21
|
+
const pub = (0, import_node_crypto.createPublicKey)(publicKeyPem);
|
|
22
|
+
const raw = pub.export({ type: "spki", format: "der" });
|
|
23
|
+
const hash = (0, import_node_crypto.createHash)("sha256").update(raw).digest("hex");
|
|
24
|
+
return `sha256:${hash}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// src/lib/reviewSigningKey.ts
|
|
28
|
+
var REQUIRED_PRIVATE_KEY_MODE = 384;
|
|
29
|
+
var PUBLIC_KEY_MODE = 420;
|
|
30
|
+
function publicKeyPathFor(privateKeyPath) {
|
|
31
|
+
if (privateKeyPath.endsWith(".pem")) {
|
|
32
|
+
return privateKeyPath.slice(0, -".pem".length) + ".pub";
|
|
33
|
+
}
|
|
34
|
+
return privateKeyPath + ".pub";
|
|
35
|
+
}
|
|
36
|
+
var ReviewSigningKeyError = class extends Error {
|
|
37
|
+
constructor(message) {
|
|
38
|
+
super(message);
|
|
39
|
+
this.name = "ReviewSigningKeyError";
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
function ensureReviewSigningKey(opts) {
|
|
43
|
+
const privateKeyPath = opts.privateKeyPath;
|
|
44
|
+
const publicKeyPath = publicKeyPathFor(privateKeyPath);
|
|
45
|
+
let existingStat;
|
|
46
|
+
try {
|
|
47
|
+
existingStat = (0, import_node_fs3.statSync)(privateKeyPath);
|
|
48
|
+
} catch (err) {
|
|
49
|
+
if (err.code === "ENOENT") {
|
|
50
|
+
return mintNewKey(privateKeyPath, publicKeyPath);
|
|
51
|
+
}
|
|
52
|
+
throw err;
|
|
53
|
+
}
|
|
54
|
+
const mode = existingStat.mode & 511;
|
|
55
|
+
if (mode !== REQUIRED_PRIVATE_KEY_MODE) {
|
|
56
|
+
throw new ReviewSigningKeyError(
|
|
57
|
+
`review-signing key at ${privateKeyPath} has mode 0${mode.toString(8).padStart(3, "0")}; required 0${REQUIRED_PRIVATE_KEY_MODE.toString(8).padStart(3, "0")} (owner read+write, no group/other access). Refusing to load. Fix with: chmod 600 ${privateKeyPath}`
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
let privateKey;
|
|
61
|
+
let publicKeyPem;
|
|
62
|
+
let fingerprint;
|
|
63
|
+
try {
|
|
64
|
+
const privateKeyPem = (0, import_node_fs3.readFileSync)(privateKeyPath, "utf8");
|
|
65
|
+
privateKey = (0, import_node_crypto2.createPrivateKey)({ key: privateKeyPem, format: "pem" });
|
|
66
|
+
} catch (err) {
|
|
67
|
+
throw new ReviewSigningKeyError(
|
|
68
|
+
`review-signing key at ${privateKeyPath} could not be loaded: ${err.message}`
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
if (privateKey.asymmetricKeyType !== "ed25519") {
|
|
73
|
+
throw new Error(
|
|
74
|
+
`expected asymmetricKeyType=ed25519, got ${privateKey.asymmetricKeyType ?? "<unknown>"}`
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
publicKeyPem = exportPublicPem(privateKey);
|
|
78
|
+
fingerprint = fingerprintFromPem(publicKeyPem);
|
|
79
|
+
} catch (err) {
|
|
80
|
+
throw new ReviewSigningKeyError(
|
|
81
|
+
`review-signing key at ${privateKeyPath} could not be parsed as an Ed25519 private key: ${err.message}`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
const existingPub = (0, import_node_fs3.readFileSync)(publicKeyPath, "utf8");
|
|
86
|
+
if (existingPub !== publicKeyPem) {
|
|
87
|
+
(0, import_node_fs3.writeFileSync)(publicKeyPath, publicKeyPem, { mode: PUBLIC_KEY_MODE });
|
|
88
|
+
(0, import_node_fs3.chmodSync)(publicKeyPath, PUBLIC_KEY_MODE);
|
|
89
|
+
}
|
|
90
|
+
} catch (err) {
|
|
91
|
+
if (err.code === "ENOENT") {
|
|
92
|
+
(0, import_node_fs3.writeFileSync)(publicKeyPath, publicKeyPem, { mode: PUBLIC_KEY_MODE });
|
|
93
|
+
(0, import_node_fs3.chmodSync)(publicKeyPath, PUBLIC_KEY_MODE);
|
|
94
|
+
} else {
|
|
95
|
+
throw err;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
privateKeyPath,
|
|
100
|
+
publicKeyPath,
|
|
101
|
+
privateKey,
|
|
102
|
+
publicKeyPem,
|
|
103
|
+
fingerprint,
|
|
104
|
+
created: false
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function exportPublicPem(privateKey) {
|
|
108
|
+
const publicKeyObj = (0, import_node_crypto2.createPublicKey)(privateKey);
|
|
109
|
+
return publicKeyObj.export({ type: "spki", format: "pem" });
|
|
110
|
+
}
|
|
111
|
+
function resolveReviewSigningKeyPath() {
|
|
112
|
+
const override = process.env["REVIEW_SIGNING_KEY_PATH"];
|
|
113
|
+
if (override && override.length > 0) return override;
|
|
114
|
+
const stateDir = process.env["STAMP_STATE_DIR"] ?? "/srv/git/.stamp-state";
|
|
115
|
+
return stateDir.replace(/\/+$/, "") + "/review-signing-key.pem";
|
|
116
|
+
}
|
|
117
|
+
function mintNewKey(privateKeyPath, publicKeyPath) {
|
|
118
|
+
const parent = (0, import_node_path3.dirname)(privateKeyPath);
|
|
119
|
+
(0, import_node_fs3.mkdirSync)(parent, { recursive: true, mode: 448 });
|
|
120
|
+
const { publicKey, privateKey } = (0, import_node_crypto2.generateKeyPairSync)("ed25519");
|
|
121
|
+
const privateKeyPem = privateKey.export({
|
|
122
|
+
type: "pkcs8",
|
|
123
|
+
format: "pem"
|
|
124
|
+
});
|
|
125
|
+
const publicKeyPem = publicKey.export({
|
|
126
|
+
type: "spki",
|
|
127
|
+
format: "pem"
|
|
128
|
+
});
|
|
129
|
+
(0, import_node_fs3.writeFileSync)(privateKeyPath, privateKeyPem, {
|
|
130
|
+
mode: REQUIRED_PRIVATE_KEY_MODE
|
|
131
|
+
});
|
|
132
|
+
(0, import_node_fs3.chmodSync)(privateKeyPath, REQUIRED_PRIVATE_KEY_MODE);
|
|
133
|
+
(0, import_node_fs3.writeFileSync)(publicKeyPath, publicKeyPem, { mode: PUBLIC_KEY_MODE });
|
|
134
|
+
(0, import_node_fs3.chmodSync)(publicKeyPath, PUBLIC_KEY_MODE);
|
|
135
|
+
return {
|
|
136
|
+
privateKeyPath,
|
|
137
|
+
publicKeyPath,
|
|
138
|
+
privateKey,
|
|
139
|
+
publicKeyPem,
|
|
140
|
+
fingerprint: fingerprintFromPem(publicKeyPem),
|
|
141
|
+
created: true
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// src/server/bootstrap-review-key.ts
|
|
146
|
+
function printGeneratedBanner(fingerprint, publicKeyPath) {
|
|
147
|
+
const border = "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500";
|
|
148
|
+
const lines = [
|
|
149
|
+
"",
|
|
150
|
+
border,
|
|
151
|
+
" STAMP-SERVER: review-signing key generated (first boot)",
|
|
152
|
+
border,
|
|
153
|
+
" fingerprint: " + fingerprint,
|
|
154
|
+
" public key: " + publicKeyPath,
|
|
155
|
+
"",
|
|
156
|
+
" Next step \u2014 commit this fingerprint to",
|
|
157
|
+
" `.stamp/trusted-keys/manifest.yml` with `capabilities: [server]`",
|
|
158
|
+
" in every repo that delegates reviews to this server.",
|
|
159
|
+
"",
|
|
160
|
+
" See `docs/plans/server-attested-reviews.md` (Trust model section)",
|
|
161
|
+
" for the manifest entry format. The pubkey is also fetchable via",
|
|
162
|
+
" `ssh git@<host> stamp-server-pubkey --review-signing`.",
|
|
163
|
+
border,
|
|
164
|
+
""
|
|
165
|
+
];
|
|
166
|
+
process.stdout.write(lines.join("\n"));
|
|
167
|
+
}
|
|
168
|
+
function main() {
|
|
169
|
+
const apiKey = process.env["ANTHROPIC_API_KEY"];
|
|
170
|
+
if (!apiKey || apiKey.length === 0) {
|
|
171
|
+
console.log(
|
|
172
|
+
"stamp-bootstrap-review-key: ANTHROPIC_API_KEY unset; review capability disabled, skipping keygen"
|
|
173
|
+
);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const privateKeyPath = resolveReviewSigningKeyPath();
|
|
177
|
+
let result;
|
|
178
|
+
try {
|
|
179
|
+
result = ensureReviewSigningKey({ privateKeyPath });
|
|
180
|
+
} catch (err) {
|
|
181
|
+
if (err instanceof ReviewSigningKeyError) {
|
|
182
|
+
process.stderr.write("error: " + err.message + "\n");
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
throw err;
|
|
186
|
+
}
|
|
187
|
+
if (result.created) {
|
|
188
|
+
printGeneratedBanner(result.fingerprint, result.publicKeyPath);
|
|
189
|
+
} else {
|
|
190
|
+
console.log(
|
|
191
|
+
`stamp-bootstrap-review-key: reusing existing review-signing key at ${result.privateKeyPath} (fingerprint: ${result.fingerprint})`
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
main();
|
|
196
|
+
//# sourceMappingURL=bootstrap-review-key.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/lib/reviewSigningKey.ts","../../src/lib/keys.ts","../../src/lib/paths.ts","../../src/server/bootstrap-review-key.ts"],"sourcesContent":["/**\n * Server-side Ed25519 review-signing key bootstrap (AGT-327 / M2).\n *\n * This module is the load-bearing source of truth for stamp-server's\n * review-signing key: the private key whose signatures over each\n * approval payload prove the verdict came from a real LLM call made\n * BY THE SERVER against the canonical reviewer prompt. The operator\n * cannot forge this signature — that's the entire point of the\n * server-attested reviews design (see\n * `docs/plans/server-attested-reviews.md`, \"Trust model\" / \"Server\n * deployment artifact\"). Idempotency is therefore non-negotiable:\n * every restart must reuse the same key, otherwise the fingerprint\n * the operator committed to `.stamp/trusted-keys/manifest.yml` stops\n * matching and every prior attestation chain breaks at verify time.\n *\n * Lifecycle:\n *\n * 1. Container boots, entrypoint resolves the key path (env override\n * `REVIEW_SIGNING_KEY_PATH` or the default\n * `$STAMP_STATE_DIR/review-signing-key.pem`).\n * 2. `ensureReviewSigningKey({ path })` is invoked.\n * - File absent → generate a fresh Ed25519 keypair, write the\n * private half mode 0600 + public half mode 0644, return with\n * `created: true`.\n * - File present, mode 0600, readable → load + return with\n * `created: false`. Same fingerprint as the previous boot.\n * - File present but wrong mode / unreadable → throw a structured\n * error. The caller (bootstrap script) must abort startup;\n * silently re-generating would rotate the server's identity\n * without operator consent.\n *\n * What this module does NOT do:\n *\n * - Print to stderr / advertise the fingerprint. The bootstrap script\n * wraps that; this module is plain library code with no side effects\n * on stdio so it stays unit-testable.\n * - Resolve the path. Path resolution (the env-var override, the\n * default-state-dir fallback) lives in the bootstrap script — this\n * module takes an absolute path and operates on it.\n * - Consult `ANTHROPIC_API_KEY` or any other env var. The capability\n * gate lives at the script layer; if review capability is disabled,\n * the bootstrap script simply doesn't call into here.\n * - Validate that the path lives inside `$STATE_DIR`. Operators may\n * legitimately override `REVIEW_SIGNING_KEY_PATH` to a mount of\n * their choice (e.g. a secrets manager fuse mount).\n *\n * Companion to `src/lib/keys.ts`'s `ensureUserKeypair` (operator-side\n * signing key). That module manages keys at well-known paths under\n * `~/.stamp/keys/`; this one is path-driven so the server-side\n * deployment can pin the location via env without monkey-patching\n * resolution code.\n */\n\nimport {\n createPrivateKey,\n createPublicKey,\n generateKeyPairSync,\n KeyObject,\n} from \"node:crypto\";\nimport {\n chmodSync,\n mkdirSync,\n readFileSync,\n statSync,\n writeFileSync,\n} from \"node:fs\";\nimport { dirname } from \"node:path\";\n\nimport { fingerprintFromPem } from \"./keys.js\";\n\n/**\n * The required mode bits on the private key file. Standard\n * owner-only-read posture for any long-lived signing key on a shared\n * filesystem; matches the mode the operator's `~/.stamp/keys/ed25519`\n * file uses (see `saveUserKeypair`). OpenSSH-strict-perms semantics\n * apply by analogy: any group or other bits on a private key are a\n * misconfiguration the bootstrap MUST refuse to silently accept.\n */\nexport const REQUIRED_PRIVATE_KEY_MODE = 0o600;\n\n/** Public-half mode. Matches the user-keypair posture; nothing reads\n * the .pub file with strict-perms expectations, but uniform mode keeps\n * operator mental-model simple. */\nexport const PUBLIC_KEY_MODE = 0o644;\n\n/** Suffix swap for the public key path. The private key lives at\n * `<base>.pem`; the public key lives at `<base>.pub`. This is the\n * convention the design doc bakes in (\"$STATE_DIR/review-signing-key.pem\"\n * + the public half fetched via SSH verb). */\nexport function publicKeyPathFor(privateKeyPath: string): string {\n // Replace a trailing .pem (case-sensitive) with .pub. If the operator\n // points REVIEW_SIGNING_KEY_PATH at something without a .pem suffix,\n // append .pub so we still produce a sibling file rather than\n // accidentally overwriting the private key.\n if (privateKeyPath.endsWith(\".pem\")) {\n return privateKeyPath.slice(0, -\".pem\".length) + \".pub\";\n }\n return privateKeyPath + \".pub\";\n}\n\nexport interface ReviewSigningKeyResult {\n /** Absolute path of the private key file on disk. Echoed back so the\n * caller doesn't have to track it separately when printing\n * diagnostics. */\n privateKeyPath: string;\n /** Absolute path of the sibling public key file. */\n publicKeyPath: string;\n /** Private key as a Node `KeyObject` rather than a raw PEM string.\n * This is the load-bearing security property of the return shape:\n * `JSON.stringify` on a `KeyObject` produces `{}` by design, so a\n * future caller that accidentally serializes the whole result\n * (structured logging, error contexts, `JSON.stringify(result)`)\n * cannot leak the private material. Signing call sites use\n * `crypto.sign(null, data, privateKey)` directly against this\n * object; callers that need PEM bytes for disk writes have the\n * paths and can read from disk explicitly. */\n privateKey: KeyObject;\n /** PEM-encoded public key (spki). Safe to log/serialize. */\n publicKeyPem: string;\n /** `sha256:<hex>` over the SPKI DER bytes — matches `fingerprintFromPem`\n * output so the same string round-trips into\n * `.stamp/trusted-keys/manifest.yml` and `attestationV4.server_key_id`. */\n fingerprint: string;\n /** `true` on the boot that minted the key; `false` on every\n * subsequent boot that reused it. Drives the loud first-boot\n * fingerprint advertisement in the bootstrap script. */\n created: boolean;\n}\n\n/**\n * Error class for fatal bootstrap conditions. The bootstrap script\n * catches this specifically and converts it into a non-zero exit with\n * the operator-readable message — distinct from generic Node EACCES/\n * ENOENT errors which usually indicate orchestration bugs (volume not\n * mounted yet, etc.) and should bubble with their original stack.\n */\nexport class ReviewSigningKeyError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"ReviewSigningKeyError\";\n }\n}\n\n/**\n * Ensure a review-signing keypair exists at `privateKeyPath`, generating\n * one on first call and reusing it on subsequent calls. See the module\n * docstring for the full lifecycle contract.\n *\n * Throws `ReviewSigningKeyError` on conditions where silent recovery\n * would compromise the trust model:\n * - Existing private key has group/other permission bits set\n * - Existing private key file is unreadable\n * - Existing private key parses but the public key derivation fails\n *\n * Generic FS errors (write failure, parent dir not writable, etc.) bubble\n * with their original error so platform misconfiguration surfaces\n * clearly rather than getting wrapped into an opaque domain error.\n */\nexport function ensureReviewSigningKey(opts: {\n privateKeyPath: string;\n}): ReviewSigningKeyResult {\n const privateKeyPath = opts.privateKeyPath;\n const publicKeyPath = publicKeyPathFor(privateKeyPath);\n\n // Look for an existing private key first. The presence-or-absence\n // check is done via statSync (rather than readFileSync + ENOENT\n // catch) so we can also inspect the mode before reading the bytes —\n // a wrong-mode file should abort before we even open it.\n let existingStat;\n try {\n existingStat = statSync(privateKeyPath);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n // First-boot path: mint a new key.\n return mintNewKey(privateKeyPath, publicKeyPath);\n }\n // Any other stat error (EACCES on the parent dir, EIO, etc.) is\n // an environment problem the operator needs to see verbatim.\n throw err;\n }\n\n // File exists — verify mode before reading.\n //\n // statSync's mode field is the full st_mode, including the file-type\n // bits in the high half. Mask to the low 9 bits (rwxrwxrwx) for the\n // permission compare. Anything other than exactly 0600 (owner rw,\n // no group, no other) is a refusal: a 0640 file leaks the key to\n // any group member, a 0644 file leaks it world-wide. We refuse\n // rather than auto-chmod because the wrong mode is often a sign\n // that someone restored from a backup that didn't preserve perms,\n // or that an unrelated process is touching the file — either way,\n // failing loud is correct.\n const mode = existingStat.mode & 0o777;\n if (mode !== REQUIRED_PRIVATE_KEY_MODE) {\n throw new ReviewSigningKeyError(\n `review-signing key at ${privateKeyPath} has mode 0${mode.toString(8).padStart(3, \"0\")}; ` +\n `required 0${REQUIRED_PRIVATE_KEY_MODE.toString(8).padStart(3, \"0\")} (owner read+write, no group/other access). ` +\n `Refusing to load. Fix with: chmod 600 ${privateKeyPath}`,\n );\n }\n\n // Mode looks right — read and parse. We materialize the PEM as a\n // string only as long as it takes to construct the KeyObject, then\n // discard it: the public return surface holds the opaque\n // KeyObject (non-serializable, can't accidentally leak via\n // JSON.stringify) and the derived public-half PEM (safe to log).\n // A caller that wants to sign feeds the KeyObject directly to\n // crypto.sign(null, data, privateKey).\n let privateKey: KeyObject;\n let publicKeyPem: string;\n let fingerprint: string;\n try {\n const privateKeyPem = readFileSync(privateKeyPath, \"utf8\");\n privateKey = createPrivateKey({ key: privateKeyPem, format: \"pem\" });\n } catch (err) {\n throw new ReviewSigningKeyError(\n `review-signing key at ${privateKeyPath} could not be loaded: ${(err as Error).message}`,\n );\n }\n\n // Re-derive the public half from the private key rather than reading\n // the .pub file. The .pub file is a convenience for the SSH verb that\n // serves the pubkey; the signing identity is whatever the private key\n // says it is. If a future operator deletes the .pub file by accident\n // we still want the server to come up.\n try {\n if (privateKey.asymmetricKeyType !== \"ed25519\") {\n throw new Error(\n `expected asymmetricKeyType=ed25519, got ${privateKey.asymmetricKeyType ?? \"<unknown>\"}`,\n );\n }\n publicKeyPem = exportPublicPem(privateKey);\n fingerprint = fingerprintFromPem(publicKeyPem);\n } catch (err) {\n throw new ReviewSigningKeyError(\n `review-signing key at ${privateKeyPath} could not be parsed as an Ed25519 private key: ` +\n `${(err as Error).message}`,\n );\n }\n\n // Re-write the public key file if it's missing OR out of sync with\n // the private key. Out-of-sync is the more interesting case: an\n // operator who manually swaps the private key (e.g. for rotation\n // ahead of the dedicated rotate command) would leave stale public\n // bytes on disk otherwise, and the SSH pubkey verb would then serve\n // a fingerprint that doesn't match what the server is actually\n // signing with. Idempotent on the common path (read existing,\n // compare, no-op).\n try {\n const existingPub = readFileSync(publicKeyPath, \"utf8\");\n if (existingPub !== publicKeyPem) {\n writeFileSync(publicKeyPath, publicKeyPem, { mode: PUBLIC_KEY_MODE });\n chmodSync(publicKeyPath, PUBLIC_KEY_MODE);\n }\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n writeFileSync(publicKeyPath, publicKeyPem, { mode: PUBLIC_KEY_MODE });\n chmodSync(publicKeyPath, PUBLIC_KEY_MODE);\n } else {\n throw err;\n }\n }\n\n return {\n privateKeyPath,\n publicKeyPath,\n privateKey,\n publicKeyPem,\n fingerprint,\n created: false,\n };\n}\n\n/** Helper: derive the SPKI PEM bytes for the public half of a private\n * key object. `createPublicKey` accepts a private `KeyObject` and\n * returns the corresponding public `KeyObject`; `.export({type:\"spki\",\n * format:\"pem\"})` produces the PEM bytes. Public PEM is the form the\n * rest of the codebase uses for `fingerprintFromPem` + the .pub\n * sibling file. */\nfunction exportPublicPem(privateKey: KeyObject): string {\n const publicKeyObj = createPublicKey(privateKey);\n return publicKeyObj.export({ type: \"spki\", format: \"pem\" }) as string;\n}\n\n/**\n * Load an existing review-signing keypair from `privateKeyPath`. Unlike\n * `ensureReviewSigningKey`, this is the runtime-request path — if the\n * key file is absent we THROW (`ReviewSigningKeyError`) rather than\n * minting a fresh one. Auto-minting on every review request would let\n * a transient FS hiccup (volume not yet mounted, key file unlinked by\n * a misconfigured tool) silently rotate the server's identity mid-flight,\n * which is the exact failure mode the trust model exists to prevent.\n *\n * The bootstrap script (`src/server/bootstrap-review-key.ts`) is the\n * ONLY caller permitted to mint — it runs once per container boot, as\n * root, before sshd accepts connections. Every subsequent caller (the\n * review pipeline, future admin verbs that need to read the fingerprint)\n * MUST use this loader so the \"key is stable across boots\" invariant is\n * structural rather than convention.\n *\n * Mode + parse validation are the same as `ensureReviewSigningKey`: a\n * wrong-mode or unparseable file throws. The realistic load-time failure\n * mode is \"file went missing\" — likely a deployment misconfiguration the\n * operator needs to see verbatim.\n */\nexport function loadReviewSigningKey(opts: {\n privateKeyPath: string;\n}): ReviewSigningKeyResult {\n const privateKeyPath = opts.privateKeyPath;\n const publicKeyPath = publicKeyPathFor(privateKeyPath);\n\n let existingStat;\n try {\n existingStat = statSync(privateKeyPath);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n throw new ReviewSigningKeyError(\n `review-signing key not found at ${privateKeyPath}. ` +\n `The server's bootstrap step (stamp-bootstrap-review-key) is ` +\n `expected to mint this on first boot. Check that ` +\n `ANTHROPIC_API_KEY is set on the server (so bootstrap runs) ` +\n `and that REVIEW_SIGNING_KEY_PATH / STAMP_STATE_DIR points at ` +\n `a writable volume that persists across restarts.`,\n );\n }\n throw err;\n }\n\n const mode = existingStat.mode & 0o777;\n if (mode !== REQUIRED_PRIVATE_KEY_MODE) {\n throw new ReviewSigningKeyError(\n `review-signing key at ${privateKeyPath} has mode 0${mode.toString(8).padStart(3, \"0\")}; ` +\n `required 0${REQUIRED_PRIVATE_KEY_MODE.toString(8).padStart(3, \"0\")} (owner read+write, no group/other access). ` +\n `Refusing to load. Fix with: chmod 600 ${privateKeyPath}`,\n );\n }\n\n let privateKey: KeyObject;\n let publicKeyPem: string;\n let fingerprint: string;\n try {\n const privateKeyPem = readFileSync(privateKeyPath, \"utf8\");\n privateKey = createPrivateKey({ key: privateKeyPem, format: \"pem\" });\n } catch (err) {\n throw new ReviewSigningKeyError(\n `review-signing key at ${privateKeyPath} could not be loaded: ${(err as Error).message}`,\n );\n }\n\n try {\n if (privateKey.asymmetricKeyType !== \"ed25519\") {\n throw new Error(\n `expected asymmetricKeyType=ed25519, got ${privateKey.asymmetricKeyType ?? \"<unknown>\"}`,\n );\n }\n publicKeyPem = exportPublicPem(privateKey);\n fingerprint = fingerprintFromPem(publicKeyPem);\n } catch (err) {\n throw new ReviewSigningKeyError(\n `review-signing key at ${privateKeyPath} could not be parsed as an Ed25519 private key: ` +\n `${(err as Error).message}`,\n );\n }\n\n return {\n privateKeyPath,\n publicKeyPath,\n privateKey,\n publicKeyPem,\n fingerprint,\n created: false,\n };\n}\n\n/**\n * Resolve the absolute path to the review-signing private key from env\n * variables, mirroring the precedence the bootstrap script uses:\n *\n * 1. `REVIEW_SIGNING_KEY_PATH` (explicit override)\n * 2. `$STAMP_STATE_DIR/review-signing-key.pem`\n * 3. `/srv/git/.stamp-state/review-signing-key.pem` (container default)\n *\n * Kept here (not duplicated in bootstrap-review-key.ts) so that the\n * pipeline at request time and the bootstrap at boot time always agree\n * on which file is \"the\" signing key. A future move (e.g. to a secrets-\n * manager mount) only needs to update one resolver.\n */\nexport function resolveReviewSigningKeyPath(): string {\n const override = process.env[\"REVIEW_SIGNING_KEY_PATH\"];\n if (override && override.length > 0) return override;\n const stateDir = process.env[\"STAMP_STATE_DIR\"] ?? \"/srv/git/.stamp-state\";\n return stateDir.replace(/\\/+$/, \"\") + \"/review-signing-key.pem\";\n}\n\n/**\n * First-boot path. Creates the parent directory if missing (so a fresh\n * volume that hasn't seen `mkdir -p $STAMP_STATE_DIR` yet still works,\n * though entrypoint.sh does that anyway), then writes both halves of a\n * fresh Ed25519 keypair with the correct modes. Two chmod calls per\n * file rather than relying on writeFileSync's `mode` option alone:\n * Node's writeFileSync honors `mode` only on file CREATION, not on\n * write to an existing file, and a chmod after the write is the\n * canonical defensive pattern.\n */\nfunction mintNewKey(\n privateKeyPath: string,\n publicKeyPath: string,\n): ReviewSigningKeyResult {\n // Ensure the parent directory exists. Mode 0700 is conservative —\n // the parent is typically `$STAMP_STATE_DIR` which entrypoint.sh\n // already sets up with broader bits for the git user, so this is\n // really just a safety net for `mkdir -p` semantics when the\n // bootstrap is invoked outside the container (tests, dev).\n const parent = dirname(privateKeyPath);\n mkdirSync(parent, { recursive: true, mode: 0o700 });\n\n const { publicKey, privateKey } = generateKeyPairSync(\"ed25519\");\n // PEM serialization is needed once for the on-disk write; after that\n // the PEM string goes out of scope and only the KeyObject is held\n // on the returned result. Mirrors the load path's \"materialize\n // briefly, then drop\" pattern so accidental serialization of the\n // result can't leak the private bytes.\n const privateKeyPem = privateKey.export({\n type: \"pkcs8\",\n format: \"pem\",\n }) as string;\n const publicKeyPem = publicKey.export({\n type: \"spki\",\n format: \"pem\",\n }) as string;\n\n writeFileSync(privateKeyPath, privateKeyPem, {\n mode: REQUIRED_PRIVATE_KEY_MODE,\n });\n chmodSync(privateKeyPath, REQUIRED_PRIVATE_KEY_MODE);\n\n writeFileSync(publicKeyPath, publicKeyPem, { mode: PUBLIC_KEY_MODE });\n chmodSync(publicKeyPath, PUBLIC_KEY_MODE);\n\n return {\n privateKeyPath,\n publicKeyPath,\n privateKey,\n publicKeyPem,\n fingerprint: fingerprintFromPem(publicKeyPem),\n created: true,\n };\n}\n","import {\n createHash,\n createPublicKey,\n generateKeyPairSync,\n KeyObject,\n} from \"node:crypto\";\nimport {\n chmodSync,\n readdirSync,\n readFileSync,\n writeFileSync,\n} from \"node:fs\";\nimport { join } from \"node:path\";\nimport {\n ensureDir,\n isFile,\n stampTrustedKeysDir,\n userKeysDir,\n} from \"./paths.js\";\n\nexport interface Keypair {\n privateKeyPem: string;\n publicKeyPem: string;\n fingerprint: string; // \"sha256:<hex>\"\n}\n\nconst PRIVATE_KEY_FILE = \"ed25519\";\nconst PUBLIC_KEY_FILE = \"ed25519.pub\";\n\nexport function generateKeypair(): Keypair {\n const { publicKey, privateKey } = generateKeyPairSync(\"ed25519\");\n const privateKeyPem = privateKey.export({\n type: \"pkcs8\",\n format: \"pem\",\n }) as string;\n const publicKeyPem = publicKey.export({\n type: \"spki\",\n format: \"pem\",\n }) as string;\n return {\n privateKeyPem,\n publicKeyPem,\n fingerprint: fingerprintFromPem(publicKeyPem),\n };\n}\n\nexport function fingerprintFromPem(publicKeyPem: string): string {\n const pub = createPublicKey(publicKeyPem);\n const raw = pub.export({ type: \"spki\", format: \"der\" }) as Buffer;\n const hash = createHash(\"sha256\").update(raw).digest(\"hex\");\n return `sha256:${hash}`;\n}\n\nexport function loadUserKeypair(): Keypair | null {\n const dir = userKeysDir();\n const privPath = join(dir, PRIVATE_KEY_FILE);\n const pubPath = join(dir, PUBLIC_KEY_FILE);\n if (!isFile(privPath) || !isFile(pubPath)) return null;\n const privateKeyPem = readFileSync(privPath, \"utf8\");\n const publicKeyPem = readFileSync(pubPath, \"utf8\");\n return {\n privateKeyPem,\n publicKeyPem,\n fingerprint: fingerprintFromPem(publicKeyPem),\n };\n}\n\nexport function saveUserKeypair(kp: Keypair): void {\n const dir = userKeysDir();\n ensureDir(dir, 0o700);\n chmodSync(dir, 0o700);\n const privPath = join(dir, PRIVATE_KEY_FILE);\n const pubPath = join(dir, PUBLIC_KEY_FILE);\n writeFileSync(privPath, kp.privateKeyPem, { mode: 0o600 });\n writeFileSync(pubPath, kp.publicKeyPem, { mode: 0o644 });\n}\n\nexport function ensureUserKeypair(): {\n keypair: Keypair;\n created: boolean;\n} {\n const existing = loadUserKeypair();\n if (existing) return { keypair: existing, created: false };\n const kp = generateKeypair();\n saveUserKeypair(kp);\n return { keypair: kp, created: true };\n}\n\nexport function publicKeyFingerprintFilename(fingerprint: string): string {\n // \"sha256:abc...\" -> \"sha256_abc....pub\" (colons are valid on unix but messy)\n return fingerprint.replace(\":\", \"_\") + \".pub\";\n}\n\nexport function publicKeyFromObject(obj: KeyObject): string {\n return obj.export({ type: \"spki\", format: \"pem\" }) as string;\n}\n\n/**\n * Look up a public key PEM in a repo's .stamp/trusted-keys/ directory by\n * fingerprint. Returns null if no file in the directory matches.\n */\nexport function findTrustedKey(\n repoRoot: string,\n fingerprint: string,\n): string | null {\n const dir = stampTrustedKeysDir(repoRoot);\n let files: string[];\n try {\n files = readdirSync(dir);\n } catch {\n return null;\n }\n for (const f of files) {\n if (!f.endsWith(\".pub\")) continue;\n let pem: string;\n try {\n pem = readFileSync(join(dir, f), \"utf8\");\n } catch {\n continue;\n }\n try {\n if (fingerprintFromPem(pem) === fingerprint) return pem;\n } catch {\n // skip malformed keys\n }\n }\n return null;\n}\n","import { existsSync, mkdirSync, readFileSync, statSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { dirname, isAbsolute, join, resolve } from \"node:path\";\n\nexport function findRepoRoot(startFrom: string = process.cwd()): string {\n let current = resolve(startFrom);\n while (true) {\n if (existsSync(join(current, \".git\"))) return current;\n const parent = dirname(current);\n if (parent === current) {\n throw new Error(\n `not inside a git repository (searched up from ${startFrom})`,\n );\n }\n current = parent;\n }\n}\n\nexport function stampConfigDir(repoRoot: string): string {\n return join(repoRoot, \".stamp\");\n}\n\nexport function stampReviewersDir(repoRoot: string): string {\n return join(repoRoot, \".stamp\", \"reviewers\");\n}\n\nexport function stampTrustedKeysDir(repoRoot: string): string {\n return join(repoRoot, \".stamp\", \"trusted-keys\");\n}\n\nexport function stampConfigFile(repoRoot: string): string {\n return join(repoRoot, \".stamp\", \"config.yml\");\n}\n\nexport function stampStateDbPath(repoRoot: string): string {\n return join(gitCommonDir(repoRoot), \"stamp\", \"state.db\");\n}\n\n/**\n * Marker file that records \"we have shown the LLM data-flow notice in this\n * repo at least once.\" Lives next to state.db under the git common dir so\n * it's per-repo (not per-worktree, not committed).\n */\nexport function stampLlmNoticeMarkerPath(repoRoot: string): string {\n return join(gitCommonDir(repoRoot), \"stamp\", \"llm-notice-shown\");\n}\n\n/**\n * Resolve the git common directory for `repoRoot`. For a normal checkout this\n * is `<repoRoot>/.git`; for a worktree, `<repoRoot>/.git` is a *file* of the\n * form `gitdir: <path>` and the real common dir lives at `<gitdir>/commondir`\n * (a path relative to gitdir, typically `../..`). Mirrors `git rev-parse\n * --git-common-dir` without spawning git.\n *\n * State that should be shared across every worktree of one repository (review\n * verdicts, the per-machine sqlite db) lives under this common dir, so callers\n * resolve their paths through here rather than hard-coding `<repoRoot>/.git`.\n */\nexport function gitCommonDir(repoRoot: string): string {\n const dotGit = join(repoRoot, \".git\");\n const st = statSync(dotGit);\n if (st.isDirectory()) return dotGit;\n\n // Worktree (or submodule): `.git` is a file. Parse the `gitdir:` line, then\n // follow the `commondir` pointer from there. Submodules have no `commondir`,\n // so the gitdir itself is the writable common dir — fall through to that.\n const contents = readFileSync(dotGit, \"utf8\");\n const match = contents.match(/^gitdir:\\s*(.+)$/m);\n if (!match || !match[1]) {\n throw new Error(\n `expected '.git' at ${repoRoot} to be a directory or a 'gitdir:' pointer file, got: ${contents.slice(0, 120)}`,\n );\n }\n const gitdirRaw = match[1].trim();\n const gitdir = isAbsolute(gitdirRaw) ? gitdirRaw : resolve(repoRoot, gitdirRaw);\n\n const commondirPath = join(gitdir, \"commondir\");\n if (!existsSync(commondirPath)) return gitdir;\n const commondirRaw = readFileSync(commondirPath, \"utf8\").trim();\n return isAbsolute(commondirRaw) ? commondirRaw : resolve(gitdir, commondirRaw);\n}\n\nexport function userKeysDir(): string {\n return join(homedir(), \".stamp\", \"keys\");\n}\n\n/**\n * Per-user stamp-server config. Holds {host, port, user, repo_root_prefix}\n * so commands like `stamp provision` can reach the operator's stamp server\n * without making the agent guess at SSH endpoints.\n */\nexport function userServerConfigPath(): string {\n return join(homedir(), \".stamp\", \"server.yml\");\n}\n\n/**\n * Per-user stamp config. Today holds reviewer-model selections; structured\n * as a top-level object so future per-user knobs (telemetry sinks, default\n * timeouts, etc.) can land alongside without renaming the file. Lives\n * separately from per-repo `.stamp/config.yml` because cost/speed is\n * operator infrastructure rather than committed review policy — different\n * operators on the same repo are free to pick different models without\n * a merge-conflict over preference, and this file is intentionally\n * EXCLUDED from the v3 reviewer attestation hash chain.\n */\nexport function userConfigPath(): string {\n return join(homedir(), \".stamp\", \"config.yml\");\n}\n\nexport function ensureDir(path: string, mode = 0o755): void {\n if (!existsSync(path)) {\n mkdirSync(path, { recursive: true, mode });\n }\n}\n\nexport function isFile(path: string): boolean {\n try {\n return statSync(path).isFile();\n } catch {\n return false;\n }\n}\n","/**\n * Boot-time entrypoint: ensure the server's Ed25519 review-signing key\n * exists and (on first generation) advertise its fingerprint loudly so\n * the operator can commit it to `.stamp/trusted-keys/manifest.yml`.\n *\n * Runs once per container boot, as root, from /entrypoint.sh — after\n * stamp-seed-users, before sshd is exec'd. The bootstrap is gated on\n * `ANTHROPIC_API_KEY`: if the env var is unset, review capability is\n * disabled by design and we skip the keygen entirely. This matches the\n * design doc's contract (\"Absent ANTHROPIC_API_KEY → stamp-server runs\n * as today, rejecting review requests with a clear 'review capability\n * not configured' error. Reviews are opt-in.\").\n *\n * Path resolution (in priority order):\n * 1. REVIEW_SIGNING_KEY_PATH env var — full path override, lets\n * operators stash the key in a platform-specific mount (secrets\n * manager fuse, etc.) without touching code.\n * 2. $STAMP_STATE_DIR/review-signing-key.pem — the convention the\n * design doc names (\"review signing key lives at\n * $STATE_DIR/review-signing-key.pem\"). The shell var\n * STAMP_STATE_DIR is exported by entrypoint.sh and points at\n * /srv/git/.stamp-state on the standard container; we honor it\n * so a future entrypoint that moves the state dir doesn't\n * require an in-code update here.\n * 3. /srv/git/.stamp-state/review-signing-key.pem — last-resort\n * default for invocations outside the container (rare; tests\n * always override path 1).\n *\n * Output streams follow the standard CLI convention: prose and\n * operational status (including the first-boot banner) go to stdout;\n * errors go to stderr with the `error: ` prefix. The first-boot\n * fingerprint advertisement is operator-instruction prose, not an\n * error, so it lives on stdout. On re-boot we print a one-line\n * \"reused existing key\" log so operators see something during normal\n * boots without re-spamming the loud block.\n *\n * Failure handling:\n * - ReviewSigningKeyError (wrong mode, unreadable, unparseable):\n * print to stderr with the `error: ` prefix and exit 1.\n * Entrypoint.sh treats this as a fatal startup failure — the\n * trust model requires a stable server identity, and silently\n * regenerating on a wrong-mode file would rotate that identity\n * without operator consent.\n * - Generic FS errors (write fail on volume not mounted, etc.):\n * bubble with original message + non-zero exit. These are\n * orchestration bugs the operator needs to see verbatim.\n */\n\nimport {\n ensureReviewSigningKey,\n resolveReviewSigningKeyPath,\n ReviewSigningKeyError,\n} from \"../lib/reviewSigningKey.js\";\n\n// Path resolution is shared with the pipeline's request-time loader\n// (`resolveReviewSigningKeyPath` in src/lib/reviewSigningKey.ts) so the\n// bootstrap and the pipeline can never disagree on which file is \"the\"\n// signing key. See that function's docstring for the precedence order.\n\nfunction printGeneratedBanner(fingerprint: string, publicKeyPath: string): void {\n // Visually distinct border + the instruction line that AC #2\n // requires. The instruction text matches the exact phrasing in the\n // ticket so operators searching their logs for the docs cue can\n // find it. Border uses U+2500 (BOX DRAWINGS LIGHT HORIZONTAL) to\n // match the established structural-marker convention used by\n // `stamp status`, `stamp review`, etc. Goes to stdout — this is\n // operator-instruction prose, not an error.\n const border =\n \"────────────────────────────────────────────────────────────────────\";\n const lines = [\n \"\",\n border,\n \" STAMP-SERVER: review-signing key generated (first boot)\",\n border,\n \" fingerprint: \" + fingerprint,\n \" public key: \" + publicKeyPath,\n \"\",\n \" Next step — commit this fingerprint to\",\n \" `.stamp/trusted-keys/manifest.yml` with `capabilities: [server]`\",\n \" in every repo that delegates reviews to this server.\",\n \"\",\n \" See `docs/plans/server-attested-reviews.md` (Trust model section)\",\n \" for the manifest entry format. The pubkey is also fetchable via\",\n \" `ssh git@<host> stamp-server-pubkey --review-signing`.\",\n border,\n \"\",\n ];\n process.stdout.write(lines.join(\"\\n\"));\n}\n\nfunction main(): void {\n const apiKey = process.env[\"ANTHROPIC_API_KEY\"];\n if (!apiKey || apiKey.length === 0) {\n // Review capability disabled — design contract says do nothing.\n // We still emit a brief log line so operators investigating \"why\n // is there no review key on disk?\" can find the explanation in\n // boot logs.\n console.log(\n \"stamp-bootstrap-review-key: ANTHROPIC_API_KEY unset; review capability disabled, skipping keygen\",\n );\n return;\n }\n\n const privateKeyPath = resolveReviewSigningKeyPath();\n\n let result;\n try {\n result = ensureReviewSigningKey({ privateKeyPath });\n } catch (err) {\n if (err instanceof ReviewSigningKeyError) {\n // stderr + lowercase `error: ` prefix per the codebase\n // convention; the domain-specific message body (which path,\n // what's wrong, what to do) is already constructed by\n // ReviewSigningKeyError.\n process.stderr.write(\"error: \" + err.message + \"\\n\");\n process.exit(1);\n }\n throw err;\n }\n\n if (result.created) {\n printGeneratedBanner(result.fingerprint, result.publicKeyPath);\n } else {\n console.log(\n `stamp-bootstrap-review-key: reusing existing review-signing key at ${result.privateKeyPath} (fingerprint: ${result.fingerprint})`,\n );\n }\n}\n\nmain();\n"],"mappings":"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","names":["import_node_crypto","import_node_fs","import_node_path","import_node_fs","import_node_path"]}
|