@rightkit/release 0.2.134 → 0.2.136
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -0
- package/addon-command.mjs +53 -34
- package/apple-profile-renew.mjs +30 -12
- package/artifact-storage.mjs +53 -12
- package/background-process.mjs +18 -0
- package/build-release.mjs +4 -3
- package/cache-command.mjs +4 -3
- package/cargo-contract.mjs +1 -1
- package/cargo-guard.mjs +4 -3
- package/cargo-registry-readback.mjs +51 -0
- package/cargo-target.mjs +3 -1
- package/cli/right-release.mjs +26 -0
- package/codesign-argv.mjs +64 -0
- package/codesign-environment.mjs +58 -0
- package/codesign-shim.mjs +51 -0
- package/committed-source.mjs +3 -3
- package/development.mjs +11 -9
- package/direct-bootstrap.mjs +19 -9
- package/dmg-build.mjs +117 -0
- package/dmg-layout.mjs +123 -0
- package/github-release.mjs +74 -37
- package/heavy-command.mjs +3 -2
- package/install-app.mjs +358 -0
- package/inventory-check.mjs +980 -0
- package/macos-notarization.mjs +24 -8
- package/macos-signing-session.mjs +50 -2
- package/managed-cargo.mjs +23 -19
- package/managed-host-secrets.mjs +30 -0
- package/managed-roots.mjs +39 -0
- package/model-promote.mjs +31 -37
- package/notarize-macos.mjs +5 -5
- package/notary-auth.mjs +4 -0
- package/osascript-guard.mjs +24 -0
- package/ownership-approval-command.mjs +15 -4
- package/ownership-approval.mjs +114 -49
- package/ownership-availability.mjs +25 -0
- package/ownership-catalog-config.json +18 -0
- package/ownership-catalog.mjs +94 -0
- package/ownership-command.mjs +17 -10
- package/ownership-declaration.json +15 -0
- package/ownership-default-policy.json +1 -11
- package/ownership-evidence.mjs +10 -8
- package/ownership-git-inventory.mjs +29 -1
- package/ownership-helper-paths.json +4 -4
- package/ownership-hooks.mjs +107 -0
- package/ownership-internal-owners.json +44 -0
- package/ownership-key-authority.mjs +112 -7
- package/ownership-key-command.mjs +76 -19
- package/ownership-key-trust.mjs +97 -9
- package/ownership-lockfiles.mjs +311 -0
- package/ownership-report-command.mjs +12 -3
- package/ownership-report.mjs +20 -17
- package/ownership-repositories.json +19 -0
- package/ownership-scan-worker.mjs +96 -11
- package/ownership-source.mjs +39 -0
- package/ownership-staged.mjs +193 -0
- package/ownership-verdict.mjs +74 -0
- package/ownership.mjs +215 -107
- package/package.json +1 -1
- package/protected-credentials.mjs +111 -0
- package/prune-r2.mjs +4 -3
- package/pub-upload.mjs +3 -85
- package/publication-artifacts.mjs +108 -0
- package/publication-gate.mjs +28 -7
- package/publish-cargo.mjs +47 -16
- package/publish-swift.mjs +46 -17
- package/publish-update.mjs +61 -39
- package/r2-verified-upload.mjs +41 -0
- package/registry-parity.mjs +5 -3
- package/release-state.mjs +1 -0
- package/release-token.mjs +2 -0
- package/release.mjs +2 -1
- package/rightkit-drift.mjs +3 -2
- package/rightkit-ownership.json +2028 -139
- package/rightkit-signing-agent.mjs +161 -19
- package/rightkit-sync.mjs +6 -2
- package/rightkit-versions.json +220 -106
- package/runtime-artifact-manifest.mjs +20 -6
- package/sign-updater.mjs +5 -0
- package/signing-agent-client.mjs +78 -22
- package/signing-agent-command.mjs +5 -4
- package/signing-agent-install.mjs +31 -4
- package/standalone-clone-verify.mjs +3 -2
- package/tauri-bundle-marker.mjs +3 -2
- package/upload-large.mjs +36 -82
- package/verifier.mjs +5 -1
package/macos-notarization.mjs
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { mkdirSync, readFileSync, realpathSync, statSync, writeFileSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { spawnSync } from "./background-process.mjs";
|
|
5
5
|
import { MACOS_PORTABLE_SIGNING_CONTRACT } from "./macos-signing-contract.mjs";
|
|
6
|
+
import { detectMacSigningSession, runMacGuiNotarization } from "./macos-signing-session.mjs";
|
|
7
|
+
import { notarytoolAuthArgs } from "./notary-auth.mjs";
|
|
6
8
|
|
|
7
9
|
function checkedRun(args, run) {
|
|
8
10
|
const result = run("xcrun", args, { encoding: "utf8" });
|
|
@@ -10,9 +12,13 @@ function checkedRun(args, run) {
|
|
|
10
12
|
return result.stdout ?? "";
|
|
11
13
|
}
|
|
12
14
|
|
|
13
|
-
export function stapleMacosArtifact(file,
|
|
15
|
+
export function stapleMacosArtifact(file, options = {}) {
|
|
16
|
+
const { run = spawnSync, env = process.env } = options;
|
|
14
17
|
file = path.resolve(file);
|
|
15
18
|
if (!/\.(dmg|pkg|app)$/i.test(file)) throw new Error(`not a stapleable macOS artifact: ${file}`);
|
|
19
|
+
if (detectMacSigningSession({ env, run }).guiRunner) {
|
|
20
|
+
return { ...runMacGuiNotarization({ ...options, kind: "staple", file }).stapling[0], file };
|
|
21
|
+
}
|
|
16
22
|
const probe = run("xcrun", ["stapler", "validate", file], { encoding: "utf8" });
|
|
17
23
|
if (probe.error) throw new Error(`cannot validate macOS staple: ${probe.error.message}`);
|
|
18
24
|
const alreadyStapled = probe.status === 0;
|
|
@@ -27,7 +33,7 @@ function fileEvidence(file) {
|
|
|
27
33
|
|
|
28
34
|
// DMG/PKG receive their own ticket. For a separately submitted app ZIP,
|
|
29
35
|
// target.notarize.app (CLI --app) identifies the bundle to staple before packaging.
|
|
30
|
-
export function notarizeMacosArtifact({ archive, receiptPath, authArgs, app, run = spawnSync }) {
|
|
36
|
+
export function notarizeMacosArtifact({ archive, receiptPath, authArgs, profile, app, run = spawnSync, env = process.env, ...agentOptions }) {
|
|
31
37
|
archive = path.resolve(archive);
|
|
32
38
|
const stapleTargets = /\.(dmg|pkg)$/i.test(archive) ? [archive] : [];
|
|
33
39
|
if (app) {
|
|
@@ -36,11 +42,21 @@ export function notarizeMacosArtifact({ archive, receiptPath, authArgs, app, run
|
|
|
36
42
|
stapleTargets.push(bundle);
|
|
37
43
|
}
|
|
38
44
|
if (!stapleTargets.length) throw new Error("notarization archives require notarize.app (or --app) for stapling their app bundle");
|
|
39
|
-
let result;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
45
|
+
let result, stapling;
|
|
46
|
+
if (detectMacSigningSession({ env, run }).guiRunner) {
|
|
47
|
+
// SSH ignores API credential environment/argv entirely. Only profile name
|
|
48
|
+
// reaches Aqua, where Keychain authentication already works.
|
|
49
|
+
const selectedProfile = profile ?? (authArgs?.[0] === "--keychain-profile" && authArgs.length === 2 ? authArgs[1] : undefined);
|
|
50
|
+
const report = runMacGuiNotarization({ ...agentOptions, kind: "notarize", file: archive, app, profile: selectedProfile, env, run });
|
|
51
|
+
result = report.notarization;
|
|
52
|
+
stapling = report.stapling.map((item) => ({ ...item, file: stapleTargets.find((file) => realpathSync.native(file) === item.file) ?? item.file }));
|
|
53
|
+
} else {
|
|
54
|
+
authArgs ??= notarytoolAuthArgs({ profile, env });
|
|
55
|
+
try { result = JSON.parse(checkedRun(["notarytool", "submit", archive, "--wait", "--output-format", "json", ...authArgs], run)); }
|
|
56
|
+
catch (error) { throw new Error(`macOS notarization failed for ${archive}: ${error.message}`); }
|
|
57
|
+
if (result.status !== "Accepted") throw new Error(`notarization was not accepted for ${archive}: ${result.status ?? "missing status"}`);
|
|
58
|
+
stapling = [...new Set(stapleTargets)].map((file) => stapleMacosArtifact(file, { run, env }));
|
|
59
|
+
}
|
|
44
60
|
// Stapling changes container bytes. Record final bytes, never the submission digest.
|
|
45
61
|
const receipt = {
|
|
46
62
|
schema: 1,
|
|
@@ -3,9 +3,10 @@ import os from "node:os";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { spawnSync } from "./background-process.mjs";
|
|
5
5
|
|
|
6
|
-
import { requestSigningAgentSync } from "./signing-agent-client.mjs";
|
|
6
|
+
import { requestSigningAgentSync, validateNotaryAgentReport } from "./signing-agent-client.mjs";
|
|
7
7
|
import { installSigningAgent, requireMacGuiSession } from "./signing-agent-install.mjs";
|
|
8
|
-
import { signingAgentPaths, defaultSigningRoots, ensurePrivateDirectory, privatePath, within } from "./rightkit-signing-agent.mjs";
|
|
8
|
+
import { signingAgentPaths, defaultSigningRoots, ensurePrivateDirectory, privatePath, within, NOTARIZATION_TIMEOUT_MS, MAX_NOTARIZATION_TIMEOUT_MS } from "./rightkit-signing-agent.mjs";
|
|
9
|
+
import { refuseManagedEnvSecrets } from "./managed-host-secrets.mjs";
|
|
9
10
|
|
|
10
11
|
export class MacSigningSessionError extends Error {
|
|
11
12
|
constructor(code, message) {
|
|
@@ -126,8 +127,55 @@ export function runMacCodesignCommands(commands, options = {}) {
|
|
|
126
127
|
return results;
|
|
127
128
|
}
|
|
128
129
|
|
|
130
|
+
export function runMacGuiNotarization({ kind = "notarize", file, profile = "apple-dev-notary", app, validateOnly,
|
|
131
|
+
cwd = process.cwd(), env = process.env, run = spawnSync, io = fs, home = os.userInfo().homedir,
|
|
132
|
+
uid = process.getuid?.(), username = os.userInfo().username, allowedRoots, timeoutMs = NOTARIZATION_TIMEOUT_MS,
|
|
133
|
+
request = requestSigningAgentSync, install = installSigningAgent } = {}) {
|
|
134
|
+
if (!["notarize", "staple"].includes(kind) || !Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > MAX_NOTARIZATION_TIMEOUT_MS) throw new TypeError("Invalid notarization request or timeout");
|
|
135
|
+
if (kind === "notarize" && (typeof profile !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_. -]{0,127}$/.test(profile))) throw new TypeError("Notarization requires a profile name only");
|
|
136
|
+
requireMacGuiSession({ run, uid });
|
|
137
|
+
const paths = signingAgentPaths(home);
|
|
138
|
+
let roots = allowedRoots ?? defaultSigningRoots(home, env.RIGHTKIT_MANAGED_ROOT);
|
|
139
|
+
if (!allowedRoots && io.existsSync(paths.config)) {
|
|
140
|
+
privatePath(paths.config, 0o600, { io, uid });
|
|
141
|
+
roots = JSON.parse(io.readFileSync(paths.config, "utf8")).roots;
|
|
142
|
+
}
|
|
143
|
+
const plan = { schema: 1, type: kind, path: path.resolve(file), cwd: path.resolve(cwd),
|
|
144
|
+
allowedRoots: roots.filter((root) => io.existsSync(root)), timeoutMs,
|
|
145
|
+
...(kind === "notarize" ? { profile, staple: true, ...(app ? { app: path.resolve(app) } : {}) } : { validateOnly: validateOnly === true }) };
|
|
146
|
+
const options = { home, timeoutMs: timeoutMs + 5000 };
|
|
147
|
+
let report;
|
|
148
|
+
try { report = request(plan, options); }
|
|
149
|
+
catch (error) {
|
|
150
|
+
let missing = error.code === "MAC_SIGNING_AGENT_UNAVAILABLE";
|
|
151
|
+
// Upgrade earlier installed agents once; never turn a rejected path or
|
|
152
|
+
// malformed request into an install/retry loop.
|
|
153
|
+
if (error.code === "MAC_SIGNING_AGENT_PROTOCOL") {
|
|
154
|
+
const health = request({ schema: 1, type: "health" }, { home, timeoutMs: 1500 });
|
|
155
|
+
missing = !health.capabilities?.includes(kind);
|
|
156
|
+
}
|
|
157
|
+
if (!missing) throw error;
|
|
158
|
+
install({ home, username, uid, run, io, managedRoot: env.RIGHTKIT_MANAGED_ROOT });
|
|
159
|
+
report = request(plan, options);
|
|
160
|
+
}
|
|
161
|
+
validateNotaryAgentReport(report);
|
|
162
|
+
const targets = kind === "staple" ? [plan.path] : [...new Set([...( /\.(dmg|pkg)$/i.test(plan.path) ? [plan.path] : []), ...(plan.app ? [plan.app] : [])])];
|
|
163
|
+
const canonical = (value) => (io.realpathSync.native ?? io.realpathSync)(value);
|
|
164
|
+
if (!report.ok || (kind === "notarize" && (report.notarization.status !== "Accepted" || !report.notarization.id
|
|
165
|
+
|| !report.steps.some((step) => step.step === "submit" && step.exitCode === 0)))
|
|
166
|
+
|| report.stapling.length !== targets.length || targets.some((target) => !report.stapling.some((item) => item.file === canonical(target)
|
|
167
|
+
&& report.steps.some((step) => step.step === "validate" && step.file === item.file && step.exitCode === 0)
|
|
168
|
+
&& (kind !== "notarize" || report.steps.some((step) => step.step === "staple" && step.file === item.file && step.exitCode === 0))))
|
|
169
|
+
|| report.steps.some((step) => step.step !== "probe" && step.exitCode !== 0)) {
|
|
170
|
+
throw new MacSigningSessionError("MAC_SIGNING_AGENT_PROTOCOL", "Signing agent returned incomplete notarization or stapling evidence");
|
|
171
|
+
}
|
|
172
|
+
return report;
|
|
173
|
+
}
|
|
174
|
+
|
|
129
175
|
export function signMacUpdaterFiles(files, options = {}) {
|
|
130
176
|
const { cwd = process.cwd(), env = process.env, run = spawnSync, username = os.userInfo().username } = options;
|
|
177
|
+
// Inherited updater key material must not bypass the Keychain or GUI signing agent on managed hosts.
|
|
178
|
+
refuseManagedEnvSecrets(env, ["TAURI_SIGNING_PRIVATE_KEY", "TAURI_SIGNING_PRIVATE_KEY_PASSWORD"]);
|
|
131
179
|
const commands = files.map((file) => ["exec", "tauri", "signer", "sign", file]);
|
|
132
180
|
const signingEnv = { ...env, TAURI_SIGNING_PRIVATE_KEY_PASSWORD: env.TAURI_SIGNING_PRIVATE_KEY_PASSWORD ?? "" };
|
|
133
181
|
if (env.SSH_CONNECTION || env.SSH_TTY || env.SSH_CLIENT || (!signingEnv.TAURI_SIGNING_PRIVATE_KEY && detectMacSigningSession({ env, run }).guiRunner)) {
|
package/managed-cargo.mjs
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
+
import { discoverBrokerSocket, managedControlCandidates, resolveManagedRoots } from './managed-roots.mjs';
|
|
1
2
|
import { randomUUID } from "node:crypto";
|
|
2
3
|
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { spawnSync } from "./background-process.mjs";
|
|
5
6
|
|
|
6
7
|
export function managedRuntimeClient(env = process.env, platform = process.platform, exists = existsSync) {
|
|
7
|
-
const roots =
|
|
8
|
-
|
|
9
|
-
.filter((root) => typeof root === "string" && root.length > 0);
|
|
8
|
+
const roots = managedControlCandidates({ env, platform });
|
|
9
|
+
const paths = platform === 'win32' ? path.win32 : path.posix;
|
|
10
10
|
for (const root of roots) {
|
|
11
|
-
const client =
|
|
11
|
+
const client = paths.join(root, "runtime-current", "build-control-clients", "src", "rightkit.mjs");
|
|
12
12
|
if (exists(client)) return client;
|
|
13
13
|
}
|
|
14
14
|
return undefined;
|
|
@@ -19,7 +19,7 @@ export function managedRuntimeClient(env = process.env, platform = process.platf
|
|
|
19
19
|
export function acquireManagedRunLease({ command, env = process.env, platform = process.platform, exists = existsSync, pid = process.pid } = {}) {
|
|
20
20
|
const client = managedRuntimeClient(env, platform, exists);
|
|
21
21
|
if (!client) return { path: null, release() {} };
|
|
22
|
-
const runs = path.join(
|
|
22
|
+
const runs = path.join(resolveManagedRoots({ env, platform }).dataRoot, "rightkit-build-control", "global", "runs");
|
|
23
23
|
const file = path.join(runs, `${pid}-${randomUUID()}.json`);
|
|
24
24
|
try {
|
|
25
25
|
mkdirSync(runs, { recursive: true });
|
|
@@ -31,24 +31,28 @@ export function acquireManagedRunLease({ command, env = process.env, platform =
|
|
|
31
31
|
return { path: file, release() { rmSync(file, { force: true }); } };
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
function managedHost(env) {
|
|
34
|
+
export function managedHost(env) {
|
|
35
35
|
return Boolean(env.RIGHTKIT_BUILD_BROKER_SOCKET)
|
|
36
|
-
|| /(?:^|[;:])[^;:]*[/\\](?:\.rightkit-managed|rightkitmanagedagent)[/\\]agent-bin(?:[;:]|$)/i.test(String(env.PATH ?? ""));
|
|
36
|
+
|| /(?:^|[;:])[^;:]*[/\\](?:\.rightkit-managed|rightkitmanagedagent|rightkit[/\\]build-control)[/\\]agent-bin(?:[;:]|$)/i.test(String(env.PATH ?? ""));
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
export function runCargoSync(args, options = {}) {
|
|
39
|
+
export function runCargoSync(args, options = {}, { platform = process.platform, exists = existsSync, spawn = spawnSync } = {}) {
|
|
40
40
|
const env = options.env ?? process.env;
|
|
41
|
-
const client = managedRuntimeClient(env);
|
|
41
|
+
const client = managedRuntimeClient(env, platform, exists);
|
|
42
42
|
if (client) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
return { status: null, stdout: "", stderr: "", error: new Error("installed RightKit runtime client is unavailable for managed Cargo metadata") };
|
|
43
|
+
const socket = discoverBrokerSocket(env, { platform, exists });
|
|
44
|
+
if (platform === 'darwin' && (!socket || !exists(socket))) return unavailable('RightKit broker socket is unavailable');
|
|
45
|
+
const childEnv = socket ? { ...env, RIGHTKIT_BUILD_BROKER_SOCKET: socket } : env;
|
|
46
|
+
const result = spawn(process.execPath, [client, "cargo", ...args], { ...options, env: childEnv });
|
|
47
|
+
if (platform === 'darwin' && result.status !== 0 && /RK_BROKER_UNAVAILABLE|RightKit broker (?:unavailable|endpoint is not configured)/i.test(String(result.stderr ?? ''))) {
|
|
48
|
+
return { ...result, error: unavailable(String(result.stderr).trim()).error };
|
|
49
|
+
}
|
|
50
|
+
return result;
|
|
52
51
|
}
|
|
53
|
-
return
|
|
52
|
+
if (platform === 'darwin' || (managedHost(env) && platform === 'win32')) return unavailable('installed RightKit runtime client is unavailable for managed Cargo metadata');
|
|
53
|
+
return spawn("cargo", args, options);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function unavailable(message) {
|
|
57
|
+
return { status: null, stdout: "", stderr: message, error: Object.assign(new Error(message), { code: 'RK_BROKER_UNAVAILABLE' }) };
|
|
54
58
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { managedHost } from "./managed-cargo.mjs";
|
|
2
|
+
|
|
3
|
+
export const MANAGED_HOST_ENV_SECRET_REFUSED = "MANAGED_HOST_ENV_SECRET_REFUSED";
|
|
4
|
+
|
|
5
|
+
export class ManagedHostEnvSecretError extends Error {
|
|
6
|
+
constructor(name) {
|
|
7
|
+
super(`${name} is refused on a managed RightKit host; load the secret from the provisioned OS credential store (macOS Keychain or protected token file) instead`);
|
|
8
|
+
this.name = "ManagedHostEnvSecretError";
|
|
9
|
+
this.code = MANAGED_HOST_ENV_SECRET_REFUSED;
|
|
10
|
+
this.variable = name;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function isManagedHost(env = process.env) {
|
|
15
|
+
return managedHost(env);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Throws unconditionally; the message names the variable only, never its value.
|
|
19
|
+
export function refuseManagedEnvSecret(name) {
|
|
20
|
+
throw new ManagedHostEnvSecretError(name);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// On a managed host, refuse any named environment secret that is set (non-blank).
|
|
24
|
+
// Unmanaged hosts are unaffected.
|
|
25
|
+
export function refuseManagedEnvSecrets(env = process.env, names = []) {
|
|
26
|
+
if (!isManagedHost(env)) return;
|
|
27
|
+
for (const name of names) {
|
|
28
|
+
if (String(env[name] ?? "").trim()) refuseManagedEnvSecret(name);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { posix, win32 } from 'node:path';
|
|
4
|
+
|
|
5
|
+
/** RIGHTKIT_MANAGED_ROOT remains a data-root override for existing release callers. */
|
|
6
|
+
export function resolveManagedRoots({ env = process.env, platform = process.platform, home = homedir(), controlRoot, dataRoot } = {}) {
|
|
7
|
+
const paths = platform === 'win32' ? win32 : posix;
|
|
8
|
+
const legacyRoot = platform === 'win32' ? 'D:\\.rightkit-managed' : '/Volumes/D/.rightkit-managed';
|
|
9
|
+
dataRoot ??= env.RIGHTKIT_DATA_ROOT || env.RIGHTKIT_MANAGED_ROOT || env.RIGHTKIT_EXTERNAL_ROOT || legacyRoot;
|
|
10
|
+
controlRoot ??= env.RIGHTKIT_CONTROL_ROOT || (platform === 'darwin'
|
|
11
|
+
? paths.join(home, 'Library', 'Application Support', 'RightKit', 'build-control') : dataRoot);
|
|
12
|
+
return Object.freeze({ controlRoot, dataRoot, legacyRoot, runtimeCurrent: paths.join(controlRoot, 'runtime-current'),
|
|
13
|
+
configPath: paths.join(controlRoot, 'config.json'), socket: platform === 'win32' ? null : paths.join(controlRoot, 'broker.sock'),
|
|
14
|
+
logs: paths.join(controlRoot, 'logs'), agentBin: paths.join(controlRoot, 'agent-bin'),
|
|
15
|
+
agentPolicy: paths.join(controlRoot, 'agent-policy'), ownershipInstall: paths.join(controlRoot, 'ownership-install'),
|
|
16
|
+
// The Mac signing agent already lives on the internal disk; moving it would force a re-provision.
|
|
17
|
+
signingAgent: platform === 'darwin' ? paths.join(home, 'Library', 'Application Support', 'Orthic Labs', 'shared', 'signing-agent')
|
|
18
|
+
: paths.join(controlRoot, 'signing-agent') });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function managedControlCandidates(options = {}) {
|
|
22
|
+
const platform = options.platform ?? process.platform;
|
|
23
|
+
if (!['darwin', 'win32'].includes(platform)) {
|
|
24
|
+
const env = options.env ?? process.env;
|
|
25
|
+
return [...new Set([options.controlRoot, env.RIGHTKIT_CONTROL_ROOT, options.dataRoot, env.RIGHTKIT_DATA_ROOT, env.RIGHTKIT_MANAGED_ROOT, env.RIGHTKIT_EXTERNAL_ROOT, options.clientRoot].filter(Boolean))];
|
|
26
|
+
}
|
|
27
|
+
const roots = resolveManagedRoots(options);
|
|
28
|
+
return [...new Set([roots.controlRoot, roots.dataRoot, options.clientRoot, roots.legacyRoot].filter(Boolean))];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function discoverBrokerSocket(env = process.env, { exists = existsSync, ...options } = {}) {
|
|
32
|
+
if (typeof env.RIGHTKIT_BUILD_BROKER_SOCKET === 'string' && env.RIGHTKIT_BUILD_BROKER_SOCKET) return env.RIGHTKIT_BUILD_BROKER_SOCKET;
|
|
33
|
+
if ((options.platform ?? process.platform) === 'win32') return undefined;
|
|
34
|
+
for (const root of managedControlCandidates({ ...options, env })) {
|
|
35
|
+
const socket = posix.join(root, 'broker.sock');
|
|
36
|
+
if (exists(socket)) return socket;
|
|
37
|
+
}
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
package/model-promote.mjs
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { verifyArtifactFile, readVerifiedArtifact, digestBytes } from "./publication-artifacts.mjs";
|
|
3
|
+
import { uploadVerifiedR2File, uploadVerifiedR2Artifact } from "./r2-verified-upload.mjs";
|
|
4
|
+
import { refuseManagedEnvSecrets } from "./managed-host-secrets.mjs";
|
|
5
|
+
import { realpathSync } from "node:fs";
|
|
6
|
+
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
5
7
|
import os from "node:os";
|
|
6
8
|
import path from "node:path";
|
|
7
|
-
import {
|
|
9
|
+
import { spawnSync } from "./background-process.mjs";
|
|
8
10
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
9
11
|
|
|
10
12
|
import {
|
|
@@ -17,8 +19,6 @@ import { registerRightAppsRelease } from "./rightapps-register.mjs";
|
|
|
17
19
|
import { assertCleanSource } from "./source-gate.mjs";
|
|
18
20
|
|
|
19
21
|
export const RUNTIME_ARTIFACT_TRUSTED_KEY_ID = "rightkit-runtime-artifacts-2026-07";
|
|
20
|
-
const TOOL_ROOT = path.dirname(fileURLToPath(import.meta.url));
|
|
21
|
-
const UPLOAD = path.join(TOOL_ROOT, "upload-large.mjs");
|
|
22
22
|
|
|
23
23
|
export class RuntimeArtifactPromotionError extends Error {
|
|
24
24
|
constructor(code, message) {
|
|
@@ -52,8 +52,8 @@ export async function promoteRuntimeArtifact(input, deps = {}) {
|
|
|
52
52
|
if (!input.privateKey) throw new Error("runtime artifact signing key is required");
|
|
53
53
|
|
|
54
54
|
const [artifact, evidence] = await Promise.all([
|
|
55
|
-
metadata(input.artifactFile),
|
|
56
|
-
metadata(input.evidenceFile),
|
|
55
|
+
metadata(input.artifactFile, config.artifact),
|
|
56
|
+
metadata(input.evidenceFile, config.evidence),
|
|
57
57
|
]);
|
|
58
58
|
assertMetadata(artifact, config.artifact, "artifact");
|
|
59
59
|
if (evidence.sha256 !== config.evidence.sha256) throw new Error("evidence SHA-256 does not match promotion config");
|
|
@@ -84,6 +84,7 @@ export async function promoteRuntimeArtifact(input, deps = {}) {
|
|
|
84
84
|
file: input.artifactFile,
|
|
85
85
|
key: manifest.object.r2Key,
|
|
86
86
|
lane: manifest.distribution.delivery,
|
|
87
|
+
evidence: { sha256: artifact.sha256, sizeBytes: artifact.sizeBytes },
|
|
87
88
|
});
|
|
88
89
|
}
|
|
89
90
|
await register(envelope);
|
|
@@ -104,6 +105,7 @@ export function defaultWindowsRuntimeArtifactSigningKeyFile(env = process.env) {
|
|
|
104
105
|
}
|
|
105
106
|
|
|
106
107
|
export async function loadRuntimeArtifactSigningKey({ env = process.env, platform = process.platform, explicitFile } = {}) {
|
|
108
|
+
refuseManagedEnvSecrets(env, ["RIGHTKIT_RUNTIME_ARTIFACT_PRIVATE_KEY", "RIGHTKIT_RUNTIME_ARTIFACT_PRIVATE_KEY_FILE"]);
|
|
107
109
|
const direct = env.RIGHTKIT_RUNTIME_ARTIFACT_PRIVATE_KEY;
|
|
108
110
|
if (direct?.trim()) return direct.replace(/\\n/g, "\n");
|
|
109
111
|
const configuredFile = explicitFile || env.RIGHTKIT_RUNTIME_ARTIFACT_PRIVATE_KEY_FILE;
|
|
@@ -140,7 +142,9 @@ async function main() {
|
|
|
140
142
|
sizeBytes: raw.artifact.sizeBytes,
|
|
141
143
|
},
|
|
142
144
|
evidence: { sha256: raw.evidence.sha256 },
|
|
143
|
-
manifest:
|
|
145
|
+
manifest: args.artifactId === undefined
|
|
146
|
+
? raw.manifest
|
|
147
|
+
: { ...raw.manifest, artifactId: args.artifactId },
|
|
144
148
|
};
|
|
145
149
|
assertPromotionConfig(config);
|
|
146
150
|
assertPromotionAuthority(args.authority, config.manifest.entitlement?.appKey);
|
|
@@ -187,6 +191,7 @@ artifact.sizeBytes, and evidence.sha256 must exactly match those files.
|
|
|
187
191
|
|
|
188
192
|
Options:
|
|
189
193
|
--dry-run Validate real files and sign, without R2 or RightApps mutations
|
|
194
|
+
--artifact-id <slug> Override manifest.artifactId for this promotion
|
|
190
195
|
--signing-key-file <pem> Override the protected PEM path
|
|
191
196
|
--key-id <id> Must equal ${RUNTIME_ARTIFACT_TRUSTED_KEY_ID}
|
|
192
197
|
|
|
@@ -206,11 +211,13 @@ function parseArgs(args) {
|
|
|
206
211
|
let config = "";
|
|
207
212
|
let signingKeyFile;
|
|
208
213
|
let keyId = process.env.RIGHTKIT_RUNTIME_ARTIFACT_KEY_ID || RUNTIME_ARTIFACT_TRUSTED_KEY_ID;
|
|
214
|
+
let artifactId;
|
|
209
215
|
let dryRun = false;
|
|
210
216
|
for (let i = 0; i < args.length; i++) {
|
|
211
217
|
const arg = args[i];
|
|
212
218
|
if (arg === "--authority") authority = args[++i] ?? "";
|
|
213
219
|
else if (arg === "--config") config = args[++i] ?? "";
|
|
220
|
+
else if (arg === "--artifact-id") artifactId = args[++i] ?? "";
|
|
214
221
|
else if (arg === "--signing-key-file") signingKeyFile = args[++i] ?? "";
|
|
215
222
|
else if (arg === "--key-id") keyId = args[++i] ?? "";
|
|
216
223
|
else if (arg === "--dry-run") dryRun = true;
|
|
@@ -218,7 +225,7 @@ function parseArgs(args) {
|
|
|
218
225
|
}
|
|
219
226
|
if (!config) throw new Error("model promote requires --config <path>");
|
|
220
227
|
if (!authority) throw new Error("model promote requires --authority <app-key>");
|
|
221
|
-
return { authority, config, signingKeyFile, keyId, dryRun };
|
|
228
|
+
return { authority, config, signingKeyFile, keyId, artifactId, dryRun };
|
|
222
229
|
}
|
|
223
230
|
|
|
224
231
|
function readVerificationEvidence(args, cwd) {
|
|
@@ -234,28 +241,22 @@ function git(cwd, args) {
|
|
|
234
241
|
return result.stdout.trim();
|
|
235
242
|
}
|
|
236
243
|
|
|
237
|
-
async function fileMetadata(file) {
|
|
238
|
-
const
|
|
239
|
-
if (
|
|
240
|
-
|
|
241
|
-
await new Promise((resolve, reject) => {
|
|
242
|
-
const stream = createReadStream(file);
|
|
243
|
-
stream.on("data", (chunk) => hash.update(chunk));
|
|
244
|
-
stream.on("error", reject);
|
|
245
|
-
stream.on("end", resolve);
|
|
246
|
-
});
|
|
247
|
-
return { sha256: hash.digest("hex"), sizeBytes: info.size };
|
|
244
|
+
async function fileMetadata(file, evidence) {
|
|
245
|
+
const artifact = verifyArtifactFile(file, evidence);
|
|
246
|
+
if (artifact.sizeBytes <= 0) throw new Error(`promotion input is not a nonempty file: ${file}`);
|
|
247
|
+
return artifact;
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
-
async function defaultUpload({ file, content, key, lane }) {
|
|
250
|
+
export async function defaultUpload({ file, content, key, lane, evidence }, deps = {}) {
|
|
251
251
|
const bucketAlias = lane === "private-r2" ? "private" : "public";
|
|
252
252
|
const env = runtimeArtifactUploadEnv();
|
|
253
|
-
if (file) return
|
|
253
|
+
if (file) return uploadVerifiedR2File(file, evidence, key, bucketAlias, { ...deps, env });
|
|
254
254
|
const tempRoot = await mkdtemp(path.join(os.tmpdir(), "rightkit-runtime-pointer-"));
|
|
255
255
|
const pointerFile = path.join(tempRoot, "manifest.json");
|
|
256
256
|
try {
|
|
257
|
-
|
|
258
|
-
await
|
|
257
|
+
const bytes = Buffer.from(content);
|
|
258
|
+
await writeFile(pointerFile, bytes, { mode: 0o600 });
|
|
259
|
+
uploadVerifiedR2Artifact(readVerifiedArtifact(pointerFile, { sha256: digestBytes(bytes) }), key, bucketAlias, { ...deps, env });
|
|
259
260
|
} finally {
|
|
260
261
|
await rm(tempRoot, { recursive: true, force: true });
|
|
261
262
|
}
|
|
@@ -265,14 +266,6 @@ async function registerRuntimeArtifact(envelope, releaseToken) {
|
|
|
265
266
|
return registerRightAppsRelease("/v1/admin/apps/runtime-artifacts", envelope, { token: releaseToken });
|
|
266
267
|
}
|
|
267
268
|
|
|
268
|
-
function run(command, args, env = process.env) {
|
|
269
|
-
return new Promise((resolve, reject) => {
|
|
270
|
-
const child = spawn(command, args, { cwd: process.cwd(), env, stdio: "inherit", windowsHide: true });
|
|
271
|
-
child.on("error", reject);
|
|
272
|
-
child.on("exit", (code) => code === 0 ? resolve() : reject(new Error(`${command} exited ${code}`)));
|
|
273
|
-
});
|
|
274
|
-
}
|
|
275
|
-
|
|
276
269
|
async function readNonemptySecret(file) {
|
|
277
270
|
const value = await readFile(file, "utf8");
|
|
278
271
|
if (!value.trim()) throw new Error(`runtime artifact signing key file is empty: ${file}`);
|
|
@@ -285,7 +278,7 @@ function assertPromotionConfig(value) {
|
|
|
285
278
|
assertExactKeys(value.evidence, ["sha256"], "evidence");
|
|
286
279
|
assertExactKeys(value.manifest, [
|
|
287
280
|
"artifactKind", "entitlement", "distribution", "target", "versions", "provenance", "promotion",
|
|
288
|
-
], "manifest config");
|
|
281
|
+
], "manifest config", ["artifactId"]);
|
|
289
282
|
assertExactKeys(value.manifest.promotion, ["promotionId", "promotedAt"], "manifest config promotion");
|
|
290
283
|
}
|
|
291
284
|
|
|
@@ -294,14 +287,15 @@ function assertMetadata(actual, expected, label) {
|
|
|
294
287
|
if (actual.sizeBytes !== expected.sizeBytes) throw new Error(`${label} size does not match promotion config`);
|
|
295
288
|
}
|
|
296
289
|
|
|
297
|
-
function assertExactKeys(value, expected, label) {
|
|
290
|
+
function assertExactKeys(value, expected, label, optional = []) {
|
|
298
291
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object`);
|
|
299
|
-
const allowed = new Set(expected);
|
|
292
|
+
const allowed = new Set([...expected, ...optional]);
|
|
300
293
|
for (const key of Object.keys(value)) if (!allowed.has(key)) throw new Error(`${label} has unknown field ${key}`);
|
|
301
294
|
for (const key of expected) if (!Object.hasOwn(value, key)) throw new Error(`${label}.${key} is required`);
|
|
302
295
|
}
|
|
303
296
|
|
|
304
|
-
|
|
297
|
+
const realOrSelf = (file) => { try { return realpathSync(file); } catch { return path.resolve(file); } };
|
|
298
|
+
if (process.argv[1] && realOrSelf(process.argv[1]) === realOrSelf(fileURLToPath(import.meta.url))) {
|
|
305
299
|
main().catch((error) => {
|
|
306
300
|
console.error(`right-release model promote: ${error.code ? `[${error.code}] ` : ""}${error.message}`);
|
|
307
301
|
process.exitCode = 1;
|
package/notarize-macos.mjs
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { notarytoolAuthArgs } from "./notary-auth.mjs";
|
|
5
4
|
import { notarizeMacosArtifact } from "./macos-notarization.mjs";
|
|
6
5
|
|
|
7
6
|
const args = process.argv.slice(2);
|
|
@@ -24,10 +23,11 @@ if (dryRun) {
|
|
|
24
23
|
if (process.platform !== "darwin") fail("macOS notarization must run on macOS");
|
|
25
24
|
if (!existsSync(archive)) fail(`missing archive: ${archive}`);
|
|
26
25
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
try { notarizeMacosArtifact({ archive, receiptPath, profile, app }); }
|
|
27
|
+
catch (error) {
|
|
28
|
+
if (error.report) console.error(JSON.stringify(error.report));
|
|
29
|
+
fail(`${error.code ? `[${error.code}] ` : ""}${error.message}`);
|
|
30
|
+
}
|
|
31
31
|
|
|
32
32
|
function fail(message) {
|
|
33
33
|
console.error(`right-notarize-macos: ${message}`);
|
package/notary-auth.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import os from "node:os";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { refuseManagedEnvSecrets } from "./managed-host-secrets.mjs";
|
|
3
4
|
|
|
4
5
|
export function notarytoolAuthArgs({ profile, env = process.env } = {}) {
|
|
5
6
|
// A configured profile explicitly selects Keychain authentication. Inherited
|
|
@@ -10,6 +11,9 @@ export function notarytoolAuthArgs({ profile, env = process.env } = {}) {
|
|
|
10
11
|
}
|
|
11
12
|
return ["--keychain-profile", profile.trim()];
|
|
12
13
|
}
|
|
14
|
+
// Env-selected App Store Connect API auth is refused on managed hosts; the
|
|
15
|
+
// provisioned Keychain profile is the only automatic source there.
|
|
16
|
+
refuseManagedEnvSecrets(env, ["APPLE_API_KEY_PATH", "APPLE_API_KEY", "APPLE_API_ISSUER"]);
|
|
13
17
|
const keyPath = env.APPLE_API_KEY_PATH?.trim();
|
|
14
18
|
const keyId = env.APPLE_API_KEY?.trim();
|
|
15
19
|
const issuer = env.APPLE_API_ISSUER?.trim();
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { realpathSync } from "node:fs";
|
|
2
|
+
import { spawnSync } from "./background-process.mjs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
export function automationDenied({ parentPid = process.ppid, run = spawnSync, caller: declaredCaller } = {}) {
|
|
7
|
+
const result = declaredCaller ? {} : run("/bin/ps", ["-p", String(parentPid), "-o", "command="], {
|
|
8
|
+
encoding: "utf8", timeout: 1000, shell: false,
|
|
9
|
+
});
|
|
10
|
+
const caller = declaredCaller || result.status === 0 && result.stdout?.trim() || `PID ${parentPid}`;
|
|
11
|
+
const error = new Error(`osascript blocked for caller ${caller}; use right-release dmg build for headless layout`);
|
|
12
|
+
error.code = "MAC_AUTOMATION_DENIED";
|
|
13
|
+
error.caller = caller;
|
|
14
|
+
error.parentPid = parentPid;
|
|
15
|
+
return error;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const realOrSelf = (file) => { try { return realpathSync(file); } catch { return path.resolve(file); } };
|
|
19
|
+
if (process.argv[1] && realOrSelf(process.argv[1]) === realOrSelf(fileURLToPath(import.meta.url))) {
|
|
20
|
+
const error = automationDenied({ caller: process.argv[2] === "--caller" ? process.argv[3] : undefined });
|
|
21
|
+
console.error(JSON.stringify({ event: "mac-automation-denied", code: error.code, caller: error.caller,
|
|
22
|
+
parentPid: error.parentPid, message: error.message }));
|
|
23
|
+
process.exitCode = 125;
|
|
24
|
+
}
|
|
@@ -1,16 +1,26 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Draft/import/verify utilities. Signing is intentionally absent; native prompts own signing.
|
|
3
|
-
import { readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { readFileSync, writeFileSync, realpathSync } from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { createDraft, loadTrustedExternal, verifyException } from "./ownership-approval.mjs";
|
|
6
|
+
import { createDraft, loadTrustedExternal, policyEntryDigest, validateRecord, verifyException } from "./ownership-approval.mjs";
|
|
7
|
+
import { SCANNER_VERSION } from "./ownership.mjs";
|
|
8
|
+
import manifest from "./rightkit-ownership.json" with { type: "json" };
|
|
7
9
|
|
|
8
10
|
function arg(args, name) { const i = args.indexOf(name); return i < 0 ? undefined : args[i + 1]; }
|
|
9
11
|
function json(file) { return JSON.parse(readFileSync(path.resolve(file), "utf8")); }
|
|
12
|
+
export function createInstalledDraft(input, { catalog = manifest, scannerVersion = SCANNER_VERSION, now = new Date() } = {}) {
|
|
13
|
+
const draft = createDraft({ ...input, issued: now.toISOString().slice(0, 10), policyDigest: policyEntryDigest(catalog, input.fingerprint), minimumScannerVersion: scannerVersion, minimumCatalogVersion: catalog.version ?? String(catalog.schema) });
|
|
14
|
+
const problems = validateRecord(draft);
|
|
15
|
+
const duration = Date.parse(draft.payload.expires) - Date.parse(draft.payload.issued);
|
|
16
|
+
if (!(duration > 0 && duration <= 90 * 86400000)) problems.push("draft expiry must be after issued and within 90 days");
|
|
17
|
+
if (problems.length) throw new Error(problems.join("; "));
|
|
18
|
+
return draft;
|
|
19
|
+
}
|
|
10
20
|
export function main(args) {
|
|
11
21
|
if (args.includes("--draft")) {
|
|
12
22
|
const input = json(arg(args, "--draft"));
|
|
13
|
-
const draft =
|
|
23
|
+
const draft = createInstalledDraft(input);
|
|
14
24
|
const out = arg(args, "--out");
|
|
15
25
|
const data = `${JSON.stringify(draft, null, 2)}\n`;
|
|
16
26
|
if (out) writeFileSync(path.resolve(out), data, { flag: "wx" }); else process.stdout.write(data);
|
|
@@ -33,7 +43,8 @@ export function main(args) {
|
|
|
33
43
|
}
|
|
34
44
|
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
45
|
}
|
|
36
|
-
|
|
46
|
+
const realOrSelf = (file) => { try { return realpathSync(file); } catch { return path.resolve(file); } };
|
|
47
|
+
if (process.argv[1] && realOrSelf(process.argv[1]) === realOrSelf(fileURLToPath(import.meta.url))) {
|
|
37
48
|
try { process.exitCode = main(process.argv.slice(2)); }
|
|
38
49
|
catch (error) { console.error(`[right-release] ownership approval: ${error.message}`); process.exitCode = 1; }
|
|
39
50
|
}
|