@rightkit/release 0.2.121 → 0.2.123
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/build-release.mjs +10 -2
- package/cli/right-release.mjs +12 -0
- package/committed-source.mjs +200 -0
- package/ownership-approval-command.mjs +39 -0
- package/ownership-approval.mjs +370 -0
- package/ownership-command.mjs +50 -14
- package/ownership-default-policy.json +21 -0
- package/ownership-evidence.mjs +224 -0
- package/ownership-git-inventory.mjs +195 -0
- package/ownership-helper-paths.json +11 -0
- package/ownership-key-authority.mjs +85 -0
- package/ownership-key-command.mjs +530 -0
- package/ownership-key-log.mjs +185 -0
- package/ownership-key-trust.mjs +326 -0
- package/ownership-report-command.mjs +87 -0
- package/ownership-report.mjs +144 -0
- package/ownership-scan-worker.mjs +15 -0
- package/ownership.mjs +504 -197
- package/package.json +4 -1
- package/preflight.mjs +19 -16
- package/publication-gate.mjs +2 -1
- package/publish-cargo.mjs +5 -0
- package/release.mjs +1 -1
- package/rightkit-drift.mjs +112 -0
- package/rightkit-ownership.json +131 -0
- package/rightkit-sync.mjs +150 -0
- package/rightkit-versions.json +111 -26
- package/upload-release.mjs +3 -0
package/build-release.mjs
CHANGED
|
@@ -239,6 +239,13 @@ try {
|
|
|
239
239
|
if (preflightBlockers.length > 0) {
|
|
240
240
|
fail(`preflight found ${preflightBlockers.length} blocking problem(s):\n${formatPreflight(preflightBlockers)}`);
|
|
241
241
|
}
|
|
242
|
+
const ownershipEvidence = preflight.find((check) => check.name === "ownership")?.evidence;
|
|
243
|
+
if (!ownershipEvidence) fail("ownership evidence was not produced by release preflight");
|
|
244
|
+
// Pending-enrollment advisory evidence asserts no commit (ownership-evidence.mjs); only
|
|
245
|
+
// committed-tree evidence must match the release commit.
|
|
246
|
+
if (ownershipEvidence.source !== "enrollment-pending-advisory" && ownershipEvidence.commit !== commit) {
|
|
247
|
+
fail("ownership scan source differs from release commit");
|
|
248
|
+
}
|
|
242
249
|
for (const check of preflight) {
|
|
243
250
|
if (check.status !== "ok") console.error(`[right-release] preflight ${check.name}: ${check.detail}`);
|
|
244
251
|
}
|
|
@@ -379,7 +386,7 @@ try {
|
|
|
379
386
|
});
|
|
380
387
|
},
|
|
381
388
|
seal: async ({ sealedDir }) => {
|
|
382
|
-
sealRelease({ configRoot: appRoot, managedCargoTarget, nativeLayout, sealedDir, releaseId, config, target, platform, commit, inputHashes, toolVersions, cacheKey, signingIdentity });
|
|
389
|
+
sealRelease({ configRoot: appRoot, managedCargoTarget, nativeLayout, sealedDir, releaseId, config, target, platform, commit, inputHashes, toolVersions, cacheKey, signingIdentity, ownershipEvidence });
|
|
383
390
|
verifySealedRelease(sealedDir);
|
|
384
391
|
if (cacheMode === "shared") markCacheEntrySuccessful({ layout: sharedLayout });
|
|
385
392
|
checkpoint(stateRoot, "sealed");
|
|
@@ -419,7 +426,7 @@ function sqlCipherFeatures(text) {
|
|
|
419
426
|
.filter((value) => /sqlcipher|openssl/i.test(value));
|
|
420
427
|
}
|
|
421
428
|
|
|
422
|
-
function sealRelease({ configRoot, managedCargoTarget, nativeLayout, sealedDir, releaseId, config, target, platform, commit, inputHashes, toolVersions, cacheKey, signingIdentity }) {
|
|
429
|
+
function sealRelease({ configRoot, managedCargoTarget, nativeLayout, sealedDir, releaseId, config, target, platform, commit, inputHashes, toolVersions, cacheKey, signingIdentity, ownershipEvidence }) {
|
|
423
430
|
// Every app's right-release.config.mjs names installer/updater artifacts
|
|
424
431
|
// relative to its resolved native target. On a broker-managed host that
|
|
425
432
|
// directory is never populated (the broker owns CARGO_TARGET_DIR and
|
|
@@ -482,6 +489,7 @@ function sealRelease({ configRoot, managedCargoTarget, nativeLayout, sealedDir,
|
|
|
482
489
|
app: config.app,
|
|
483
490
|
version: config.version,
|
|
484
491
|
commit,
|
|
492
|
+
ownershipEvidence,
|
|
485
493
|
platform,
|
|
486
494
|
cacheKey,
|
|
487
495
|
// Portable contracts sign only raw executables — installer/embedding
|
package/cli/right-release.mjs
CHANGED
|
@@ -23,6 +23,8 @@ const commands = new Map([
|
|
|
23
23
|
["github", "github-release.mjs"],
|
|
24
24
|
["cargo-target", "cargo-target.mjs"],
|
|
25
25
|
["ownership", "ownership-command.mjs"],
|
|
26
|
+
["ownership-approval", "ownership-approval-command.mjs"],
|
|
27
|
+
["ownership-report", "ownership-report-command.mjs"],
|
|
26
28
|
]);
|
|
27
29
|
|
|
28
30
|
const args = process.argv.slice(2);
|
|
@@ -171,6 +173,16 @@ Commands:
|
|
|
171
173
|
doctor --all Verify all Right Suite app release contracts
|
|
172
174
|
suite-doctor Verify all local Right Suite repositories
|
|
173
175
|
ownership [--app <dir>] [--json] Fail on app copies of RightKit-owned capabilities
|
|
176
|
+
ownership enroll [--root] [--name <device>] [--root-fingerprint <sha256>] [--app <dir>]
|
|
177
|
+
Enroll one native key or create a signed-device request
|
|
178
|
+
ownership approve-key <fingerprint|request.json> Approve one pending device key from an active host
|
|
179
|
+
ownership revoke-key <fingerprint> Revoke one active device key
|
|
180
|
+
ownership-approval --draft <input.json> [--out <draft.json>]
|
|
181
|
+
Draft an exception for native owner approval
|
|
182
|
+
ownership-approval --verify <signed.json> --context <scan.json>
|
|
183
|
+
Verify exact signed exception coverage
|
|
184
|
+
ownership-report --workspace <dir> [--markdown <file>] [--json-out <file>]
|
|
185
|
+
Report cross-app ownership & pending requests
|
|
174
186
|
cache status [--json] Inspect the shared local build cache
|
|
175
187
|
cache prune [--dry-run|--apply] [--json] Plan or apply safe shared-target pruning
|
|
176
188
|
cache migrate --config <file> [--dry-run|--apply] [--json]
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { spawnSync } from "./background-process.mjs";
|
|
5
|
+
|
|
6
|
+
const MAX_TREE_BYTES = 32 * 1024 * 1024;
|
|
7
|
+
const MAX_TOTAL_BLOB_BYTES = 128 * 1024 * 1024;
|
|
8
|
+
const MAX_BLOB_BYTES = 64 * 1024 * 1024;
|
|
9
|
+
const COMMAND_TIMEOUT_MS = 15_000;
|
|
10
|
+
const BLOB_TIMEOUT_MS = 30_000;
|
|
11
|
+
const acquiredSources = new WeakMap();
|
|
12
|
+
|
|
13
|
+
export class CommittedSourceIncompleteError extends Error {
|
|
14
|
+
constructor(message, cause) {
|
|
15
|
+
super(message, cause ? { cause } : undefined);
|
|
16
|
+
this.name = "CommittedSourceIncompleteError";
|
|
17
|
+
this.code = "COMMITTED_SOURCE_INCOMPLETE";
|
|
18
|
+
this.coverage = [{ path: ".", status: "unscanned", reason: "committed-source-acquisition-failed" }];
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function nativeGitPath(platform = process.platform) {
|
|
23
|
+
if (platform === "win32") return "C:\\Program Files\\Git\\cmd\\git.exe";
|
|
24
|
+
if (platform === "darwin") return "/usr/bin/git";
|
|
25
|
+
return "/usr/bin/git";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function gitEnv(env = process.env, platform = process.platform) {
|
|
29
|
+
const clean = Object.fromEntries(Object.entries(env).filter(([key]) => !/^GIT_/i.test(key)));
|
|
30
|
+
clean.GIT_CONFIG_NOSYSTEM = "1";
|
|
31
|
+
clean.GIT_CONFIG_GLOBAL = platform === "win32" ? "NUL" : "/dev/null";
|
|
32
|
+
clean.GIT_OPTIONAL_LOCKS = "0";
|
|
33
|
+
clean.GIT_TERMINAL_PROMPT = "0";
|
|
34
|
+
return clean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function runGit(root, args, { encoding = "buffer", maxBuffer = 4 * 1024 * 1024, timeout = COMMAND_TIMEOUT_MS, runner = spawnSync, env = gitEnv(), input } = {}) {
|
|
38
|
+
const result = runner(nativeGitPath(), ["-C", root, "--no-replace-objects", "-c", "core.fsmonitor=false", "-c", `core.hooksPath=${os.devNull}`, ...args], {
|
|
39
|
+
encoding, env, windowsHide: true, maxBuffer, timeout, ...(input ? { input } : {}),
|
|
40
|
+
});
|
|
41
|
+
if (result?.error || result?.status !== 0) throw new Error(`committed ownership: git ${args[0] ?? "command"} failed${result?.error?.code ? ` (${result.error.code})` : ""}`);
|
|
42
|
+
return result.stdout;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function validatePath(value) {
|
|
46
|
+
if (!value || value.includes("\\") || value.startsWith("/") || /^[A-Za-z]:/.test(value)) return false;
|
|
47
|
+
const parts = value.split("/");
|
|
48
|
+
return parts.every((part) => part && part !== "." && part !== "..") && path.posix.normalize(value) === value;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function hashGitObject(type, bytes, algorithm) {
|
|
52
|
+
return createHash(algorithm).update(`${type} ${bytes.length}\0`).update(bytes).digest("hex");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function parseTree(bytes, objectFormat) {
|
|
56
|
+
if (bytes.length > MAX_TREE_BYTES) throw new Error("committed ownership: tree listing exceeds 32MiB");
|
|
57
|
+
const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
58
|
+
const rows = [];
|
|
59
|
+
for (const record of text.split("\0")) {
|
|
60
|
+
if (!record) continue;
|
|
61
|
+
const tab = record.indexOf("\t");
|
|
62
|
+
if (tab < 0) throw new Error("committed ownership: malformed tree entry");
|
|
63
|
+
const [mode, type, oid] = record.slice(0, tab).split(" ");
|
|
64
|
+
const rel = record.slice(tab + 1);
|
|
65
|
+
const digestLength = objectFormat === "sha256" ? 64 : 40;
|
|
66
|
+
if (!validatePath(rel) || !new RegExp(`^[0-9a-f]{${digestLength}}$`).test(oid ?? "")) throw new Error("committed ownership: invalid tree path or object id");
|
|
67
|
+
if (type !== "blob" && type !== "commit") throw new Error(`committed ownership: unsupported tree object type at ${rel}`);
|
|
68
|
+
if (!new Set(["100644", "100755", "120000", "160000"]).has(mode)) throw new Error(`committed ownership: unsupported tree mode at ${rel}`);
|
|
69
|
+
if ((type === "commit") !== (mode === "160000") || (mode === "160000" && type !== "commit")) throw new Error(`committed ownership: mismatched tree mode and object type at ${rel}`);
|
|
70
|
+
rows.push({ rel, mode, type, oid });
|
|
71
|
+
}
|
|
72
|
+
rows.sort((a, b) => a.rel.localeCompare(b.rel));
|
|
73
|
+
return rows;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function parseBatchCheck(bytes, expectedOids) {
|
|
77
|
+
if (!expectedOids.length) {
|
|
78
|
+
if (bytes.length) throw new Error("committed ownership: unexpected blob size response");
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
const lines = bytes.toString("utf8").trimEnd().split("\n");
|
|
82
|
+
if (lines.length !== expectedOids.length) throw new Error("committed ownership: malformed blob size response");
|
|
83
|
+
return lines.map((line, index) => {
|
|
84
|
+
const [oid, type, sizeText] = line.trim().split(" ");
|
|
85
|
+
const size = Number(sizeText);
|
|
86
|
+
if (oid !== expectedOids[index] || type !== "blob" || !Number.isSafeInteger(size) || size < 0) throw new Error("committed ownership: invalid blob size response");
|
|
87
|
+
return size;
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function readBatch(root, oids, totalBytes, runner) {
|
|
92
|
+
if (!oids.length) return new Map();
|
|
93
|
+
const input = Buffer.from(`${oids.join("\n")}\n`);
|
|
94
|
+
const outputLimit = totalBytes + oids.length * 128;
|
|
95
|
+
const output = runGit(root, ["cat-file", "--batch"], { encoding: "buffer", maxBuffer: outputLimit, timeout: BLOB_TIMEOUT_MS, runner, env: gitEnv() , input });
|
|
96
|
+
if (!Buffer.isBuffer(output)) throw new Error("committed ownership: Git returned non-binary blob data");
|
|
97
|
+
const result = new Map();
|
|
98
|
+
let offset = 0;
|
|
99
|
+
for (const expected of oids) {
|
|
100
|
+
const endHeader = output.indexOf(0x0a, offset);
|
|
101
|
+
if (endHeader < 0) throw new Error("committed ownership: malformed blob header");
|
|
102
|
+
const [oid, type, sizeText] = output.subarray(offset, endHeader).toString("ascii").split(" ");
|
|
103
|
+
const size = Number(sizeText);
|
|
104
|
+
if (oid !== expected || type !== "blob" || !Number.isSafeInteger(size) || size < 0 || size > MAX_BLOB_BYTES) throw new Error("committed ownership: invalid blob object");
|
|
105
|
+
const start = endHeader + 1, end = start + size;
|
|
106
|
+
if (end >= output.length || output[end] !== 0x0a) throw new Error("committed ownership: truncated blob object");
|
|
107
|
+
const bytes = output.subarray(start, end);
|
|
108
|
+
const algorithm = expected.length === 64 ? "sha256" : "sha1";
|
|
109
|
+
if (hashGitObject("blob", bytes, algorithm) !== expected) throw new Error(`committed ownership: blob hash mismatch ${expected}`);
|
|
110
|
+
result.set(expected, Buffer.from(bytes));
|
|
111
|
+
offset = end + 1;
|
|
112
|
+
}
|
|
113
|
+
if (offset !== output.length) throw new Error("committed ownership: unexpected blob output");
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function acquire(root, { expectedCommit, expectedTree, requiredPaths = [], runner = spawnSync, env = gitEnv() } = {}) {
|
|
118
|
+
const text = (args) => runGit(root, args, { encoding: "utf8", runner, env }).trim();
|
|
119
|
+
const repositoryRoot = path.resolve(text(["rev-parse", "--show-toplevel"]));
|
|
120
|
+
const pathPrefix = text(["rev-parse", "--show-prefix"]).replaceAll("\\", "/");
|
|
121
|
+
const originResult = runner(nativeGitPath(), ["-C", repositoryRoot, "--no-replace-objects", "-c", "core.fsmonitor=false", "-c", `core.hooksPath=${os.devNull}`, "config", "--get", "remote.origin.url"], {
|
|
122
|
+
encoding: "utf8", env, windowsHide: true, maxBuffer: 4 * 1024 * 1024, timeout: COMMAND_TIMEOUT_MS,
|
|
123
|
+
});
|
|
124
|
+
const origin = originResult?.status === 0 ? String(originResult.stdout).trim() : null;
|
|
125
|
+
const objectFormat = text(["rev-parse", "--show-object-format"]);
|
|
126
|
+
if (objectFormat !== "sha1" && objectFormat !== "sha256") throw new Error("committed ownership: unsupported Git object format");
|
|
127
|
+
const commit = text(["rev-parse", "--verify", "HEAD^{commit}"]);
|
|
128
|
+
const digestLength = objectFormat === "sha256" ? 64 : 40;
|
|
129
|
+
if (!new RegExp(`^[0-9a-f]{${digestLength}}$`).test(commit) || (expectedCommit && commit !== expectedCommit)) throw new Error("committed ownership: expected commit is not current HEAD");
|
|
130
|
+
const tree = text(["rev-parse", "--verify", `${commit}^{tree}`]);
|
|
131
|
+
if (!new RegExp(`^[0-9a-f]{${digestLength}}$`).test(tree) || (expectedTree && tree !== expectedTree)) throw new Error("committed ownership: expected tree does not match commit");
|
|
132
|
+
const rows = parseTree(runGit(root, ["ls-tree", "-r", "-z", "--full-tree", tree], { encoding: "buffer", maxBuffer: MAX_TREE_BYTES, timeout: COMMAND_TIMEOUT_MS, runner, env }), objectFormat);
|
|
133
|
+
const wanted = new Set(requiredPaths.filter(validatePath));
|
|
134
|
+
const shouldRead = (row) => row.type === "blob" && row.mode !== "120000" && (
|
|
135
|
+
wanted.has(row.rel) || ["Cargo.toml", "package.json", "Package.swift", "ownership-exceptions.json"].includes(path.posix.basename(row.rel)) ||
|
|
136
|
+
/\.(?:rs|ts|tsx|js|mjs|cjs|jsx|swift|m|mm|c|cc|cpp|h|py|go|kt|java|cs)$/i.test(row.rel)
|
|
137
|
+
);
|
|
138
|
+
const candidates = rows.filter(shouldRead);
|
|
139
|
+
const uniqueOids = [...new Set(candidates.map((row) => row.oid))];
|
|
140
|
+
const sizeInput = Buffer.from(`${uniqueOids.join("\n")}\n`);
|
|
141
|
+
if (sizeInput.length > MAX_TREE_BYTES) throw new Error("committed ownership: blob size query exceeds 32MiB");
|
|
142
|
+
const sizesOutput = uniqueOids.length ? runGit(root, ["cat-file", "--batch-check=%(objectname) %(objecttype) %(objectsize)"], {
|
|
143
|
+
encoding: "buffer", maxBuffer: Math.min(MAX_TREE_BYTES, uniqueOids.length * 128 + 1), timeout: COMMAND_TIMEOUT_MS, runner, env, input: sizeInput,
|
|
144
|
+
}) : Buffer.alloc(0);
|
|
145
|
+
const sizes = parseBatchCheck(sizesOutput, uniqueOids);
|
|
146
|
+
const sizeByOid = new Map(uniqueOids.map((oid, i) => [oid, sizes[i]]));
|
|
147
|
+
const blobOids = [], coverage = [], blobsTotal = new Set();
|
|
148
|
+
let totalBlobBytes = 0;
|
|
149
|
+
for (const row of candidates) {
|
|
150
|
+
const size = sizeByOid.get(row.oid);
|
|
151
|
+
if (size > MAX_BLOB_BYTES) { coverage.push({ path: row.rel, status: "unscanned", reason: "committed-blob-over-64MiB" }); continue; }
|
|
152
|
+
if (!blobsTotal.has(row.oid) && totalBlobBytes + size > MAX_TOTAL_BLOB_BYTES) {
|
|
153
|
+
coverage.push({ path: row.rel, status: "unscanned", reason: "committed-blobs-over-128MiB" }); continue;
|
|
154
|
+
}
|
|
155
|
+
if (!blobsTotal.has(row.oid)) { blobsTotal.add(row.oid); totalBlobBytes += size; }
|
|
156
|
+
blobOids.push(row.oid);
|
|
157
|
+
}
|
|
158
|
+
const blobs = readBatch(root, [...new Set(blobOids)], totalBlobBytes, runner);
|
|
159
|
+
const entries = rows.map((row) => ({ ...row, size: row.type === "blob" ? sizeByOid.get(row.oid) : 0, bytes: blobs.get(row.oid) }));
|
|
160
|
+
return Object.freeze({ repositoryRoot, pathPrefix, origin, commit, tree, objectFormat, entries, blobs, coverage });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function acquireCommittedSource(root, options = {}) {
|
|
164
|
+
const { expectedCommit, expectedTree, requiredPaths = [] } = options;
|
|
165
|
+
try { return createSourceHandle(acquire(path.resolve(root), { expectedCommit, expectedTree, requiredPaths })); }
|
|
166
|
+
catch (error) { if (error instanceof CommittedSourceIncompleteError) throw error; throw new CommittedSourceIncompleteError(error.message, error); }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function acquireCommittedSourceForTest(root, options = {}) {
|
|
170
|
+
try { return createSourceHandle(acquire(path.resolve(root), options)); }
|
|
171
|
+
catch (error) { if (error instanceof CommittedSourceIncompleteError) throw error; throw new CommittedSourceIncompleteError(error.message, error); }
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function createSourceHandle(data) {
|
|
175
|
+
const handle = Object.freeze({
|
|
176
|
+
repositoryRoot: data.repositoryRoot, pathPrefix: data.pathPrefix, origin: data.origin,
|
|
177
|
+
commit: data.commit, tree: data.tree, objectFormat: data.objectFormat,
|
|
178
|
+
});
|
|
179
|
+
acquiredSources.set(handle, data);
|
|
180
|
+
return handle;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function committedSourceDataForInternalUse(handle) {
|
|
184
|
+
return acquiredSources.get(handle) ?? null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function assertCommittedCheckoutClean(root, { expectedCommit, expectedTree, runner = spawnSync } = {}) {
|
|
188
|
+
if (expectedCommit || expectedTree) {
|
|
189
|
+
const currentCommit = runGit(path.resolve(root), ["rev-parse", "--verify", "HEAD^{commit}"], { encoding: "utf8", timeout: COMMAND_TIMEOUT_MS, runner, env: gitEnv() }).trim();
|
|
190
|
+
const currentTree = runGit(path.resolve(root), ["rev-parse", "--verify", `${currentCommit}^{tree}`], { encoding: "utf8", timeout: COMMAND_TIMEOUT_MS, runner, env: gitEnv() }).trim();
|
|
191
|
+
if ((expectedCommit && currentCommit !== expectedCommit) || (expectedTree && currentTree !== expectedTree)) throw new CommittedSourceIncompleteError("committed ownership: HEAD changed during protected source acquisition");
|
|
192
|
+
}
|
|
193
|
+
const output = runGit(path.resolve(root), ["status", "--porcelain=v1", "-z", "--untracked-files=all"], {
|
|
194
|
+
encoding: "buffer", maxBuffer: MAX_TREE_BYTES, timeout: COMMAND_TIMEOUT_MS, runner, env: gitEnv(),
|
|
195
|
+
});
|
|
196
|
+
if (output.length) throw new CommittedSourceIncompleteError("committed ownership: protected release checkout has tracked or untracked changes");
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export const committedOwnershipLimitsForTest = Object.freeze({ MAX_TREE_BYTES, MAX_TOTAL_BLOB_BYTES, MAX_BLOB_BYTES });
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Draft/import/verify utilities. Signing is intentionally absent; native prompts own signing.
|
|
3
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { createDraft, loadTrustedExternal, verifyException } from "./ownership-approval.mjs";
|
|
7
|
+
|
|
8
|
+
function arg(args, name) { const i = args.indexOf(name); return i < 0 ? undefined : args[i + 1]; }
|
|
9
|
+
function json(file) { return JSON.parse(readFileSync(path.resolve(file), "utf8")); }
|
|
10
|
+
export function main(args) {
|
|
11
|
+
if (args.includes("--draft")) {
|
|
12
|
+
const input = json(arg(args, "--draft"));
|
|
13
|
+
const draft = createDraft(input);
|
|
14
|
+
const out = arg(args, "--out");
|
|
15
|
+
const data = `${JSON.stringify(draft, null, 2)}\n`;
|
|
16
|
+
if (out) writeFileSync(path.resolve(out), data, { flag: "wx" }); else process.stdout.write(data);
|
|
17
|
+
return 0;
|
|
18
|
+
}
|
|
19
|
+
if (args.includes("--import")) {
|
|
20
|
+
const record = json(arg(args, "--import"));
|
|
21
|
+
const out = arg(args, "--out");
|
|
22
|
+
if (!out) throw new Error("--import requires --out");
|
|
23
|
+
writeFileSync(path.resolve(out), `${JSON.stringify(record, null, 2)}\n`, { flag: "wx" });
|
|
24
|
+
return 0;
|
|
25
|
+
}
|
|
26
|
+
if (args.includes("--verify")) {
|
|
27
|
+
const record = json(arg(args, "--verify"));
|
|
28
|
+
const context = json(arg(args, "--context"));
|
|
29
|
+
const loaded = loadTrustedExternal();
|
|
30
|
+
const result = verifyException(record, { ...context, trust: loaded.trust });
|
|
31
|
+
process.stdout.write(`${JSON.stringify({ ...result, pending: loaded.pending }, null, 2)}\n`);
|
|
32
|
+
return result.ok ? 0 : 1;
|
|
33
|
+
}
|
|
34
|
+
throw new Error("usage: node ownership-approval-command.mjs --draft input.json [--out draft.json] | --import signed.json --out record.json | --verify signed.json --context scan.json");
|
|
35
|
+
}
|
|
36
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
37
|
+
try { process.exitCode = main(process.argv.slice(2)); }
|
|
38
|
+
catch (error) { console.error(`[right-release] ownership approval: ${error.message}`); process.exitCode = 1; }
|
|
39
|
+
}
|