@rightkit/release 0.2.132 → 0.2.133
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/apple-profile-renew.mjs +220 -0
- package/artifact-storage.mjs +344 -0
- package/build-release.mjs +39 -18
- package/cli/right-release.mjs +9 -3
- package/create-mac-updater.mjs +8 -15
- package/development-signing.mjs +193 -0
- package/development.mjs +15 -13
- package/installer-storage-doctor.mjs +120 -0
- package/macos-notarization.mjs +72 -0
- package/macos-signing-session.mjs +150 -0
- package/mirror-root-artifact.mjs +46 -11
- package/model-promote.mjs +32 -5
- package/notarize-macos.mjs +10 -32
- package/ownership-key-command.mjs +3 -1
- package/ownership-report.mjs +15 -5
- package/package.json +1 -1
- package/publish-update.mjs +14 -6
- package/release.mjs +42 -18
- package/rightkit-signing-agent.mjs +253 -0
- package/rightkit-versions.json +33 -24
- package/runtime-artifact-manifest.mjs +5 -1
- package/sign-macos.mjs +32 -4
- package/sign-updater.mjs +8 -13
- package/signing-agent-client.mjs +85 -0
- package/signing-agent-command.mjs +21 -0
- package/signing-agent-install.mjs +100 -0
- package/signing-receipts.mjs +10 -0
- package/upload-release.mjs +4 -0
package/ownership-report.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { checkApp, EXCEPTIONS_FILE } from "./ownership.mjs";
|
|
4
4
|
import { DEFAULT_APPS } from "./standalone-clone-verify.mjs";
|
|
5
5
|
import { sha256 } from "./ownership-approval.mjs";
|
|
6
|
+
import { findInstallerCopyScripts, findLargeRepositorySubdirectories } from "./installer-storage-doctor.mjs";
|
|
6
7
|
|
|
7
8
|
export const OWNERSHIP_APPS = Object.freeze([
|
|
8
9
|
...DEFAULT_APPS.map(({ key, appDir }) => ({ key, root: key === "coderight" ? key : path.posix.join(key, appDir === "." ? "" : appDir), scope: key === "coderight" ? "repository" : appDir })),
|
|
@@ -58,7 +59,7 @@ function exceptionRequests(root, app, report) {
|
|
|
58
59
|
});
|
|
59
60
|
}
|
|
60
61
|
|
|
61
|
-
function asRecord(app, root, scope, report, manifest, versions, scannedAt) {
|
|
62
|
+
function asRecord(app, root, scope, report, manifest, versions, scannedAt, storageFindings = []) {
|
|
62
63
|
const meta = report.metadata ?? {};
|
|
63
64
|
const findings = (report.findings ?? []).map((f) => ({ fingerprint: f.fingerprint, capability: f.capability, file: f.file, line: f.line ?? 0, occurrenceHash: f.occurrenceHash ?? sha256(canonicalJson(f)), ...(f.qaOnly ? { qaOnly: true } : {}) }));
|
|
64
65
|
const files = [...new Set(findings.map((f) => f.file))].sort();
|
|
@@ -93,8 +94,9 @@ function asRecord(app, root, scope, report, manifest, versions, scannedAt) {
|
|
|
93
94
|
covered: report.covered ?? [],
|
|
94
95
|
problems: report.problems ?? [],
|
|
95
96
|
warnings: report.warnings ?? [],
|
|
97
|
+
storageFindings,
|
|
96
98
|
};
|
|
97
|
-
record.resultDigest = sha256(canonicalJson({ app: record.app, commit: record.commit, catalogDigest: record.catalogDigest, contentDigests: record.contentDigests, coverage: record.coverage, rows: record.rows, problems: record.problems, warnings: record.warnings, enrollmentPending: record.enrollmentPending }));
|
|
99
|
+
record.resultDigest = sha256(canonicalJson({ app: record.app, commit: record.commit, catalogDigest: record.catalogDigest, contentDigests: record.contentDigests, coverage: record.coverage, rows: record.rows, problems: record.problems, warnings: record.warnings, storageFindings: record.storageFindings, enrollmentPending: record.enrollmentPending }));
|
|
98
100
|
record.exceptionRequests = exceptionRequests(root, app, report);
|
|
99
101
|
return record;
|
|
100
102
|
}
|
|
@@ -104,12 +106,20 @@ export function buildOwnershipReport({ workspaceRoot, manifest, versions, apps =
|
|
|
104
106
|
const scannedAt = now.toISOString();
|
|
105
107
|
const records = apps.map(({ key, root: relativeRoot, scope }) => {
|
|
106
108
|
const root = path.resolve(workspaceRoot, relativeRoot);
|
|
107
|
-
if (!existsSync(root)) return { app: key, scope: scope ?? ".", repositoryRoot: root, status: "unscanned", reason: "configured app root is missing", catalogVersion: catalogVersion(manifest), catalogDigest: sha256(canonicalJson(manifest)), scannedAt, host: null, commit: null, scanAgeSeconds: 0, rows: [], findings: [], violations: [], covered: [], problems: [], coverage: [{ path: ".", status: "unscanned", reason: "configured app root is missing" }], contentDigests: [], resultDigest: sha256(`${key}:missing`) };
|
|
108
|
-
try {
|
|
109
|
-
|
|
109
|
+
if (!existsSync(root)) return { app: key, scope: scope ?? ".", repositoryRoot: root, status: "unscanned", reason: "configured app root is missing", catalogVersion: catalogVersion(manifest), catalogDigest: sha256(canonicalJson(manifest)), scannedAt, host: null, commit: null, scanAgeSeconds: 0, rows: [], findings: [], violations: [], covered: [], problems: [], warnings: [], storageFindings: [], coverage: [{ path: ".", status: "unscanned", reason: "configured app root is missing" }], contentDigests: [], resultDigest: sha256(`${key}:missing`) };
|
|
110
|
+
try {
|
|
111
|
+
const scan = scanner(root, manifest);
|
|
112
|
+
const storageFindings = [
|
|
113
|
+
...findInstallerCopyScripts({ appRoot: root, workspaceRoot }),
|
|
114
|
+
...findLargeRepositorySubdirectories({ appRoot: root }),
|
|
115
|
+
];
|
|
116
|
+
return asRecord(key, root, scope, scan, manifest, versions, scannedAt, storageFindings);
|
|
117
|
+
}
|
|
118
|
+
catch (error) { return { app: key, scope: scope ?? ".", repositoryRoot: root, status: "unscanned", reason: error.message, catalogVersion: catalogVersion(manifest), catalogDigest: sha256(canonicalJson(manifest)), scannedAt, host: null, commit: null, scanAgeSeconds: 0, rows: [], findings: [], violations: [], covered: [], problems: [error.message], warnings: [], storageFindings: [], coverage: [{ path: ".", status: "unscanned", reason: error.message }], contentDigests: [], resultDigest: sha256(`${key}:${error.message}`) }; }
|
|
110
119
|
});
|
|
111
120
|
const blockMap = new Map(), requestMap = new Map(), warnings = [];
|
|
112
121
|
for (const record of records) {
|
|
122
|
+
if (record.storageFindings?.length) warnings.push({ app: record.app, message: "release storage ownership findings detected; release artifacts should stay in managed storage", findings: record.storageFindings, problems: [] });
|
|
113
123
|
for (const request of record.exceptionRequests ?? []) if (!requestMap.has(request.contentHash)) requestMap.set(request.contentHash, request);
|
|
114
124
|
if (record.enrollmentPending) {
|
|
115
125
|
warnings.push({ app: record.app, message: "approval enrollment pending; ownership findings remain backlog diagnostics", findings: record.violations, problems: record.problems, scannerWarnings: record.warnings });
|
package/package.json
CHANGED
package/publish-update.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import { spawn } from "./background-process.mjs";
|
|
|
7
7
|
import { pruneReleaseObjects } from "./prune-r2.mjs";
|
|
8
8
|
import { loadReleaseToken } from "./release-token.mjs";
|
|
9
9
|
import { registerRightAppsRelease } from "./rightapps-register.mjs";
|
|
10
|
+
import { recordPublishedVersion, resolveArtifactInput } from "./artifact-storage.mjs";
|
|
10
11
|
|
|
11
12
|
const TOOL_ROOT = path.dirname(fileURLToPath(import.meta.url));
|
|
12
13
|
const UPLOAD = path.join(TOOL_ROOT, "upload-large.mjs");
|
|
@@ -41,6 +42,10 @@ if (config?.distribution?.provider !== "r2-rightapps") {
|
|
|
41
42
|
const target = config?.targets?.[platform];
|
|
42
43
|
const artifacts = target?.updater?.artifacts ?? [];
|
|
43
44
|
const installers = target?.installer?.artifacts ?? [];
|
|
45
|
+
const versionForStorage = config.version || target?.updater?.version;
|
|
46
|
+
const artifactFile = (declared) => versionForStorage
|
|
47
|
+
? resolveArtifactInput({ root, app: config.app, version: versionForStorage, declared })
|
|
48
|
+
: path.resolve(root, declared);
|
|
44
49
|
|
|
45
50
|
if (tier === "patch" && !installers.length) fail(`${config?.app ?? "app"} ${platform} patch has no installer.artifacts`);
|
|
46
51
|
if (tier === "update" && !artifacts.length) fail(`${config?.app ?? "app"} ${platform} update has no updater.artifacts`);
|
|
@@ -57,7 +62,7 @@ if (!dryRun && (tier === "update" || artifacts.length > 0)) {
|
|
|
57
62
|
if (tier === "patch") {
|
|
58
63
|
for (const installer of installers) {
|
|
59
64
|
assertCurrentKey(installer.key, "installer");
|
|
60
|
-
const file =
|
|
65
|
+
const file = artifactFile(installer.file);
|
|
61
66
|
if (!dryRun) await access(file).catch(() => fail(`missing signed installer: ${installer.file}`));
|
|
62
67
|
}
|
|
63
68
|
const platforms = {};
|
|
@@ -69,8 +74,8 @@ if (tier === "patch") {
|
|
|
69
74
|
assertCurrentKey(artifact.key, "updater");
|
|
70
75
|
const installerKey = patchInstallerKey(artifact);
|
|
71
76
|
assertCurrentKey(installerKey, "installer");
|
|
72
|
-
const file =
|
|
73
|
-
const signatureFile =
|
|
77
|
+
const file = artifactFile(artifact.file);
|
|
78
|
+
const signatureFile = artifactFile(artifact.signature);
|
|
74
79
|
if (!dryRun) {
|
|
75
80
|
await access(file).catch(() => fail(`missing patch updater payload: ${artifact.file}`));
|
|
76
81
|
await access(signatureFile).catch(() => fail(`missing patch updater signature: ${artifact.signature}`));
|
|
@@ -96,7 +101,7 @@ if (tier === "patch") {
|
|
|
96
101
|
});
|
|
97
102
|
}
|
|
98
103
|
for (const installer of installers) {
|
|
99
|
-
const file =
|
|
104
|
+
const file = artifactFile(installer.file);
|
|
100
105
|
if (!uploadedKeys.has(installer.key)) {
|
|
101
106
|
console.log(`${dryRun ? "[dry-run] " : ""}UPLOAD public/${installer.key}`);
|
|
102
107
|
if (!dryRun) await run(process.execPath, [UPLOAD, file, installer.key, "public"], root);
|
|
@@ -121,6 +126,7 @@ if (tier === "patch") {
|
|
|
121
126
|
private: target.updater.artifacts.map((artifact) => artifact.key),
|
|
122
127
|
},
|
|
123
128
|
});
|
|
129
|
+
if (!dryRun) recordPublishedVersion({ app: config.app, version: versionForStorage, platform });
|
|
124
130
|
process.exit(0);
|
|
125
131
|
}
|
|
126
132
|
const version = config.version || target.updater.version;
|
|
@@ -158,6 +164,7 @@ if (tier === "patch") {
|
|
|
158
164
|
private: [],
|
|
159
165
|
},
|
|
160
166
|
});
|
|
167
|
+
if (!dryRun) recordPublishedVersion({ app: config.app, version: versionForStorage, platform });
|
|
161
168
|
process.exit(0);
|
|
162
169
|
}
|
|
163
170
|
|
|
@@ -167,8 +174,8 @@ const updaterManifestArtifacts = new Map();
|
|
|
167
174
|
const uploadedKeys = new Set();
|
|
168
175
|
for (const artifact of artifacts) {
|
|
169
176
|
assertCurrentKey(artifact.key, "updater");
|
|
170
|
-
const file =
|
|
171
|
-
const signatureFile =
|
|
177
|
+
const file = artifactFile(artifact.file);
|
|
178
|
+
const signatureFile = artifactFile(artifact.signature);
|
|
172
179
|
if (!dryRun) {
|
|
173
180
|
await access(file).catch(() => fail(`missing updater artifact: ${artifact.file}`));
|
|
174
181
|
await access(signatureFile).catch(() => fail(`missing updater signature: ${artifact.signature}`));
|
|
@@ -231,6 +238,7 @@ await pruneReleaseObjects({
|
|
|
231
238
|
private: [...registrations.keys()],
|
|
232
239
|
},
|
|
233
240
|
});
|
|
241
|
+
if (!dryRun) recordPublishedVersion({ app: config.app, version: versionForStorage, platform });
|
|
234
242
|
|
|
235
243
|
async function fileMetadata(file) {
|
|
236
244
|
const bytes = await readFile(file);
|
package/release.mjs
CHANGED
|
@@ -22,9 +22,12 @@ import { patchTauriBundleType } from "./tauri-bundle-marker.mjs";
|
|
|
22
22
|
import { verifyNsisEmbeddedBinary } from "./nsis-payload.mjs";
|
|
23
23
|
import { terminateProcessTree } from "./heavy-command.mjs";
|
|
24
24
|
import { materializeHardeningEvidence } from "./hardening-evidence.mjs";
|
|
25
|
+
import { signingReceiptPath } from "./signing-receipts.mjs";
|
|
26
|
+
import { validateNotarizationReceipt } from "./macos-notarization.mjs";
|
|
25
27
|
import { MACOS_PORTABLE_SIGNING_CONTRACT } from "./macos-signing-contract.mjs";
|
|
26
28
|
import { runConfiguredChecks } from "./release-checks.mjs";
|
|
27
29
|
import { resolveMacosDeveloperIdIdentity } from "./macos-signing-identity.mjs";
|
|
30
|
+
import { DEFAULT_STORAGE_THRESHOLD_BYTES, findInstallerCopyScripts, findLargeRepositorySubdirectories } from "./installer-storage-doctor.mjs";
|
|
28
31
|
|
|
29
32
|
const TOOL_ROOT = path.dirname(fileURLToPath(import.meta.url));
|
|
30
33
|
const HARDENING_SCAN = path.resolve(TOOL_ROOT, "hardeningscan.mjs");
|
|
@@ -200,6 +203,20 @@ if (opts.doctor) {
|
|
|
200
203
|
}
|
|
201
204
|
if (target.sign?.files?.length) console.log(`sign: ${target.sign.files.join(", ")}`);
|
|
202
205
|
if (target.publishBlocked) console.log(`publishBlocked: ${target.publishBlocked}`);
|
|
206
|
+
const managedRoot = process.env.RIGHTKIT_MANAGED_ROOT;
|
|
207
|
+
const installerCopyFindings = findInstallerCopyScripts({
|
|
208
|
+
appRoot: root,
|
|
209
|
+
workspaceRoot: process.env.RIGHTKIT_WORKSPACE_ROOT ?? path.resolve(doctorInvocation.repoRoot, ".."),
|
|
210
|
+
});
|
|
211
|
+
const storageFindings = [
|
|
212
|
+
...installerCopyFindings,
|
|
213
|
+
...findLargeRepositorySubdirectories({ appRoot: root, managedRoot, thresholdBytes: Number(process.env.RIGHTKIT_ARTIFACT_STORAGE_THRESHOLD_BYTES ?? DEFAULT_STORAGE_THRESHOLD_BYTES) }),
|
|
214
|
+
];
|
|
215
|
+
if (storageFindings.length) {
|
|
216
|
+
console.log("artifact-storage: report-only storage findings detected:");
|
|
217
|
+
for (const finding of storageFindings) console.log(` - ${finding.path}: ${finding.reason}`);
|
|
218
|
+
fail(`artifact-storage found ${storageFindings.length} repository storage finding(s); move outputs to RIGHT_RELEASE_ARTIFACT_DIR`);
|
|
219
|
+
} else console.log("artifact-storage: no installer-copy scripts detected");
|
|
203
220
|
|
|
204
221
|
// Config above, MACHINE below. Printing config never told anyone whether this
|
|
205
222
|
// box could finish a build; these checks do.
|
|
@@ -284,13 +301,13 @@ if (opts.platform === "win") {
|
|
|
284
301
|
if (opts.platform !== "mac" || runMacPhase("package")) await runCommand(command, root);
|
|
285
302
|
|
|
286
303
|
for (const rel of target.artifacts ?? []) {
|
|
287
|
-
await mustExist(
|
|
304
|
+
await mustExist(resolveReleaseArtifactPath(root, rel), `missing release artifact: ${rel}`);
|
|
288
305
|
}
|
|
289
306
|
|
|
290
307
|
if (nativeFinalization) await completeNativeFinalization(nativeFinalization, { root, target, nativeAssembly });
|
|
291
308
|
|
|
292
309
|
if (opts.platform === "win" && target.signingContract === WINDOWS_NSIS_SIGNING_CONTRACT) {
|
|
293
|
-
const files = target.sign.files.map((p) =>
|
|
310
|
+
const files = target.sign.files.map((p) => resolveReleaseArtifactPath(root, p));
|
|
294
311
|
for (const file of files) await mustExist(file, `missing signing artifact: ${file}`);
|
|
295
312
|
// Before the installer earns its own signature, prove it carries the bytes we
|
|
296
313
|
// signed. An installer-only signature on a mutated payload verifies perfectly
|
|
@@ -298,7 +315,7 @@ if (opts.platform === "win" && target.signingContract === WINDOWS_NSIS_SIGNING_C
|
|
|
298
315
|
await verifyWindowsEmbedding({ root, target, files, markers: bundleMarkerReceipts });
|
|
299
316
|
await signWindows(files, "installer", root);
|
|
300
317
|
if (target.postSign) await runCommand(target.postSign, root);
|
|
301
|
-
const updaterFiles = [...new Set((target.updater?.artifacts ?? []).map((artifact) =>
|
|
318
|
+
const updaterFiles = [...new Set((target.updater?.artifacts ?? []).map((artifact) => resolveReleaseArtifactPath(root, artifact.file)))];
|
|
302
319
|
if (!updaterFiles.length) fail(`${config.app} win must declare updater.artifacts for post-code-signing minisign`);
|
|
303
320
|
await run("node", [SIGN_UPDATER, ...(opts.dryRun ? ["--dry-run"] : []), ...updaterFiles.flatMap((file) => ["--file", file])], root);
|
|
304
321
|
}
|
|
@@ -308,7 +325,7 @@ if (target.postSign && opts.platform !== "win" && (opts.platform !== "mac" || ru
|
|
|
308
325
|
}
|
|
309
326
|
|
|
310
327
|
if (opts.platform === "mac") {
|
|
311
|
-
const archive =
|
|
328
|
+
const archive = resolveReleaseArtifactPath(root, target.notarize.file);
|
|
312
329
|
await mustExist(archive, `missing macOS notarization archive: ${archive}`);
|
|
313
330
|
await notarizeMacos(archive, root);
|
|
314
331
|
}
|
|
@@ -326,7 +343,7 @@ if (opts.platform !== "mac" || runMacPhase("harden")) {
|
|
|
326
343
|
await run("node", [
|
|
327
344
|
HARDENING_SCAN,
|
|
328
345
|
...(hardeningEvidence ? ["--allow-evidence", hardeningEvidence] : []),
|
|
329
|
-
...scanTargets.map((p) =>
|
|
346
|
+
...scanTargets.map((p) => resolveReleaseArtifactPath(root, p)),
|
|
330
347
|
], root);
|
|
331
348
|
}
|
|
332
349
|
}
|
|
@@ -337,7 +354,7 @@ if (opts.upload && target.publish) {
|
|
|
337
354
|
|
|
338
355
|
if (opts.upload && !target.publish) {
|
|
339
356
|
for (const item of target.upload ?? []) {
|
|
340
|
-
const file =
|
|
357
|
+
const file = resolveReleaseArtifactPath(root, item.file);
|
|
341
358
|
await mustExist(file, `missing upload artifact: ${item.file}`);
|
|
342
359
|
await run("node", [UPLOAD_LARGE, file, item.key, item.bucket ?? "public"], root);
|
|
343
360
|
}
|
|
@@ -358,6 +375,20 @@ function usage(code, message) {
|
|
|
358
375
|
process.exit(code);
|
|
359
376
|
}
|
|
360
377
|
|
|
378
|
+
// Product package scripts may emit final installer bytes directly into the
|
|
379
|
+
// managed artifact directory. Keep legacy target-relative paths working while
|
|
380
|
+
// preferring managed output when the local path does not exist.
|
|
381
|
+
function resolveReleaseArtifactPath(root, declared) {
|
|
382
|
+
const local = path.resolve(root, declared);
|
|
383
|
+
if (existsSync(local)) return local;
|
|
384
|
+
const managed = process.env.RIGHT_RELEASE_ARTIFACT_DIR;
|
|
385
|
+
if (managed) {
|
|
386
|
+
const candidate = path.join(path.resolve(managed), path.basename(declared));
|
|
387
|
+
if (existsSync(candidate)) return candidate;
|
|
388
|
+
}
|
|
389
|
+
return local;
|
|
390
|
+
}
|
|
391
|
+
|
|
361
392
|
function fail(message) {
|
|
362
393
|
console.error(`right-release: ${message}`);
|
|
363
394
|
process.exit(1);
|
|
@@ -793,9 +824,7 @@ async function signWindows(files, phase, root) {
|
|
|
793
824
|
}
|
|
794
825
|
|
|
795
826
|
function windowsReceiptPath(root, phase) {
|
|
796
|
-
|
|
797
|
-
if (configured) return path.resolve(root, configured);
|
|
798
|
-
return path.join(process.env.RIGHT_RELEASE_STATE_ROOT ?? path.join(root, ".right-release", "receipts"), `windows-${phase}.json`);
|
|
827
|
+
return signingReceiptPath({ appRoot: root, target, platform: "win", phase, stateRoot: process.env.RIGHT_RELEASE_STATE_ROOT });
|
|
799
828
|
}
|
|
800
829
|
|
|
801
830
|
async function signMacos(files, root) {
|
|
@@ -815,6 +844,7 @@ async function notarizeMacos(archive, root) {
|
|
|
815
844
|
if (opts.dryRun) args.push("--dry-run");
|
|
816
845
|
args.push("--receipt", receipt);
|
|
817
846
|
if (target.notarize.profile) args.push("--profile", target.notarize.profile);
|
|
847
|
+
if (target.notarize.app) args.push("--app", resolveReleaseArtifactPath(root, target.notarize.app));
|
|
818
848
|
args.push(archive);
|
|
819
849
|
if (!opts.dryRun && existsSync(receipt)) unlinkSync(receipt);
|
|
820
850
|
console.log("right-release: notarizing macOS archive");
|
|
@@ -823,9 +853,7 @@ async function notarizeMacos(archive, root) {
|
|
|
823
853
|
}
|
|
824
854
|
|
|
825
855
|
function macosReceiptPath(root, phase) {
|
|
826
|
-
|
|
827
|
-
if (configured) return path.resolve(root, configured);
|
|
828
|
-
return path.join(process.env.RIGHT_RELEASE_STATE_ROOT ?? path.join(root, ".right-release", "receipts"), `macos-${phase}.json`);
|
|
856
|
+
return signingReceiptPath({ appRoot: root, target, platform: "mac", phase, stateRoot: process.env.RIGHT_RELEASE_STATE_ROOT });
|
|
829
857
|
}
|
|
830
858
|
|
|
831
859
|
function verifyMacosSigningReceipt(receipt, files) {
|
|
@@ -843,12 +871,8 @@ function verifyMacosSigningReceipt(receipt, files) {
|
|
|
843
871
|
}
|
|
844
872
|
|
|
845
873
|
function verifyMacosNotarizationReceipt(receipt, archive) {
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
try { evidence = JSON.parse(readFileSync(receipt, "utf8")); } catch { fail(`invalid macOS notarization receipt: ${receipt}`); }
|
|
849
|
-
const sha256 = createHash("sha256").update(readFileSync(archive)).digest("hex");
|
|
850
|
-
if (evidence.schema !== 1 || evidence.signingContract !== MACOS_PORTABLE_SIGNING_CONTRACT || path.resolve(evidence.archive?.file ?? "") !== archive
|
|
851
|
-
|| evidence.archive?.sha256 !== sha256 || evidence.notarization?.status !== "Accepted") fail(`invalid macOS notarization receipt evidence: ${receipt}`);
|
|
874
|
+
try { validateNotarizationReceipt({ receiptPath: receipt, archive }); }
|
|
875
|
+
catch (error) { fail(error.message); }
|
|
852
876
|
}
|
|
853
877
|
|
|
854
878
|
/**
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import net from "node:net";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { randomBytes, timingSafeEqual, randomUUID } from "node:crypto";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { spawnSync } from "./background-process.mjs";
|
|
9
|
+
|
|
10
|
+
export const AGENT_LABEL = "dev.rightkit.signing-agent";
|
|
11
|
+
export const MAX_FRAME = 1024 * 1024;
|
|
12
|
+
export class SigningAgentError extends Error {
|
|
13
|
+
constructor(code, message) { super(message); this.name = "SigningAgentError"; this.code = code; }
|
|
14
|
+
}
|
|
15
|
+
const fail = (code, message) => { throw new SigningAgentError(code, message); };
|
|
16
|
+
export function signingAgentPaths(home = os.userInfo().homedir) {
|
|
17
|
+
const directory = path.join(home, "Library/Application Support/Orthic Labs/shared/signing-agent");
|
|
18
|
+
return { directory, socket: path.join(directory, "agent.sock"), token: path.join(directory, "token"),
|
|
19
|
+
config: path.join(directory, "config.json"), runtime: path.join(directory, "runtime"),
|
|
20
|
+
log: path.join(directory, "requests.jsonl"), plist: path.join(home, "Library/LaunchAgents", `${AGENT_LABEL}.plist`) };
|
|
21
|
+
}
|
|
22
|
+
export function defaultSigningRoots(home = os.userInfo().homedir, managedRoot = "/Volumes/D/.rightkit-managed") {
|
|
23
|
+
return ["/Volumes/D/claude", managedRoot, "/Volumes/D/rightkit-build-control", path.join(home, "Library/Caches/RightSuite/release")];
|
|
24
|
+
}
|
|
25
|
+
export const within = (root, file) => file === root || file.startsWith(`${root}${path.sep}`);
|
|
26
|
+
export function privatePath(file, mode, { io = fs, uid = process.getuid?.(), kind = "file" } = {}) {
|
|
27
|
+
const info = io.lstatSync(file);
|
|
28
|
+
if (info.uid !== uid || (info.mode & 0o777) !== mode || info.isSymbolicLink()
|
|
29
|
+
|| !(kind === "directory" ? info.isDirectory() : kind === "socket" ? info.isSocket() : info.isFile())) {
|
|
30
|
+
fail("MAC_SIGNING_AGENT_UNSAFE_PATH", "Signing agent path has invalid ownership, type, or permissions");
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export function ensurePrivateDirectory(directory, { io = fs, uid = process.getuid?.() } = {}) {
|
|
34
|
+
// Reject symlink ancestors before mkdir/chmod. Existing public ancestors are
|
|
35
|
+
// fine only if no other uid can replace their children.
|
|
36
|
+
const parts = path.resolve(directory).split(path.sep).filter(Boolean);
|
|
37
|
+
let current = path.parse(directory).root;
|
|
38
|
+
for (const part of parts) {
|
|
39
|
+
current = path.join(current, part);
|
|
40
|
+
if (!io.existsSync(current)) io.mkdirSync(current, { mode: 0o700 });
|
|
41
|
+
const info = io.lstatSync(current);
|
|
42
|
+
if (!info.isDirectory() || info.isSymbolicLink() || ![0, uid].includes(info.uid)
|
|
43
|
+
|| ((info.mode & 0o022) && !(info.mode & 0o1000))) fail("MAC_SIGNING_AGENT_UNSAFE_PATH", "Unsafe signing agent directory ancestor");
|
|
44
|
+
}
|
|
45
|
+
io.chmodSync(directory, 0o700);
|
|
46
|
+
privatePath(directory, 0o700, { io, uid, kind: "directory" });
|
|
47
|
+
}
|
|
48
|
+
function shape(value, fields) {
|
|
49
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).some((key) => !fields.includes(key))) {
|
|
50
|
+
fail("MAC_SIGNING_AGENT_PROTOCOL", "Unknown request fields or invalid object");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function absolute(value) {
|
|
54
|
+
if (typeof value !== "string" || !path.isAbsolute(value) || value.includes("\0")) fail("MAC_SIGNING_AGENT_PATH_DENIED", "Paths must be absolute");
|
|
55
|
+
return path.resolve(value);
|
|
56
|
+
}
|
|
57
|
+
export function validateSigningRequest(request, { roots, stagingRoot, io = fs } = {}) {
|
|
58
|
+
const common = ["schema", "token", "type", "allowedRoots", "cwd", "timeoutMs"];
|
|
59
|
+
if (request?.type === "health") {
|
|
60
|
+
shape(request, ["schema", "token", "type"]);
|
|
61
|
+
if (request.schema !== 1) fail("MAC_SIGNING_AGENT_PROTOCOL", "Unsupported protocol schema");
|
|
62
|
+
return request;
|
|
63
|
+
}
|
|
64
|
+
if (!["codesign", "updater"].includes(request?.type)) fail("MAC_SIGNING_AGENT_PROTOCOL", "Unsupported signing request type");
|
|
65
|
+
shape(request, [...common, request.type === "codesign" ? "items" : "files"]);
|
|
66
|
+
if (request.schema !== 1 || !Number.isInteger(request.timeoutMs) || request.timeoutMs < 1 || request.timeoutMs > 180000) {
|
|
67
|
+
fail("MAC_SIGNING_AGENT_PROTOCOL", "Invalid schema or signing deadline");
|
|
68
|
+
}
|
|
69
|
+
if (!Array.isArray(request.allowedRoots) || !request.allowedRoots.length || request.allowedRoots.length > 32) fail("MAC_SIGNING_AGENT_PATH_DENIED", "Allowed roots required");
|
|
70
|
+
// Callers may narrow installed policy, never widen it. Check lexical & real
|
|
71
|
+
// containment, so symlinks cannot turn a workspace path into another root.
|
|
72
|
+
const installed = roots.map(absolute);
|
|
73
|
+
const allowed = request.allowedRoots.map((root) => {
|
|
74
|
+
const lexical = absolute(root);
|
|
75
|
+
if (![...installed, stagingRoot].filter(Boolean).some((base) => within(base, lexical))) fail("MAC_SIGNING_AGENT_PATH_DENIED", "Caller root is outside installed signing policy");
|
|
76
|
+
const canonical = io.realpathSync(lexical);
|
|
77
|
+
if (![...installed, stagingRoot].filter(Boolean).some((base) => {
|
|
78
|
+
try { return within(io.realpathSync(base), canonical); } catch { return false; }
|
|
79
|
+
})) fail("MAC_SIGNING_AGENT_PATH_DENIED", "Allowed root escapes installed policy");
|
|
80
|
+
return { lexical, canonical };
|
|
81
|
+
});
|
|
82
|
+
function checked(file, { directory = false, entitlements = false } = {}) {
|
|
83
|
+
const lexical = absolute(file);
|
|
84
|
+
const canonical = io.realpathSync(lexical);
|
|
85
|
+
if (!allowed.some((root) => within(root.lexical, lexical) && within(root.canonical, canonical))
|
|
86
|
+
|| (!entitlements && stagingRoot && within(stagingRoot, canonical))) fail("MAC_SIGNING_AGENT_PATH_DENIED", "Signing path escapes allowed roots");
|
|
87
|
+
const info = io.statSync(canonical);
|
|
88
|
+
if (directory ? !info.isDirectory() : !info.isFile() && (!info.isDirectory() || entitlements)) fail("MAC_SIGNING_AGENT_PATH_DENIED", "Invalid signing path type");
|
|
89
|
+
return canonical;
|
|
90
|
+
}
|
|
91
|
+
const cwd = checked(request.cwd, { directory: true });
|
|
92
|
+
if (request.type === "updater") {
|
|
93
|
+
if (!Array.isArray(request.files) || !request.files.length || request.files.length > 4096) fail("MAC_SIGNING_AGENT_PROTOCOL", "Updater files required");
|
|
94
|
+
const files = request.files.map((file) => {
|
|
95
|
+
const canonical = checked(file);
|
|
96
|
+
if (!io.statSync(canonical).isFile()) fail("MAC_SIGNING_AGENT_PATH_DENIED", "Updater artifact must be a file");
|
|
97
|
+
// The signer creates <artifact>.sig; reject existing output symlinks too.
|
|
98
|
+
if (io.existsSync(`${canonical}.sig`) || (() => { try { return io.lstatSync(`${canonical}.sig`).isSymbolicLink(); } catch { return false; } })()) {
|
|
99
|
+
if (io.lstatSync(`${canonical}.sig`).isSymbolicLink()) fail("MAC_SIGNING_AGENT_PATH_DENIED", "Updater signature cannot be a symlink");
|
|
100
|
+
checked(`${canonical}.sig`);
|
|
101
|
+
}
|
|
102
|
+
return canonical;
|
|
103
|
+
});
|
|
104
|
+
return { ...request, cwd, files };
|
|
105
|
+
}
|
|
106
|
+
if (!Array.isArray(request.items) || !request.items.length || request.items.length > 4096) fail("MAC_SIGNING_AGENT_PROTOCOL", "Codesign items required");
|
|
107
|
+
const items = request.items.map((item) => {
|
|
108
|
+
shape(item, ["path", "identity", "entitlements", "runtime", "timestamp", "force", "preserveEntitlements"]);
|
|
109
|
+
if (typeof item.identity !== "string" || !item.identity.trim() || item.identity === "-" || item.identity.includes("\0") || item.identity.length > 512) fail("MAC_SIGNING_AGENT_PROTOCOL", "Real signing identity required");
|
|
110
|
+
for (const field of ["runtime", "timestamp", "force", "preserveEntitlements"]) if (item[field] !== undefined && typeof item[field] !== "boolean") fail("MAC_SIGNING_AGENT_PROTOCOL", "Signing options must be boolean");
|
|
111
|
+
return { ...item, path: checked(item.path), ...(item.entitlements !== undefined ? { entitlements: checked(item.entitlements, { entitlements: true }) } : {}) };
|
|
112
|
+
});
|
|
113
|
+
return { ...request, cwd, items };
|
|
114
|
+
}
|
|
115
|
+
export function authenticateSigningToken(candidate, token) {
|
|
116
|
+
if (typeof candidate !== "string" || !/^[a-f0-9]{64}$/.test(candidate) || !timingSafeEqual(Buffer.from(candidate), Buffer.from(token))) {
|
|
117
|
+
fail("MAC_SIGNING_AGENT_AUTH", "Signing agent authentication failed");
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
export function executeSigningRequest(request, { run = spawnSync, home = os.userInfo().homedir,
|
|
121
|
+
username = os.userInfo().username, pnpm, environmentPath, now = Date.now } = {}) {
|
|
122
|
+
if (request.type === "health") return { schema: 1, ok: true, service: AGENT_LABEL, pid: process.pid };
|
|
123
|
+
const deadline = now() + request.timeoutMs;
|
|
124
|
+
const env = { HOME: home, PATH: environmentPath, TMPDIR: path.join(signingAgentPaths(home).directory, "tmp") };
|
|
125
|
+
const results = [];
|
|
126
|
+
const outputLimit = Math.min(4096, Math.floor(MAX_FRAME / (4 * (request.items?.length ?? request.files.length))));
|
|
127
|
+
const invoke = (command, args, sensitive = false) => {
|
|
128
|
+
const remaining = deadline - now();
|
|
129
|
+
if (remaining <= 0) fail("MAC_SIGNING_AGENT_TIMEOUT", "Signing deadline expired");
|
|
130
|
+
const r = run(command, args, { cwd: request.cwd, env, encoding: "utf8", stdio: "pipe", shell: false,
|
|
131
|
+
timeout: remaining, killSignal: "SIGKILL", maxBuffer: MAX_FRAME });
|
|
132
|
+
const result = { status: r.error ? 1 : r.status ?? 1, stdout: sensitive ? "" : String(r.stdout ?? "").slice(-outputLimit),
|
|
133
|
+
stderr: sensitive ? "" : String(r.stderr ?? "").slice(-outputLimit) };
|
|
134
|
+
if (result.status !== 0) fail("MAC_SIGNING_AGENT_JOB_FAILED", sensitive ? "Updater signing failed" : `Codesign failed (exit ${result.status}): ${result.stderr}`);
|
|
135
|
+
return { r, result };
|
|
136
|
+
};
|
|
137
|
+
try {
|
|
138
|
+
if (request.type === "updater") {
|
|
139
|
+
const { r } = invoke("/usr/bin/security", ["find-generic-password", "-a", username, "-s", "rightsuite-updater-key", "-w"], true);
|
|
140
|
+
const key = String(r.stdout ?? "").trim();
|
|
141
|
+
if (!key) fail("MAC_SIGNING_UPDATER_KEY_UNAVAILABLE", "Shared updater key unavailable");
|
|
142
|
+
env.TAURI_SIGNING_PRIVATE_KEY = key;
|
|
143
|
+
env.TAURI_SIGNING_PRIVATE_KEY_PASSWORD = "";
|
|
144
|
+
for (const file of request.files) results.push(invoke(pnpm, ["exec", "tauri", "signer", "sign", file], true).result);
|
|
145
|
+
} else {
|
|
146
|
+
// Preserve caller's inside-out ordering. No arbitrary argv reaches spawn.
|
|
147
|
+
for (const item of request.items) {
|
|
148
|
+
const args = [...(item.force ? ["--force"] : []), ...(item.runtime ? ["--options", "runtime"] : []),
|
|
149
|
+
...(item.timestamp ? ["--timestamp"] : []), "--sign", item.identity,
|
|
150
|
+
...(item.entitlements ? ["--entitlements", item.entitlements] : []),
|
|
151
|
+
...(item.preserveEntitlements ? ["--preserve-metadata=entitlements"] : []), item.path];
|
|
152
|
+
results.push(invoke("/usr/bin/codesign", args).result);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return { schema: 1, ok: true, results };
|
|
156
|
+
} catch (error) {
|
|
157
|
+
if (request.type === "updater") fail(error.code?.startsWith("MAC_SIGNING_") ? error.code : "MAC_SIGNING_AGENT_JOB_FAILED", "Updater signing failed inside signing agent");
|
|
158
|
+
throw error;
|
|
159
|
+
} finally { delete env.TAURI_SIGNING_PRIVATE_KEY; delete env.TAURI_SIGNING_PRIVATE_KEY_PASSWORD; }
|
|
160
|
+
}
|
|
161
|
+
export function managedSigningLog(file, event, { io = fs } = {}) {
|
|
162
|
+
if (io.existsSync(file)) {
|
|
163
|
+
privatePath(file, 0o600, { io });
|
|
164
|
+
if (io.statSync(file).size > 5 * 1024 * 1024) {
|
|
165
|
+
const previous = `${file}.1`;
|
|
166
|
+
if (io.existsSync(previous)) { privatePath(previous, 0o600, { io }); io.unlinkSync(previous); }
|
|
167
|
+
io.renameSync(file, previous);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// Only service-authored metadata is logged, never request/token/key/argv/output.
|
|
171
|
+
io.appendFileSync(file, `${JSON.stringify({ at: new Date().toISOString(), ...event })}\n`, { mode: 0o600 });
|
|
172
|
+
}
|
|
173
|
+
export async function startSigningAgent({ paths = signingAgentPaths(), config, io = fs, run = spawnSync,
|
|
174
|
+
log = (event) => managedSigningLog(paths.log, event), createServer = net.createServer } = {}) {
|
|
175
|
+
privatePath(paths.directory, 0o700, { io, kind: "directory" });
|
|
176
|
+
privatePath(paths.config, 0o600, { io });
|
|
177
|
+
config ??= JSON.parse(io.readFileSync(paths.config, "utf8"));
|
|
178
|
+
const token = randomBytes(32).toString("hex"); // Rotate on every start, including every boot.
|
|
179
|
+
for (const file of [paths.socket, paths.token]) {
|
|
180
|
+
try { io.lstatSync(file); } catch (error) { if (error.code === "ENOENT") continue; throw error; }
|
|
181
|
+
privatePath(file, 0o600, { io, kind: file === paths.socket ? "socket" : "file" });
|
|
182
|
+
io.unlinkSync(file);
|
|
183
|
+
}
|
|
184
|
+
io.writeFileSync(paths.token, token, { mode: 0o600, flag: "wx" });
|
|
185
|
+
let queue = Promise.resolve();
|
|
186
|
+
let pending = 0;
|
|
187
|
+
const server = createServer((socket) => {
|
|
188
|
+
socket.on("error", () => {});
|
|
189
|
+
socket.setTimeout(200000, () => socket.destroy());
|
|
190
|
+
let input = Buffer.alloc(0), submitted = false;
|
|
191
|
+
socket.on("data", (chunk) => {
|
|
192
|
+
if (submitted) { log({ event: "request.rejected", code: "MAC_SIGNING_AGENT_PROTOCOL" }); socket.destroy(); return; }
|
|
193
|
+
input = Buffer.concat([input, chunk]);
|
|
194
|
+
if (input.length > MAX_FRAME) { log({ event: "request.rejected", code: "MAC_SIGNING_AGENT_PROTOCOL" }); socket.destroy(); return; }
|
|
195
|
+
const end = input.indexOf(10);
|
|
196
|
+
if (end < 0) return;
|
|
197
|
+
submitted = true;
|
|
198
|
+
const id = randomUUID();
|
|
199
|
+
let request;
|
|
200
|
+
const respond = (report) => { if (!socket.destroyed) socket.end(`${JSON.stringify(report)}\n`); };
|
|
201
|
+
try {
|
|
202
|
+
if (end !== input.length - 1) fail("MAC_SIGNING_AGENT_PROTOCOL", "One request per connection required");
|
|
203
|
+
request = JSON.parse(input.subarray(0, end).toString("utf8"));
|
|
204
|
+
authenticateSigningToken(request?.token, token);
|
|
205
|
+
request = validateSigningRequest(request, { roots: config.roots, stagingRoot: path.join(paths.directory, "requests"), io });
|
|
206
|
+
if (pending >= 32) fail("MAC_SIGNING_AGENT_BUSY", "Signing queue is full");
|
|
207
|
+
} catch (error) {
|
|
208
|
+
const code = error.code?.startsWith("MAC_SIGNING_") ? error.code : "MAC_SIGNING_AGENT_PROTOCOL";
|
|
209
|
+
log({ event: "request.rejected", id, code });
|
|
210
|
+
respond({ schema: 1, ok: false, code, message: "Signing request rejected" });
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const receivedAt = Date.now();
|
|
214
|
+
pending++;
|
|
215
|
+
log({ event: "request.accepted", id, type: request.type, count: request.items?.length ?? request.files?.length ?? 0 });
|
|
216
|
+
queue = queue.then(() => {
|
|
217
|
+
if (socket.destroyed) { log({ event: "request.cancelled", id, type: request.type }); return; }
|
|
218
|
+
log({ event: "request.started", id, type: request.type });
|
|
219
|
+
try {
|
|
220
|
+
if (request.type !== "health") {
|
|
221
|
+
request.timeoutMs -= Date.now() - receivedAt;
|
|
222
|
+
if (request.timeoutMs <= 0) fail("MAC_SIGNING_AGENT_TIMEOUT", "Signing deadline expired in queue");
|
|
223
|
+
// Revalidate immediately before effects, not merely on admission.
|
|
224
|
+
request = validateSigningRequest(request, { roots: config.roots, stagingRoot: path.join(paths.directory, "requests"), io });
|
|
225
|
+
}
|
|
226
|
+
const report = executeSigningRequest(request, { run, home: config.home, username: config.username, pnpm: config.pnpm, environmentPath: config.environmentPath });
|
|
227
|
+
respond(report);
|
|
228
|
+
log({ event: "request.completed", id, type: request.type });
|
|
229
|
+
} catch (error) {
|
|
230
|
+
const code = error.code?.startsWith("MAC_SIGNING_") ? error.code : "MAC_SIGNING_AGENT_JOB_FAILED";
|
|
231
|
+
respond({ schema: 1, ok: false, code, message: request.type === "updater" ? "Updater signing failed" : error.message });
|
|
232
|
+
log({ event: "request.failed", id, type: request.type, code });
|
|
233
|
+
}
|
|
234
|
+
}).finally(() => { pending--; }).catch(() => { socket.destroy(); });
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
await new Promise((resolve, reject) => { server.once("error", reject); server.listen(paths.socket, resolve); });
|
|
238
|
+
io.chmodSync(paths.socket, 0o600);
|
|
239
|
+
log({ event: "service.started", pid: process.pid });
|
|
240
|
+
server.on("error", () => { log({ event: "service.failed", code: "MAC_SIGNING_AGENT_SOCKET" }); });
|
|
241
|
+
return server;
|
|
242
|
+
}
|
|
243
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
244
|
+
process.umask(0o077);
|
|
245
|
+
startSigningAgent().then((server) => {
|
|
246
|
+
const stop = () => { managedSigningLog(signingAgentPaths().log, { event: "service.stopping" }); server.close(() => process.exit(0)); setTimeout(() => process.exit(0), 2000).unref(); };
|
|
247
|
+
process.on("SIGTERM", stop); process.on("SIGINT", stop);
|
|
248
|
+
}).catch((error) => {
|
|
249
|
+
const event = { event: "service.failed", code: error.code ?? "MAC_SIGNING_AGENT_START_FAILED" };
|
|
250
|
+
try { managedSigningLog(signingAgentPaths().log, event); } catch {}
|
|
251
|
+
console.error(JSON.stringify(event)); process.exitCode = 1;
|
|
252
|
+
});
|
|
253
|
+
}
|