@rightkit/release 0.2.135 → 0.2.137

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/README.md +62 -0
  2. package/addon-command.mjs +53 -34
  3. package/apple-profile-renew.mjs +30 -12
  4. package/artifact-storage.mjs +47 -10
  5. package/background-process.mjs +18 -0
  6. package/build-release.mjs +1 -1
  7. package/cache-command.mjs +4 -3
  8. package/cargo-contract.mjs +1 -1
  9. package/cargo-guard.mjs +4 -3
  10. package/cargo-registry-readback.mjs +51 -0
  11. package/cargo-target.mjs +3 -1
  12. package/cli/right-release.mjs +26 -0
  13. package/codesign-environment.mjs +26 -2
  14. package/codesign-shim.mjs +2 -1
  15. package/committed-source.mjs +3 -3
  16. package/development.mjs +7 -7
  17. package/direct-bootstrap.mjs +19 -9
  18. package/dmg-build.mjs +117 -0
  19. package/dmg-layout.mjs +123 -0
  20. package/github-release.mjs +74 -37
  21. package/heavy-command.mjs +3 -2
  22. package/install-app.mjs +358 -0
  23. package/inventory-check.mjs +980 -0
  24. package/macos-signing-session.mjs +3 -0
  25. package/managed-cargo.mjs +23 -19
  26. package/managed-host-secrets.mjs +30 -0
  27. package/managed-roots.mjs +39 -0
  28. package/model-promote.mjs +31 -37
  29. package/notary-auth.mjs +4 -0
  30. package/osascript-guard.mjs +24 -0
  31. package/ownership-approval-command.mjs +15 -4
  32. package/ownership-approval.mjs +114 -49
  33. package/ownership-availability.mjs +25 -0
  34. package/ownership-catalog-config.json +18 -0
  35. package/ownership-catalog.mjs +94 -0
  36. package/ownership-command.mjs +20 -10
  37. package/ownership-declaration.json +15 -0
  38. package/ownership-default-policy.json +1 -11
  39. package/ownership-evidence.mjs +10 -8
  40. package/ownership-git-inventory.mjs +29 -1
  41. package/ownership-helper-paths.json +2 -2
  42. package/ownership-hooks.mjs +107 -0
  43. package/ownership-internal-owners.json +44 -0
  44. package/ownership-key-authority.mjs +112 -7
  45. package/ownership-key-command.mjs +76 -19
  46. package/ownership-key-trust.mjs +97 -9
  47. package/ownership-lockfiles.mjs +311 -0
  48. package/ownership-report-command.mjs +12 -3
  49. package/ownership-report.mjs +20 -17
  50. package/ownership-repositories.json +19 -0
  51. package/ownership-scan-worker.mjs +96 -11
  52. package/ownership-source.mjs +39 -0
  53. package/ownership-staged.mjs +193 -0
  54. package/ownership-verdict.mjs +74 -0
  55. package/ownership.mjs +215 -107
  56. package/package.json +1 -1
  57. package/protected-credentials.mjs +111 -0
  58. package/prune-r2.mjs +4 -3
  59. package/pub-upload.mjs +3 -85
  60. package/publication-artifacts.mjs +108 -0
  61. package/publication-gate.mjs +28 -7
  62. package/publish-cargo.mjs +47 -16
  63. package/publish-swift.mjs +46 -17
  64. package/publish-update.mjs +61 -39
  65. package/r2-verified-upload.mjs +41 -0
  66. package/registry-parity.mjs +5 -3
  67. package/release-state.mjs +1 -0
  68. package/release-token.mjs +2 -0
  69. package/rightkit-drift.mjs +3 -2
  70. package/rightkit-ownership.json +2028 -139
  71. package/rightkit-signing-agent.mjs +5 -3
  72. package/rightkit-sync.mjs +6 -2
  73. package/rightkit-versions.json +215 -103
  74. package/runtime-artifact-manifest.mjs +20 -6
  75. package/sign-updater.mjs +5 -0
  76. package/signing-agent-client.mjs +2 -1
  77. package/signing-agent-install.mjs +3 -2
  78. package/standalone-clone-verify.mjs +3 -2
  79. package/tauri-bundle-marker.mjs +3 -2
  80. package/upload-large.mjs +36 -82
  81. package/verifier.mjs +5 -1
@@ -6,6 +6,7 @@ import { spawnSync } from "./background-process.mjs";
6
6
  import { requestSigningAgentSync, validateNotaryAgentReport } from "./signing-agent-client.mjs";
7
7
  import { installSigningAgent, requireMacGuiSession } from "./signing-agent-install.mjs";
8
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) {
@@ -173,6 +174,8 @@ export function runMacGuiNotarization({ kind = "notarize", file, profile = "appl
173
174
 
174
175
  export function signMacUpdaterFiles(files, options = {}) {
175
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"]);
176
179
  const commands = files.map((file) => ["exec", "tauri", "signer", "sign", file]);
177
180
  const signingEnv = { ...env, TAURI_SIGNING_PRIVATE_KEY_PASSWORD: env.TAURI_SIGNING_PRIVATE_KEY_PASSWORD ?? "" };
178
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 = [env.RIGHTKIT_MANAGED_ROOT, env.RIGHTKIT_EXTERNAL_ROOT,
8
- platform === "win32" ? "D:\\.rightkit-managed" : platform === "darwin" ? "/Volumes/D/.rightkit-managed" : undefined]
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 = path.join(root, "runtime-current", "build-control-clients", "src", "rightkit.mjs");
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(path.dirname(path.dirname(path.dirname(path.dirname(client)))), "rightkit-build-control", "global", "runs");
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
- // The managed `rightkit` wrapper defaults the broker socket to <managed root>/broker.sock; calling the
44
- // client directly must do the same, or agent shells without the variable fail metadata.
45
- const root = path.dirname(path.dirname(path.dirname(path.dirname(client))));
46
- const socket = path.join(root, "broker.sock");
47
- const childEnv = !env.RIGHTKIT_BUILD_BROKER_SOCKET && existsSync(socket) ? { ...env, RIGHTKIT_BUILD_BROKER_SOCKET: socket } : env;
48
- return spawnSync(process.execPath, [client, "cargo", ...args], { ...options, env: childEnv, cwd: process.cwd() });
49
- }
50
- if (managedHost(env) && process.platform === "win32") {
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 spawnSync("cargo", args, options);
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 { createHash } from "node:crypto";
3
- import { createReadStream } from "node:fs";
4
- import { mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
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 { spawn, spawnSync } from "./background-process.mjs";
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: raw.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 info = await stat(file);
239
- if (!info.isFile() || info.size <= 0) throw new Error(`promotion input is not a nonempty file: ${file}`);
240
- const hash = createHash("sha256");
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 run(process.execPath, [UPLOAD, file, key, bucketAlias], env);
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
- await writeFile(pointerFile, content, { encoding: "utf8", mode: 0o600 });
258
- await run(process.execPath, [UPLOAD, pointerFile, key, bucketAlias], env);
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
- if (process.argv[1] && pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url) {
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/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 = createDraft(input);
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
- if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
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
  }