@rightkit/release 0.2.132 → 0.2.134
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 +129 -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 +44 -19
- package/rightkit-signing-agent.mjs +253 -0
- package/rightkit-versions.json +39 -27
- 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, declared: config.storage?.declaredStores ?? [], 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.
|
|
@@ -264,8 +281,9 @@ if (opts.platform === "win") {
|
|
|
264
281
|
});
|
|
265
282
|
}
|
|
266
283
|
} else if (opts.platform === "mac") {
|
|
267
|
-
const signingFiles = target.sign.prePackageFiles.map((p) => path.resolve(root, p));
|
|
268
284
|
if (runMacPhase("prepackage")) await runCommand(target.prePackage, root);
|
|
285
|
+
// Resolve after prePackage: with storage governance the artifact may live only in RIGHT_RELEASE_ARTIFACT_DIR.
|
|
286
|
+
const signingFiles = target.sign.prePackageFiles.map((p) => resolveReleaseArtifactPath(root, p));
|
|
269
287
|
for (const file of signingFiles) await mustExist(file, `missing macOS Developer ID signing artifact: ${file}`);
|
|
270
288
|
if (runMacPhase("sign")) await signMacos(signingFiles, root);
|
|
271
289
|
} else if (nativeAssembly) {
|
|
@@ -284,13 +302,13 @@ if (opts.platform === "win") {
|
|
|
284
302
|
if (opts.platform !== "mac" || runMacPhase("package")) await runCommand(command, root);
|
|
285
303
|
|
|
286
304
|
for (const rel of target.artifacts ?? []) {
|
|
287
|
-
await mustExist(
|
|
305
|
+
await mustExist(resolveReleaseArtifactPath(root, rel), `missing release artifact: ${rel}`);
|
|
288
306
|
}
|
|
289
307
|
|
|
290
308
|
if (nativeFinalization) await completeNativeFinalization(nativeFinalization, { root, target, nativeAssembly });
|
|
291
309
|
|
|
292
310
|
if (opts.platform === "win" && target.signingContract === WINDOWS_NSIS_SIGNING_CONTRACT) {
|
|
293
|
-
const files = target.sign.files.map((p) =>
|
|
311
|
+
const files = target.sign.files.map((p) => resolveReleaseArtifactPath(root, p));
|
|
294
312
|
for (const file of files) await mustExist(file, `missing signing artifact: ${file}`);
|
|
295
313
|
// Before the installer earns its own signature, prove it carries the bytes we
|
|
296
314
|
// signed. An installer-only signature on a mutated payload verifies perfectly
|
|
@@ -298,7 +316,7 @@ if (opts.platform === "win" && target.signingContract === WINDOWS_NSIS_SIGNING_C
|
|
|
298
316
|
await verifyWindowsEmbedding({ root, target, files, markers: bundleMarkerReceipts });
|
|
299
317
|
await signWindows(files, "installer", root);
|
|
300
318
|
if (target.postSign) await runCommand(target.postSign, root);
|
|
301
|
-
const updaterFiles = [...new Set((target.updater?.artifacts ?? []).map((artifact) =>
|
|
319
|
+
const updaterFiles = [...new Set((target.updater?.artifacts ?? []).map((artifact) => resolveReleaseArtifactPath(root, artifact.file)))];
|
|
302
320
|
if (!updaterFiles.length) fail(`${config.app} win must declare updater.artifacts for post-code-signing minisign`);
|
|
303
321
|
await run("node", [SIGN_UPDATER, ...(opts.dryRun ? ["--dry-run"] : []), ...updaterFiles.flatMap((file) => ["--file", file])], root);
|
|
304
322
|
}
|
|
@@ -308,7 +326,7 @@ if (target.postSign && opts.platform !== "win" && (opts.platform !== "mac" || ru
|
|
|
308
326
|
}
|
|
309
327
|
|
|
310
328
|
if (opts.platform === "mac") {
|
|
311
|
-
const archive =
|
|
329
|
+
const archive = resolveReleaseArtifactPath(root, target.notarize.file);
|
|
312
330
|
await mustExist(archive, `missing macOS notarization archive: ${archive}`);
|
|
313
331
|
await notarizeMacos(archive, root);
|
|
314
332
|
}
|
|
@@ -326,7 +344,7 @@ if (opts.platform !== "mac" || runMacPhase("harden")) {
|
|
|
326
344
|
await run("node", [
|
|
327
345
|
HARDENING_SCAN,
|
|
328
346
|
...(hardeningEvidence ? ["--allow-evidence", hardeningEvidence] : []),
|
|
329
|
-
...scanTargets.map((p) =>
|
|
347
|
+
...scanTargets.map((p) => resolveReleaseArtifactPath(root, p)),
|
|
330
348
|
], root);
|
|
331
349
|
}
|
|
332
350
|
}
|
|
@@ -337,7 +355,7 @@ if (opts.upload && target.publish) {
|
|
|
337
355
|
|
|
338
356
|
if (opts.upload && !target.publish) {
|
|
339
357
|
for (const item of target.upload ?? []) {
|
|
340
|
-
const file =
|
|
358
|
+
const file = resolveReleaseArtifactPath(root, item.file);
|
|
341
359
|
await mustExist(file, `missing upload artifact: ${item.file}`);
|
|
342
360
|
await run("node", [UPLOAD_LARGE, file, item.key, item.bucket ?? "public"], root);
|
|
343
361
|
}
|
|
@@ -358,6 +376,20 @@ function usage(code, message) {
|
|
|
358
376
|
process.exit(code);
|
|
359
377
|
}
|
|
360
378
|
|
|
379
|
+
// Product package scripts may emit final installer bytes directly into the
|
|
380
|
+
// managed artifact directory. Keep legacy target-relative paths working while
|
|
381
|
+
// preferring managed output when the local path does not exist.
|
|
382
|
+
function resolveReleaseArtifactPath(root, declared) {
|
|
383
|
+
const local = path.resolve(root, declared);
|
|
384
|
+
if (existsSync(local)) return local;
|
|
385
|
+
const managed = process.env.RIGHT_RELEASE_ARTIFACT_DIR;
|
|
386
|
+
if (managed) {
|
|
387
|
+
const candidate = path.join(path.resolve(managed), path.basename(declared));
|
|
388
|
+
if (existsSync(candidate)) return candidate;
|
|
389
|
+
}
|
|
390
|
+
return local;
|
|
391
|
+
}
|
|
392
|
+
|
|
361
393
|
function fail(message) {
|
|
362
394
|
console.error(`right-release: ${message}`);
|
|
363
395
|
process.exit(1);
|
|
@@ -793,9 +825,7 @@ async function signWindows(files, phase, root) {
|
|
|
793
825
|
}
|
|
794
826
|
|
|
795
827
|
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`);
|
|
828
|
+
return signingReceiptPath({ appRoot: root, target, platform: "win", phase, stateRoot: process.env.RIGHT_RELEASE_STATE_ROOT });
|
|
799
829
|
}
|
|
800
830
|
|
|
801
831
|
async function signMacos(files, root) {
|
|
@@ -815,6 +845,7 @@ async function notarizeMacos(archive, root) {
|
|
|
815
845
|
if (opts.dryRun) args.push("--dry-run");
|
|
816
846
|
args.push("--receipt", receipt);
|
|
817
847
|
if (target.notarize.profile) args.push("--profile", target.notarize.profile);
|
|
848
|
+
if (target.notarize.app) args.push("--app", resolveReleaseArtifactPath(root, target.notarize.app));
|
|
818
849
|
args.push(archive);
|
|
819
850
|
if (!opts.dryRun && existsSync(receipt)) unlinkSync(receipt);
|
|
820
851
|
console.log("right-release: notarizing macOS archive");
|
|
@@ -823,9 +854,7 @@ async function notarizeMacos(archive, root) {
|
|
|
823
854
|
}
|
|
824
855
|
|
|
825
856
|
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`);
|
|
857
|
+
return signingReceiptPath({ appRoot: root, target, platform: "mac", phase, stateRoot: process.env.RIGHT_RELEASE_STATE_ROOT });
|
|
829
858
|
}
|
|
830
859
|
|
|
831
860
|
function verifyMacosSigningReceipt(receipt, files) {
|
|
@@ -843,12 +872,8 @@ function verifyMacosSigningReceipt(receipt, files) {
|
|
|
843
872
|
}
|
|
844
873
|
|
|
845
874
|
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}`);
|
|
875
|
+
try { validateNotarizationReceipt({ receiptPath: receipt, archive }); }
|
|
876
|
+
catch (error) { fail(error.message); }
|
|
852
877
|
}
|
|
853
878
|
|
|
854
879
|
/**
|
|
@@ -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
|
+
}
|