@rightkit/release 0.2.135 → 0.2.137
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 +62 -0
- package/addon-command.mjs +53 -34
- package/apple-profile-renew.mjs +30 -12
- package/artifact-storage.mjs +47 -10
- package/background-process.mjs +18 -0
- package/build-release.mjs +1 -1
- package/cache-command.mjs +4 -3
- package/cargo-contract.mjs +1 -1
- package/cargo-guard.mjs +4 -3
- package/cargo-registry-readback.mjs +51 -0
- package/cargo-target.mjs +3 -1
- package/cli/right-release.mjs +26 -0
- package/codesign-environment.mjs +26 -2
- package/codesign-shim.mjs +2 -1
- package/committed-source.mjs +3 -3
- package/development.mjs +7 -7
- package/direct-bootstrap.mjs +19 -9
- package/dmg-build.mjs +117 -0
- package/dmg-layout.mjs +123 -0
- package/github-release.mjs +74 -37
- package/heavy-command.mjs +3 -2
- package/install-app.mjs +358 -0
- package/inventory-check.mjs +980 -0
- package/macos-signing-session.mjs +3 -0
- package/managed-cargo.mjs +23 -19
- package/managed-host-secrets.mjs +30 -0
- package/managed-roots.mjs +39 -0
- package/model-promote.mjs +31 -37
- package/notary-auth.mjs +4 -0
- package/osascript-guard.mjs +24 -0
- package/ownership-approval-command.mjs +15 -4
- package/ownership-approval.mjs +114 -49
- package/ownership-availability.mjs +25 -0
- package/ownership-catalog-config.json +18 -0
- package/ownership-catalog.mjs +94 -0
- package/ownership-command.mjs +20 -10
- package/ownership-declaration.json +15 -0
- package/ownership-default-policy.json +1 -11
- package/ownership-evidence.mjs +10 -8
- package/ownership-git-inventory.mjs +29 -1
- package/ownership-helper-paths.json +2 -2
- package/ownership-hooks.mjs +107 -0
- package/ownership-internal-owners.json +44 -0
- package/ownership-key-authority.mjs +112 -7
- package/ownership-key-command.mjs +76 -19
- package/ownership-key-trust.mjs +97 -9
- package/ownership-lockfiles.mjs +311 -0
- package/ownership-report-command.mjs +12 -3
- package/ownership-report.mjs +20 -17
- package/ownership-repositories.json +19 -0
- package/ownership-scan-worker.mjs +96 -11
- package/ownership-source.mjs +39 -0
- package/ownership-staged.mjs +193 -0
- package/ownership-verdict.mjs +74 -0
- package/ownership.mjs +215 -107
- package/package.json +1 -1
- package/protected-credentials.mjs +111 -0
- package/prune-r2.mjs +4 -3
- package/pub-upload.mjs +3 -85
- package/publication-artifacts.mjs +108 -0
- package/publication-gate.mjs +28 -7
- package/publish-cargo.mjs +47 -16
- package/publish-swift.mjs +46 -17
- package/publish-update.mjs +61 -39
- package/r2-verified-upload.mjs +41 -0
- package/registry-parity.mjs +5 -3
- package/release-state.mjs +1 -0
- package/release-token.mjs +2 -0
- package/rightkit-drift.mjs +3 -2
- package/rightkit-ownership.json +2028 -139
- package/rightkit-signing-agent.mjs +5 -3
- package/rightkit-sync.mjs +6 -2
- package/rightkit-versions.json +215 -103
- package/runtime-artifact-manifest.mjs +20 -6
- package/sign-updater.mjs +5 -0
- package/signing-agent-client.mjs +2 -1
- package/signing-agent-install.mjs +3 -2
- package/standalone-clone-verify.mjs +3 -2
- package/tauri-bundle-marker.mjs +3 -2
- package/upload-large.mjs +36 -82
- package/verifier.mjs +5 -1
|
@@ -7,6 +7,7 @@ import { sha256 } from "./ownership-approval.mjs";
|
|
|
7
7
|
|
|
8
8
|
const MAX_LIST_BYTES = 32 * 1024 * 1024;
|
|
9
9
|
const MAX_BLOB_BATCH_BYTES = 256 * 1024 * 1024;
|
|
10
|
+
const BLOB_CHUNK_BYTES = 32 * 1024 * 1024;
|
|
10
11
|
const DEFAULT_MAX_FILE_BYTES = 2 * 1024 * 1024;
|
|
11
12
|
const CACHE_LIMIT = 128 * 1024 * 1024;
|
|
12
13
|
const blobCache = new Map();
|
|
@@ -43,10 +44,37 @@ function decodePath(bytes) {
|
|
|
43
44
|
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
44
45
|
}
|
|
45
46
|
|
|
47
|
+
function gitBlobSizes(root, repositoryRoot, oids) {
|
|
48
|
+
const lines = runGit(root, repositoryRoot, ["cat-file", "--batch-check"], { input: Buffer.from(`${oids.join("\n")}\n`) }).toString("ascii").split("\n");
|
|
49
|
+
if (lines.pop() !== "" || lines.length !== oids.length) throw new Error("ownership inventory: malformed Git blob size response");
|
|
50
|
+
return lines.map((line, index) => {
|
|
51
|
+
const [oid, type, sizeText] = line.split(" "), size = Number(sizeText);
|
|
52
|
+
if (oid !== oids[index] || type !== "blob" || !Number.isSafeInteger(size) || size < 0 || size > MAX_BLOB_BATCH_BYTES) throw new Error("ownership inventory: invalid Git blob");
|
|
53
|
+
return size;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Chunk by blob size so a whole-repository read never exceeds one spawn's output buffer.
|
|
46
58
|
function readGitBlobs(root, repositoryRoot, oids) {
|
|
47
59
|
if (!oids.length) return new Map();
|
|
60
|
+
const sizes = gitBlobSizes(root, repositoryRoot, oids), result = new Map();
|
|
61
|
+
let chunk = [], chunkBytes = 0;
|
|
62
|
+
const flush = () => {
|
|
63
|
+
for (const [oid, bytes] of readGitBlobChunk(root, repositoryRoot, chunk, chunkBytes)) result.set(oid, bytes);
|
|
64
|
+
chunk = []; chunkBytes = 0;
|
|
65
|
+
};
|
|
66
|
+
oids.forEach((oid, index) => {
|
|
67
|
+
const cost = sizes[index] + 128;
|
|
68
|
+
if (chunk.length && chunkBytes + cost > BLOB_CHUNK_BYTES) flush();
|
|
69
|
+
chunk.push(oid); chunkBytes += cost;
|
|
70
|
+
});
|
|
71
|
+
if (chunk.length) flush();
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function readGitBlobChunk(root, repositoryRoot, oids, expectedBytes) {
|
|
48
76
|
const input = Buffer.from(`${oids.join("\n")}\n`);
|
|
49
|
-
const output = runGit(root, repositoryRoot, ["cat-file", "--batch"], { input, maxBuffer: MAX_BLOB_BATCH_BYTES });
|
|
77
|
+
const output = runGit(root, repositoryRoot, ["cat-file", "--batch"], { input, maxBuffer: Math.min(MAX_BLOB_BATCH_BYTES, expectedBytes + 1024 * 1024) });
|
|
50
78
|
const result = new Map(); let offset = 0;
|
|
51
79
|
for (const expected of oids) {
|
|
52
80
|
const lineEnd = output.indexOf(0x0a, offset);
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema": 1,
|
|
3
3
|
"windows": {
|
|
4
|
-
"path": "C:\\Users\\adrds\\.rightkit-managed\\ownership-approval\\windows\\content-
|
|
5
|
-
"sha256": "
|
|
4
|
+
"path": "C:\\Users\\adrds\\.rightkit-managed\\ownership-approval\\windows\\content-6df7a0a80c70cc9b9a66a9c30d5956500d505f8d15f9c98563a4e8e63a60f12f\\RightKit.OwnershipApproval.Windows.exe",
|
|
5
|
+
"sha256": "6df7a0a80c70cc9b9a66a9c30d5956500d505f8d15f9c98563a4e8e63a60f12f"
|
|
6
6
|
},
|
|
7
7
|
"macos": {
|
|
8
8
|
"path": "/Users/adrdsouza/.rightkit-managed/ownership-approval/macos/content-2c30864bdb666a1ca1e4388ea7a3bc83cb985196d666b96cc30e1da7cd5b0e6b/RightKitOwnershipApproval.app/Contents/MacOS/ownership-approval",
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// right-release hooks install: writes the advisory pre-commit ownership gate into an app repository.
|
|
2
|
+
//
|
|
3
|
+
// right-release hooks install [--app <dir>]
|
|
4
|
+
//
|
|
5
|
+
// The hook runs the app's pinned @rightkit/release (node_modules) via `right-release ownership --staged`.
|
|
6
|
+
// An existing foreign pre-commit is preserved as pre-commit.rightkit-foreign and chained first.
|
|
7
|
+
// The gate is advisory: `git commit --no-verify` bypasses it; broker admission and the release gate
|
|
8
|
+
// remain authoritative.
|
|
9
|
+
import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, renameSync, writeFileSync } from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
import { spawnSync } from "./background-process.mjs";
|
|
13
|
+
import { repositoryRootFor } from "./ownership-staged.mjs";
|
|
14
|
+
|
|
15
|
+
export const HOOK_MARKER = "rightkit-ownership-precommit v1";
|
|
16
|
+
const FOREIGN = "pre-commit.rightkit-foreign";
|
|
17
|
+
const PINNED_CLI = path.join("node_modules", "@rightkit", "release", "cli", "right-release.mjs");
|
|
18
|
+
|
|
19
|
+
export function renderPreCommitHook() {
|
|
20
|
+
return [
|
|
21
|
+
"#!/bin/sh",
|
|
22
|
+
`# ${HOOK_MARKER}: advisory ownership gate installed by \`right-release hooks install\`.`,
|
|
23
|
+
"# Block-tier findings without a valid exception fail here; --no-verify bypasses this gate.",
|
|
24
|
+
"# Authoritative enforcement remains in broker admission and the release gate.",
|
|
25
|
+
"ROOT=\"$(git rev-parse --show-toplevel)\" || exit 1",
|
|
26
|
+
"HOOKS_DIR=\"$(dirname \"$0\")\"",
|
|
27
|
+
`if [ -x "$HOOKS_DIR/${FOREIGN}" ]; then`,
|
|
28
|
+
` "$HOOKS_DIR/${FOREIGN}" "$@" || exit $?`,
|
|
29
|
+
"fi",
|
|
30
|
+
`GATE="$ROOT/${PINNED_CLI.split(path.sep).join("/")}"`,
|
|
31
|
+
"if [ ! -f \"$GATE\" ]; then",
|
|
32
|
+
" echo \"[rightkit] @rightkit/release is not installed in this repository; add it as a devDependency, then re-run: right-release hooks install\" >&2",
|
|
33
|
+
" exit 1",
|
|
34
|
+
"fi",
|
|
35
|
+
"exec node \"$GATE\" ownership --staged",
|
|
36
|
+
"",
|
|
37
|
+
].join("\n");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function git(cwd, args) {
|
|
41
|
+
const result = spawnSync("git", ["-C", cwd, ...args], { encoding: "utf8", windowsHide: true, env: Object.fromEntries(Object.entries(process.env).filter(([k]) => !/^GIT_/i.test(k))) });
|
|
42
|
+
if (result.status !== 0) return null;
|
|
43
|
+
return String(result.stdout).trim();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The directory git runs hooks from: core.hooksPath when configured, else the common hooks directory. */
|
|
47
|
+
export function hooksDirectoryFor(root) {
|
|
48
|
+
const configured = git(root, ["config", "--get", "core.hooksPath"]);
|
|
49
|
+
if (configured) return path.resolve(root, configured);
|
|
50
|
+
const common = git(root, ["rev-parse", "--git-path", "hooks"]);
|
|
51
|
+
if (!common) throw new Error("could not resolve the repository hooks directory");
|
|
52
|
+
return path.resolve(root, common);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Idempotent install. Returns { status: "installed" | "updated" | "unchanged" | "chained", file }.
|
|
57
|
+
*/
|
|
58
|
+
export function installPreCommitHook(repoDir) {
|
|
59
|
+
const root = repositoryRootFor(repoDir);
|
|
60
|
+
const pinned = path.join(root, PINNED_CLI);
|
|
61
|
+
if (!existsSync(pinned)) throw new Error(`@rightkit/release is not installed at ${pinned}; add it as a devDependency and run pnpm install first`);
|
|
62
|
+
const hooksDir = hooksDirectoryFor(root);
|
|
63
|
+
const file = path.join(hooksDir, "pre-commit");
|
|
64
|
+
const foreign = path.join(hooksDir, FOREIGN);
|
|
65
|
+
const body = renderPreCommitHook();
|
|
66
|
+
mkdirSync(hooksDir, { recursive: true });
|
|
67
|
+
let status = "installed";
|
|
68
|
+
if (lstatSync(file, { throwIfNoEntry: false })) {
|
|
69
|
+
const current = readFileSync(file, "utf8");
|
|
70
|
+
if (current === body) return { status: "unchanged", file };
|
|
71
|
+
if (current.includes(HOOK_MARKER)) status = "updated";
|
|
72
|
+
else {
|
|
73
|
+
if (existsSync(path.join(hooksDir, FOREIGN))) throw new Error(`${FOREIGN} already exists; resolve the chained hook manually before installing`);
|
|
74
|
+
renameSync(file, foreign);
|
|
75
|
+
status = "chained";
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const temporary = `${file}.${process.pid}.tmp`;
|
|
79
|
+
writeFileSync(temporary, body, { mode: 0o755 });
|
|
80
|
+
chmodSync(temporary, 0o755);
|
|
81
|
+
renameSync(temporary, file);
|
|
82
|
+
return { status, file };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function main(args, { print = (line) => console.log(line) } = {}) {
|
|
86
|
+
const flag = (name) => { const i = args.indexOf(name); return i >= 0 ? args[i + 1] : undefined; };
|
|
87
|
+
if (args[0] !== "install") {
|
|
88
|
+
print("right-release hooks install [--app <dir>]");
|
|
89
|
+
return args.includes("--help") ? 0 : 2;
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
const result = installPreCommitHook(path.resolve(flag("--app") ?? process.cwd()));
|
|
93
|
+
print(`[right-release] pre-commit ownership gate ${result.status}: ${result.file}`);
|
|
94
|
+
if (result.status === "chained") print(`[right-release] previous pre-commit preserved as ${FOREIGN} and runs first`);
|
|
95
|
+
return 0;
|
|
96
|
+
} catch (error) {
|
|
97
|
+
print(`[right-release] hooks install FAILED: ${error.message}`);
|
|
98
|
+
return 1;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const isEntry = (() => {
|
|
103
|
+
if (!process.argv[1]) return false;
|
|
104
|
+
try { return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url)); }
|
|
105
|
+
catch { return false; }
|
|
106
|
+
})();
|
|
107
|
+
if (isEntry) process.exitCode = main(process.argv.slice(2));
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"owners": [
|
|
3
|
+
{
|
|
4
|
+
"package": "@rightkit/build-control-broker",
|
|
5
|
+
"path": "packages/build-control-broker/package.json",
|
|
6
|
+
"reason": "Private build-control infrastructure; not an application capability"
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"package": "@rightkit/build-control-clients",
|
|
10
|
+
"path": "packages/build-control-clients/package.json",
|
|
11
|
+
"reason": "Private build-control infrastructure; not an application capability"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"package": "@rightkit/build-control-core",
|
|
15
|
+
"path": "packages/build-control-core/package.json",
|
|
16
|
+
"reason": "Private build-control infrastructure; not an application capability"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"package": "@rightkit/build-control-phase0",
|
|
20
|
+
"path": "packages/build-control-phase0/package.json",
|
|
21
|
+
"reason": "Private build-control infrastructure; not an application capability"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"package": "@rightkit/build-control-policy",
|
|
25
|
+
"path": "packages/build-control-policy/package.json",
|
|
26
|
+
"reason": "Private build-control infrastructure; not an application capability"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"package": "@rightkit/build-control-staging",
|
|
30
|
+
"path": "packages/build-control-staging/package.json",
|
|
31
|
+
"reason": "Private build-control infrastructure; not an application capability"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"package": "@rightkit/build-control-phase0-macos",
|
|
35
|
+
"path": "packages/build-control-phase0/macos/package.json",
|
|
36
|
+
"reason": "Private native build-control feasibility fixture"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"package": "@rightkit/build-control-phase0-windows",
|
|
40
|
+
"path": "packages/build-control-phase0/windows/package.json",
|
|
41
|
+
"reason": "Private native build-control feasibility fixture"
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
}
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { realpathSync, statSync } from "node:fs";
|
|
2
|
+
import { closeSync, constants, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
4
5
|
import { spawnSync } from "./background-process.mjs";
|
|
5
|
-
import { canonicalRepositoryIdentity, resolveKeyLogTrust } from "./ownership-approval.mjs";
|
|
6
|
+
import { canonicalRepositoryIdentity, resolveKeyLogTrust, trustedPath, __testOnly as approvalTestOnly } from "./ownership-approval.mjs";
|
|
6
7
|
|
|
8
|
+
import { performProtectedTrustUpdate } from "./ownership-key-trust.mjs";
|
|
9
|
+
|
|
10
|
+
const HEX = /^[a-f0-9]{64}$/;
|
|
7
11
|
const LOG_PATH = ".rightkit/ownership-keys.json";
|
|
8
12
|
const MAX_LOG_BYTES = 2 * 1024 * 1024;
|
|
9
13
|
const executable = process.platform === "win32" ? "C:\\Program Files\\Git\\cmd\\git.exe" : "/usr/bin/git";
|
|
@@ -38,9 +42,83 @@ function validateAuthority(authority) {
|
|
|
38
42
|
return { repositoryIdentity, checkoutPath };
|
|
39
43
|
}
|
|
40
44
|
|
|
45
|
+
function checkpointPath(file, rootFingerprint) {
|
|
46
|
+
if (!HEX.test(rootFingerprint ?? "")) throw new Error("protected checkpoint root fingerprint is malformed");
|
|
47
|
+
return path.join(path.dirname(file), "checkpoints", `${rootFingerprint}.json`);
|
|
48
|
+
}
|
|
49
|
+
function auditPath(file, requireNonElevated = true) {
|
|
50
|
+
const result = approvalTestOnly.protectedLocation(file, process.platform, { requireNonElevated });
|
|
51
|
+
if (!result.ok) throw new Error(`protected checkpoint audit failed: ${result.problems.join("; ")}`);
|
|
52
|
+
}
|
|
53
|
+
function boundedJson(file, audit) {
|
|
54
|
+
const st = lstatSync(file);
|
|
55
|
+
if (!st.isFile() || st.isSymbolicLink() || st.size > MAX_LOG_BYTES) throw new Error("protected checkpoint input must be a bounded regular file");
|
|
56
|
+
audit(file);
|
|
57
|
+
return JSON.parse(readFileSync(file, "utf8"));
|
|
58
|
+
}
|
|
59
|
+
function validCheckpoint(value) {
|
|
60
|
+
return value && Object.keys(value).sort().join("\0") === "head\0sequence" && Number.isSafeInteger(value.sequence) && value.sequence >= 0 && HEX.test(value.head ?? "");
|
|
61
|
+
}
|
|
62
|
+
function resolveProtectedCheckpoint(trust, bytes, file, audit) {
|
|
63
|
+
const disk = boundedJson(file, audit);
|
|
64
|
+
if (!disk || disk.rootFingerprint !== trust.rootFingerprint || !validCheckpoint(disk.keyCheckpoint)) throw new Error("protected checkpoint trust pin is malformed or differs from reader pin");
|
|
65
|
+
const floors = [trust.keyCheckpoint, disk.keyCheckpoint];
|
|
66
|
+
const statePath = checkpointPath(file, trust.rootFingerprint);
|
|
67
|
+
let state;
|
|
68
|
+
try { state = boundedJson(statePath, audit); }
|
|
69
|
+
catch (error) { if (error.code !== "ENOENT") throw error; }
|
|
70
|
+
if (state) {
|
|
71
|
+
if (Object.keys(state).sort().join("\0") !== "keyCheckpoint\0rootFingerprint\0schema" || state.schema !== 1 || state.rootFingerprint !== trust.rootFingerprint || !validCheckpoint(state.keyCheckpoint)) throw new Error("protected checkpoint state is malformed");
|
|
72
|
+
floors.push(state.keyCheckpoint);
|
|
73
|
+
}
|
|
74
|
+
let resolved;
|
|
75
|
+
for (const keyCheckpoint of floors) {
|
|
76
|
+
resolved = resolveKeyLogTrust({ ...trust, keyCheckpoint }, bytes);
|
|
77
|
+
if (!resolved.ok) throw new Error(`authority key log verification failed: ${resolved.problems.join("; ")}`);
|
|
78
|
+
}
|
|
79
|
+
return { resolved, statePath, state };
|
|
80
|
+
}
|
|
81
|
+
function syncDirectory(directory) {
|
|
82
|
+
let fd;
|
|
83
|
+
try { fd = openSync(directory, "r"); fsyncSync(fd); }
|
|
84
|
+
catch (error) { if (process.platform !== "win32" || !["EISDIR", "EPERM"].includes(error.code)) throw error; }
|
|
85
|
+
finally { if (fd !== undefined) closeSync(fd); }
|
|
86
|
+
}
|
|
87
|
+
// Called only inside the existing elevated trust-update ceremony.
|
|
88
|
+
function persistCheckpoint(trust, bytes, file, audit) {
|
|
89
|
+
const directory = path.dirname(checkpointPath(file, trust.rootFingerprint));
|
|
90
|
+
audit(path.dirname(file));
|
|
91
|
+
try { mkdirSync(directory, { mode: 0o755 }); } catch (error) { if (error.code !== "EEXIST") throw error; }
|
|
92
|
+
if (!lstatSync(directory).isDirectory() || lstatSync(directory).isSymbolicLink()) throw new Error("protected checkpoint directory is missing or linked");
|
|
93
|
+
audit(directory);
|
|
94
|
+
const lock = path.join(directory, `${trust.rootFingerprint}.lock`);
|
|
95
|
+
let lockFd;
|
|
96
|
+
try { lockFd = openSync(lock, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0), 0o600); }
|
|
97
|
+
catch (error) { throw new Error(error.code === "EEXIST" ? "protected checkpoint state lock already exists; service/admin recovery is required" : `cannot lock protected checkpoint state: ${error.message}`); }
|
|
98
|
+
const temp = path.join(directory, `.${trust.rootFingerprint}.${process.pid}.${Date.now()}.tmp`);
|
|
99
|
+
let fd;
|
|
100
|
+
try {
|
|
101
|
+
writeFileSync(lockFd, `${process.pid}\n`); fsyncSync(lockFd);
|
|
102
|
+
const current = resolveProtectedCheckpoint(trust, bytes, file, audit);
|
|
103
|
+
const state = { schema: 1, rootFingerprint: trust.rootFingerprint, keyCheckpoint: current.resolved.checkpoint };
|
|
104
|
+
fd = openSync(temp, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0), 0o644);
|
|
105
|
+
writeFileSync(fd, `${JSON.stringify(state)}\n`); fsyncSync(fd); closeSync(fd); fd = undefined;
|
|
106
|
+
audit(temp);
|
|
107
|
+
renameSync(temp, current.statePath); syncDirectory(directory);
|
|
108
|
+
const readback = resolveProtectedCheckpoint(trust, bytes, file, audit);
|
|
109
|
+
if (readback.state?.keyCheckpoint.sequence !== state.keyCheckpoint.sequence || readback.state?.keyCheckpoint.head !== state.keyCheckpoint.head) throw new Error("protected checkpoint readback differs from verified log");
|
|
110
|
+
return state.keyCheckpoint;
|
|
111
|
+
} finally {
|
|
112
|
+
if (fd !== undefined) closeSync(fd);
|
|
113
|
+
try { unlinkSync(temp); } catch (error) { if (error.code !== "ENOENT") throw error; }
|
|
114
|
+
closeSync(lockFd); unlinkSync(lock); syncDirectory(directory);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
41
117
|
/** Read one exact key-log blob from the protected authority checkout's Git HEAD. */
|
|
42
|
-
export function readCommittedKeyAuthority(trust) {
|
|
118
|
+
export function readCommittedKeyAuthority(trust) { return readAuthority(trust); }
|
|
119
|
+
function readAuthority(trust, { file, audit = auditPath } = {}) {
|
|
43
120
|
try {
|
|
121
|
+
file ??= trustedPath();
|
|
44
122
|
const expected = validateAuthority(trust?.keyAuthority);
|
|
45
123
|
const top = String(git(expected.checkoutPath, ["rev-parse", "--show-toplevel"])).trim();
|
|
46
124
|
const normalizedTop = process.platform === "win32" ? path.resolve(top).toLowerCase() : path.resolve(top);
|
|
@@ -74,12 +152,39 @@ export function readCommittedKeyAuthority(trust) {
|
|
|
74
152
|
}
|
|
75
153
|
// Re-resolve on each call so current protected policy is always merged into
|
|
76
154
|
// the cached chain result; approval-policy changes never reuse stale trust.
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
155
|
+
const protectedState = resolveProtectedCheckpoint(trust, result.bytes, file, audit);
|
|
156
|
+
const resolved = protectedState.resolved;
|
|
157
|
+
if (!protectedState.state || protectedState.state.keyCheckpoint.sequence < resolved.checkpoint.sequence) throw new Error("checkpoint advance required: run the elevated ownership trust update on this host");
|
|
158
|
+
return { ok: true, problems: [], bytes: Buffer.from(result.bytes), digest: result.digest, rootFingerprint: result.rootFingerprint, keyCheckpoint: resolved.checkpoint, resolvedTrust: resolved.trust, authority: { repositoryIdentity: expected.repositoryIdentity, commit, tree } };
|
|
80
159
|
} catch (error) {
|
|
81
160
|
return { ok: false, problems: [error?.message ?? "protected key authority could not be read"] };
|
|
82
161
|
}
|
|
83
162
|
}
|
|
84
163
|
|
|
85
|
-
export const __testOnly = Object.freeze({ MAX_LOG_BYTES });
|
|
164
|
+
export const __testOnly = Object.freeze({ MAX_LOG_BYTES, checkpointPath, readAuthority, persistCheckpoint });
|
|
165
|
+
|
|
166
|
+
// Wrapper preserves the existing updater's elevation, request digest, lock & policy checks.
|
|
167
|
+
const realOrSelf = (file) => { try { return realpathSync(file); } catch { return path.resolve(file); } };
|
|
168
|
+
if (process.argv[1] && realOrSelf(process.argv[1]) === realOrSelf(fileURLToPath(import.meta.url))) {
|
|
169
|
+
try {
|
|
170
|
+
if (process.argv.length !== 8 || process.argv[2] !== "--trust-script" || process.argv[4] !== "--request" || process.argv[6] !== "--sha256") throw new Error("usage: ownership-key-authority.mjs --trust-script <installed-script> --request <absolute-json> --sha256 <digest>");
|
|
171
|
+
const script = path.join(path.dirname(fileURLToPath(import.meta.url)), "ownership-key-trust.mjs");
|
|
172
|
+
if (path.resolve(process.argv[3]) !== script || realpathSync(script) !== script) throw new Error("protected trust updater must be the installed sibling script");
|
|
173
|
+
const requestPath = process.argv[5], digest = process.argv[7];
|
|
174
|
+
if (!path.isAbsolute(requestPath) || !HEX.test(digest ?? "")) throw new Error("checkpoint update request path or SHA-256 is malformed");
|
|
175
|
+
const st = lstatSync(requestPath);
|
|
176
|
+
if (!st.isFile() || st.isSymbolicLink() || st.size > MAX_LOG_BYTES) throw new Error("checkpoint update request must be a bounded regular file");
|
|
177
|
+
const bytes = readFileSync(requestPath);
|
|
178
|
+
if (createHash("sha256").update(bytes).digest("hex") !== digest) throw new Error("checkpoint update request SHA-256 mismatch");
|
|
179
|
+
const request = JSON.parse(bytes.toString("utf8"));
|
|
180
|
+
if (!request || request.schema !== 1 || Object.keys(request).some((key) => !["schema", "log", "rootFingerprint", "baseline", "authority"].includes(key))) throw new Error("checkpoint update request schema is malformed");
|
|
181
|
+
const file = trustedPath();
|
|
182
|
+
// Check existing sidecar before the updater changes protected trust.
|
|
183
|
+
try { const existing = boundedJson(file, (target) => auditPath(target, false)); resolveProtectedCheckpoint(existing, Buffer.from(JSON.stringify(request.log)), file, (target) => auditPath(target, false)); }
|
|
184
|
+
catch (error) { if (error.code !== "ENOENT") throw error; }
|
|
185
|
+
const updated = performProtectedTrustUpdate(request);
|
|
186
|
+
const checkpoint = persistCheckpoint(updated.trust, Buffer.from(JSON.stringify(request.log)), file, (target) => auditPath(target, false));
|
|
187
|
+
for (const item of updated.warnings) process.stderr.write(`${item.code}: ${item.message}\n`);
|
|
188
|
+
process.stdout.write(`${JSON.stringify({ path: file, checkpoint, warnings: updated.warnings })}\n`);
|
|
189
|
+
} catch (error) { process.stderr.write(`${error.message}\n`); process.exitCode = 2; }
|
|
190
|
+
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { accessSync, closeSync, existsSync, lstatSync, mkdirSync, mkdtempSync, openSync, readdirSync, readFileSync, renameSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { accessSync, closeSync, existsSync, lstatSync, mkdirSync, mkdtempSync, openSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { spawnSync } from "./background-process.mjs";
|
|
6
|
+
import { spawnSync, spawnSyncInteractive, windowsPowerShellEnvironment } from "./background-process.mjs";
|
|
7
7
|
import { createKeyEvent, keyFingerprint, verifyKeyLog, verifyKeyRequest } from "./ownership-key-log.mjs";
|
|
8
|
-
import { canonicalRepositoryIdentity, loadTrustedExternal, resolveKeyLogTrust } from "./ownership-approval.mjs";
|
|
8
|
+
import { canonicalRepositoryIdentity, DEFAULT_TRUST_PATH, loadTrustedExternal, resolveKeyLogTrust, __testOnly as approvalTestOnly } from "./ownership-approval.mjs";
|
|
9
9
|
import { readCommittedKeyAuthority } from "./ownership-key-authority.mjs";
|
|
10
10
|
|
|
11
11
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -235,15 +235,16 @@ function helperFor(run) {
|
|
|
235
235
|
if (sha(bytes) !== item.sha256) throw new Error("ownership native helper hash does not match pinned config");
|
|
236
236
|
return item.path;
|
|
237
237
|
}
|
|
238
|
-
function invokeHelper(run, helper, args) {
|
|
239
|
-
const
|
|
238
|
+
function invokeHelper(run, helper, args, interactiveRun = run) {
|
|
239
|
+
const interactive = ["--key-operation", "--enroll", "--enroll-root"].includes(args[0]);
|
|
240
|
+
const r = interactive ? interactiveRun(helper, args, { shell: false, windowsHide: false }) : run(helper, args);
|
|
240
241
|
if (r.status !== 0 || String(r.stderr ?? "").includes("RightKit ownership keychain preflight:")) {
|
|
241
242
|
throw new Error(String(r.stderr || "native ownership operation cancelled or failed").trim());
|
|
242
243
|
}
|
|
243
244
|
return String(r.stdout ?? "");
|
|
244
245
|
}
|
|
245
246
|
function helperFile(run, helper, args, outPath, what) {
|
|
246
|
-
invokeHelper(run, helper, args);
|
|
247
|
+
invokeHelper(run, helper, args, run.interactive ?? run);
|
|
247
248
|
return parse(readFileSync(outPath, "utf8"), what);
|
|
248
249
|
}
|
|
249
250
|
function readRequestFile(file) {
|
|
@@ -257,29 +258,66 @@ function validateNative(out, expectedHost = platform) {
|
|
|
257
258
|
if (out.keyId !== `${expectedHost === "windows" ? "win" : "mac"}-${fingerprint}`) throw new Error("native helper key id does not match its public key");
|
|
258
259
|
return { keyId: fingerprint, publicKeyPem: out.publicKeyPem, platform: expectedHost };
|
|
259
260
|
}
|
|
260
|
-
function elevate(run, requestPath, digest) {
|
|
261
|
-
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
const
|
|
261
|
+
function elevate(run, requestPath, digest, { modulePath = fileURLToPath(import.meta.url), nodeExe = process.execPath, hostPlatform = process.platform, audit = (target) => approvalTestOnly.protectedLocation(target, hostPlatform) } = {}) {
|
|
262
|
+
let child, wrapper, registry, currentNode;
|
|
263
|
+
const refuse = (message) => { throw Object.assign(new Error(`RK_OWNERSHIP_ELEVATION_UNPROTECTED: ${message}`), { code: "RK_OWNERSHIP_ELEVATION_UNPROTECTED", type: "RK_OWNERSHIP_ELEVATION_UNPROTECTED" }); };
|
|
264
|
+
try {
|
|
265
|
+
if (!path.isAbsolute(requestPath) || !HEX.test(digest ?? "")) refuse("request path or SHA-256 is malformed");
|
|
266
|
+
const requestStat = lstatSync(requestPath);
|
|
267
|
+
if (!requestStat.isFile() || requestStat.isSymbolicLink() || (hostPlatform === "win32" && (requestStat.attributes & 0x400)) || requestStat.size > 2 * 1024 * 1024) refuse("request must be a bounded regular non-linked file");
|
|
268
|
+
if (sha(readFileSync(requestPath)) !== digest) refuse("request bytes do not match pinned SHA-256");
|
|
269
|
+
const trustPath = DEFAULT_TRUST_PATH[hostPlatform];
|
|
270
|
+
if (!trustPath) refuse(`ownership trust path is unavailable for ${hostPlatform}`);
|
|
271
|
+
const trustDirectory = path.dirname(trustPath);
|
|
272
|
+
for (const target of [trustDirectory, trustPath]) {
|
|
273
|
+
try {
|
|
274
|
+
const stat = lstatSync(target);
|
|
275
|
+
if (stat.isSymbolicLink() || (hostPlatform === "win32" && (stat.attributes & 0x400))) refuse(`protected trust path is linked: ${target}`);
|
|
276
|
+
if (target === trustDirectory && !stat.isDirectory()) refuse("protected trust location is not a directory");
|
|
277
|
+
if (target === trustPath && !stat.isFile()) refuse("protected trust file is not a regular file");
|
|
278
|
+
} catch (error) { if (error.code !== "ENOENT") throw error; }
|
|
279
|
+
}
|
|
280
|
+
const protectedTarget = existsSync(trustPath) ? trustPath : existsSync(trustDirectory) ? trustDirectory : null;
|
|
281
|
+
if (protectedTarget) {
|
|
282
|
+
const protection = audit(protectedTarget);
|
|
283
|
+
if (protection?.ok !== true) refuse((protection?.problems ?? ["protected trust location is unprotected"]).join("; "));
|
|
284
|
+
}
|
|
285
|
+
const source = realpathSync(modulePath);
|
|
286
|
+
const sourceDir = path.dirname(source);
|
|
287
|
+
child = path.join(sourceDir, "ownership-key-trust.mjs");
|
|
288
|
+
wrapper = path.join(sourceDir, "ownership-key-authority.mjs");
|
|
289
|
+
registry = path.join(sourceDir, "ownership-repositories.json");
|
|
290
|
+
currentNode = realpathSync(nodeExe);
|
|
291
|
+
for (const target of [source, child, wrapper, registry, currentNode]) {
|
|
292
|
+
const stat = lstatSync(target);
|
|
293
|
+
if (!stat.isFile() || stat.isSymbolicLink() || (hostPlatform === "win32" && (stat.attributes & 0x400)) || realpathSync(target) !== target) refuse(`elevated input must be a regular non-linked file: ${target}`);
|
|
294
|
+
}
|
|
295
|
+
} catch (error) {
|
|
296
|
+
if (error?.code === "RK_OWNERSHIP_ELEVATION_UNPROTECTED") throw error;
|
|
297
|
+
refuse(`installed ownership elevation cannot be verified: ${error.message}`);
|
|
298
|
+
}
|
|
299
|
+
const args = [wrapper, "--trust-script", child, "--request", requestPath, "--sha256", digest];
|
|
300
|
+
if (hostPlatform === "win32") {
|
|
301
|
+
const invocation = buildWindowsElevationInvocation(currentNode, child, requestPath, digest, wrapper);
|
|
302
|
+
const r = run(invocation.parentExecutable, invocation.parentArgs, { env: windowsPowerShellEnvironment() });
|
|
266
303
|
if (r.status !== 0) throw new Error("elevated trust update failed or was cancelled");
|
|
267
304
|
} else {
|
|
268
305
|
let r;
|
|
269
|
-
try { r = run("/usr/bin/sudo", ["-k",
|
|
306
|
+
try { r = run("/usr/bin/sudo", ["-k", currentNode, ...args], { stdio: "inherit", env: { ...process.env, NODE_OPTIONS: "", NODE_PATH: "" } }); }
|
|
270
307
|
finally { try { run("/usr/bin/sudo", ["-K"], { stdio: "inherit" }); } catch {} }
|
|
271
308
|
if (r.status !== 0) throw new Error("elevated trust update failed or was cancelled");
|
|
272
309
|
}
|
|
273
310
|
}
|
|
274
|
-
function buildWindowsElevationInvocation(nodeExe, trustScript, requestPath, digest) {
|
|
311
|
+
function buildWindowsElevationInvocation(nodeExe, trustScript, requestPath, digest, checkpointScript) {
|
|
275
312
|
const powershell = "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe";
|
|
276
313
|
const quote = (value) => `'${value.replaceAll("'", "''")}'`;
|
|
277
|
-
const
|
|
314
|
+
const childArgs = checkpointScript ? [nodeExe, checkpointScript, "--trust-script", trustScript, "--request", requestPath, "--sha256", digest] : [nodeExe, trustScript, "--request", requestPath, "--sha256", digest];
|
|
315
|
+
const childScript = `& ${childArgs.map(quote).join(" ")}; exit $LASTEXITCODE`;
|
|
278
316
|
const childEncoded = Buffer.from(childScript, "utf16le").toString("base64");
|
|
279
|
-
const parentScript = `$ErrorActionPreference='Stop'; try { $p=Start-Process -FilePath ${quote(powershell)} -ArgumentList ${quote(`-NoProfile -EncodedCommand ${childEncoded}`)} -Verb RunAs -WindowStyle Hidden -Wait -PassThru; exit $p.ExitCode } catch { exit 1 }`;
|
|
317
|
+
const parentScript = `$ErrorActionPreference='Stop'; try { $env:NODE_OPTIONS=$null; $env:NODE_PATH=$null; $p=Start-Process -FilePath ${quote(powershell)} -ArgumentList ${quote(`-NoProfile -EncodedCommand ${childEncoded}`)} -Verb RunAs -WindowStyle Hidden -Wait -PassThru; exit $p.ExitCode } catch { exit 1 }`;
|
|
280
318
|
return { parentExecutable: powershell, parentArgs: ["-NoProfile", "-EncodedCommand", Buffer.from(parentScript, "utf16le").toString("base64")], childScript };
|
|
281
319
|
}
|
|
282
|
-
export const __testOnly = Object.freeze({ buildWindowsElevationInvocation, assertSafeMetadataParents, detectMinimizeHook });
|
|
320
|
+
export const __testOnly = Object.freeze({ elevate, buildWindowsElevationInvocation, assertSafeMetadataParents, detectMinimizeHook });
|
|
283
321
|
function trustState() { return loadTrustedExternal(); }
|
|
284
322
|
function currentLogTrust(root, log, trust = trustState(), authority, injected = false) {
|
|
285
323
|
if (!trust.trust?.rootFingerprint) throw new Error("protected root trust is not enrolled on this host");
|
|
@@ -304,7 +342,10 @@ function makeTrustRequest(log, rootFingerprint, baseline, authority) {
|
|
|
304
342
|
}
|
|
305
343
|
|
|
306
344
|
export async function main(args, deps = {}) {
|
|
307
|
-
const
|
|
345
|
+
const runner = deps.runner ?? spawnSync;
|
|
346
|
+
const interactiveRun = deps.runner ?? spawnSyncInteractive;
|
|
347
|
+
const run = (...runArgs) => runner(...runArgs);
|
|
348
|
+
run.interactive = interactiveRun;
|
|
308
349
|
const print = deps.print ?? console.log;
|
|
309
350
|
const app = path.resolve(flag(args, "--app") ?? process.cwd());
|
|
310
351
|
const root = deps.root ?? repoRoot(app, run);
|
|
@@ -324,7 +365,8 @@ export async function main(args, deps = {}) {
|
|
|
324
365
|
if (verb === "enroll") return await enroll(args, { ...deps, run, print, root, helper, headSnapshot, transport, authority, hook });
|
|
325
366
|
if (verb === "approve-key") return await approve(args, { ...deps, run, print, root, helper, headSnapshot, transport, authority, hook });
|
|
326
367
|
if (verb === "revoke-key") return await revoke(args, { ...deps, run, print, root, helper, headSnapshot, transport, authority, hook });
|
|
327
|
-
|
|
368
|
+
if (verb === "trust-sync") return await trustSync(args, { ...deps, run, print, root, helper, headSnapshot, transport, authority, hook });
|
|
369
|
+
throw new Error("expected enroll, approve-key, revoke-key, or trust-sync");
|
|
328
370
|
} finally {
|
|
329
371
|
closeSync(lockFd);
|
|
330
372
|
rmSync(lockPath, { force: true });
|
|
@@ -439,6 +481,21 @@ async function resumeRootEnrollment(log, d) {
|
|
|
439
481
|
print(`Ownership root enrollment recovery completed: ${pin}; active keys: ${state.keys.length}`);
|
|
440
482
|
return 0;
|
|
441
483
|
}
|
|
484
|
+
/** Advance this host's protected checkpoint to the committed key log (after another host approved or revoked a key). */
|
|
485
|
+
async function trustSync(_args, d) {
|
|
486
|
+
const log = readLog(d.root);
|
|
487
|
+
const pin = log.events[0]?.payload?.rootFingerprint;
|
|
488
|
+
const state = verifyKeyLog(log, { rootFingerprint: pin });
|
|
489
|
+
if (!state.ok) throw new Error(`committed key log is invalid: ${state.problems.join("; ")}`);
|
|
490
|
+
const trust = (d.trustLoader ?? trustState)();
|
|
491
|
+
if (!trust?.trust?.rootFingerprint) throw new Error("protected root trust is not enrolled on this host; run ownership enroll");
|
|
492
|
+
if (trust.trust.rootFingerprint !== pin) throw new Error("committed key log root does not match this host's protected trust");
|
|
493
|
+
const baseline = d.baseline ?? parse(readFileSync(path.join(here, "ownership-default-policy.json"), "utf8"), "ownership default policy");
|
|
494
|
+
await installCheckpoint(log, pin, baseline, d);
|
|
495
|
+
postVerify(d.root, log, d);
|
|
496
|
+
d.print(`Ownership trust synced to key-log sequence ${log.events.length - 1}; active keys: ${state.keys.length}`);
|
|
497
|
+
return 0;
|
|
498
|
+
}
|
|
442
499
|
async function installCheckpoint(log, rootFingerprint, baseline, d) {
|
|
443
500
|
const request = makeTrustRequest(log, rootFingerprint, baseline, d.authority);
|
|
444
501
|
try { await (d.elevate ?? elevate)(d.run, request.file, request.digest); }
|