@rightkit/release 0.2.135 → 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.
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 +17 -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 +213 -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
package/publish-swift.mjs CHANGED
@@ -9,7 +9,9 @@
9
9
  import fs from "node:fs";
10
10
  import os from "node:os";
11
11
  import path from "node:path";
12
+ import { fileURLToPath } from "node:url";
12
13
  import { spawnSync } from "./background-process.mjs";
14
+ import { digestBytes } from "./publication-artifacts.mjs";
13
15
  import { scanCrateForSecrets } from "./publish-cargo.mjs";
14
16
  import { enforcePublicationGate, defaultEvidencePath } from "./publication-gate.mjs";
15
17
 
@@ -70,8 +72,8 @@ export function buildSwiftPublishPlan({ packageDir, name, version, repo, dryRun
70
72
  return plan;
71
73
  }
72
74
 
73
- function git(args, cwd) {
74
- const result = spawnSync("git", args, { cwd, encoding: "utf8", windowsHide: true });
75
+ function git(args, cwd, execute = spawnSync) {
76
+ const result = execute("git", args, { cwd, encoding: "utf8", windowsHide: true });
75
77
  if (result.error) throw result.error;
76
78
  if (result.status !== 0) throw new Error(`git ${args.join(" ")} failed: ${(result.stderr ?? "").trim()}`);
77
79
  return (result.stdout ?? "").trim();
@@ -84,20 +86,21 @@ function copyTree(from, to) {
84
86
  const source = path.join(from, entry.name);
85
87
  const target = path.join(to, entry.name);
86
88
  if (entry.isDirectory()) copyTree(source, target);
87
- else if (entry.isFile()) fs.copyFileSync(source, target);
89
+ else if (entry.isFile()) { fs.copyFileSync(source, target); fs.chmodSync(target, fs.statSync(source).mode & 0o777); }
90
+ else throw new Error(`unsupported Swift source entry: ${entry.name}`);
88
91
  }
89
92
  }
90
93
 
91
- function mirrorAndTag(step, packageDir, work) {
94
+ function mirrorAndTag(step, packageDir, work, execute) {
92
95
  const mirror = path.join(work, "rightkit-swift"); // SwiftPM names the package by repo directory
93
- if (step.repo) git(["clone", step.repo, mirror], work);
94
- else { fs.mkdirSync(mirror, { recursive: true }); git(["init", "-q", "-b", "main"], mirror); }
96
+ if (step.repo) git(["clone", step.repo, mirror], work, execute);
97
+ else { fs.mkdirSync(mirror, { recursive: true }); git(["init", "-q", "-b", "main"], mirror, execute); }
95
98
  for (const entry of fs.readdirSync(mirror)) if (entry !== ".git") fs.rmSync(path.join(mirror, entry), { recursive: true, force: true });
96
99
  copyTree(packageDir, mirror);
97
- git(["add", "-A"], mirror);
98
- git(["-c", "user.email=release@rightsuite.invalid", "-c", "user.name=right-release", "commit", "-q", "--allow-empty", "-m", `RightKitSwift ${step.tag}`], mirror);
99
- if (git(["tag", "-l", step.tag], mirror) !== "") throw new Error(`tag ${step.tag} already exists: bump VERSION`);
100
- git(["tag", step.tag], mirror);
100
+ git(["add", "-A"], mirror, execute);
101
+ git(["-c", "user.email=release@rightsuite.invalid", "-c", "user.name=right-release", "commit", "-q", "--allow-empty", "-m", `RightKitSwift ${step.tag}`], mirror, execute);
102
+ if (git(["tag", "-l", step.tag], mirror, execute) !== "") throw new Error(`tag ${step.tag} already exists: bump VERSION`);
103
+ git(["tag", step.tag], mirror, execute);
101
104
  return mirror;
102
105
  }
103
106
 
@@ -117,33 +120,58 @@ function consumeFresh(step, mirror, work, execute) {
117
120
  if (!pins.includes(`"version" : "${step.tag}"`)) throw new Error(`consumer did not resolve exact tag ${step.tag}`);
118
121
  }
119
122
 
123
+ // Evidence binds this exact git-archive command, rooted at the package tree.
124
+ export function verifySwiftSourceArchive(bytes, evidence) {
125
+ if (!/^[a-f0-9]{64}$/.test(evidence?.artifactSha256 ?? "")) throw new Error("Swift source archive hash evidence is required");
126
+ if (digestBytes(bytes) !== evidence.artifactSha256) throw new Error("Swift source archive sha256 does not match evidence artifactSha256");
127
+ return bytes;
128
+ }
129
+
120
130
  export function runSwiftPublish(options, execute = spawnSync) {
121
131
  const packageDir = options.packageDir;
122
132
  const { name, version } = readPackageIdentity(packageDir);
123
133
  const workspaceRoot = path.resolve(options.workspace ?? path.join(packageDir, "..", ".."));
124
- enforcePublicationGate({
134
+ const gate = enforcePublicationGate({
125
135
  kind: "swift", name, version, packagePath: packageDir,
126
136
  evidencePath: path.resolve(options.evidence ?? defaultEvidencePath(workspaceRoot, "swift", name)),
127
137
  }, { dryRun: options.dryRun });
138
+ const prefix = git(["rev-parse", "--show-prefix"], packageDir, execute).replace(/\/$/, "");
139
+ const sourceCommit = gate.evidence?.commit ?? "HEAD";
140
+ const tree = git(["rev-parse", prefix ? `${sourceCommit}:${prefix}` : `${sourceCommit}^{tree}`], packageDir, execute);
141
+ const archive = execute("git", ["archive", "--format=tar", "--mtime=1970-01-01T00:00:00Z", tree], { cwd: packageDir, windowsHide: true, maxBuffer: 128 * 1024 * 1024 });
142
+ if (archive.error || archive.status !== 0 || !archive.stdout?.length) throw new Error("Swift source archive creation failed");
143
+ const bytes = Buffer.from(archive.stdout);
144
+ if (gate.ok || !options.dryRun) verifySwiftSourceArchive(bytes, gate.evidence);
128
145
  const plan = buildSwiftPublishPlan({ ...options, packageDir, name, version });
129
146
  const work = fs.mkdtempSync(path.join(os.tmpdir(), "rightkit-swift-publish-"));
130
147
  let mirror = null;
148
+ let sealedCommit;
149
+ const snapshot = path.join(work, "source");
150
+ fs.mkdirSync(snapshot);
131
151
  try {
152
+ const extracted = execute("tar", ["-xf", "-", "-C", snapshot], { input: bytes, windowsHide: true });
153
+ if (extracted.error || extracted.status !== 0) throw new Error("Swift source archive extraction failed");
154
+ const identity = readPackageIdentity(snapshot);
155
+ if (identity.name !== name || identity.version !== version) throw new Error("Swift source archive identity mismatch");
132
156
  for (const step of plan) {
133
157
  process.stdout.write(`[right-release] swift ${step.label}\n`);
134
158
  if (step.internal === "secret-scan") {
135
- const violations = scanCrateForSecrets(packageDir);
159
+ const violations = scanCrateForSecrets(snapshot);
136
160
  if (violations.length) throw new Error(`secret scan rejected: ${violations.join(", ")}`);
137
161
  continue;
138
162
  }
139
- if (step.internal === "mirror") { mirror = mirrorAndTag(step, packageDir, work); continue; }
163
+ if (step.internal === "mirror") { mirror = mirrorAndTag(step, snapshot, work, execute);
164
+ if (git(["rev-parse", `${step.tag}^{tree}`], mirror, execute) !== tree) throw new Error("Swift mirror differs from verified source archive");
165
+ sealedCommit = git(["rev-parse", step.tag], mirror, execute);
166
+ continue; }
140
167
  if (step.internal === "consume") { consumeFresh(step, mirror, work, execute); continue; }
141
168
  if (step.internal === "push") {
142
- git(["push", "origin", "HEAD"], mirror);
143
- git(["push", "origin", step.tag], mirror);
169
+ if (git(["rev-parse", `${sealedCommit}^{tree}`], mirror, execute) !== tree || git(["rev-parse", step.tag], mirror, execute) !== sealedCommit) throw new Error("Swift publication tag changed after verification");
170
+ const branch = git(["symbolic-ref", "--short", "HEAD"], mirror, execute);
171
+ git(["push", "--atomic", "origin", `${sealedCommit}:refs/heads/${branch}`, `${sealedCommit}:refs/tags/${step.tag}`], mirror, execute);
144
172
  continue;
145
173
  }
146
- const result = execute(step.command, step.args, { cwd: packageDir, encoding: "utf8", windowsHide: true, stdio: "inherit" });
174
+ const result = execute(step.command, step.args.map((arg) => arg === packageDir ? snapshot : arg), { cwd: snapshot, encoding: "utf8", windowsHide: true, stdio: "inherit" });
147
175
  if (result.error) {
148
176
  if (result.error.code === "ENOENT") {
149
177
  throw new Error(`${step.command} not found -- run publish swift on macOS/Linux with the Swift toolchain installed`);
@@ -158,7 +186,8 @@ export function runSwiftPublish(options, execute = spawnSync) {
158
186
  return { name, version, published: !options.dryRun };
159
187
  }
160
188
 
161
- if (process.argv[1]?.endsWith("publish-swift.mjs")) {
189
+ const realOrSelf = (file) => { try { return fs.realpathSync(file); } catch { return path.resolve(file); } };
190
+ if (process.argv[1] && realOrSelf(process.argv[1]) === realOrSelf(fileURLToPath(import.meta.url))) {
162
191
  try {
163
192
  const options = parseSwiftPublishArgs(process.argv.slice(2));
164
193
  const packageDir = path.resolve(path.dirname(new URL(import.meta.url).pathname.replace(/^\/(\w:)/, "$1")), "../../swift/RightKitSwift");
@@ -1,16 +1,19 @@
1
1
  #!/usr/bin/env node
2
- import { createHash } from "node:crypto";
3
- import { access, readFile, stat } from "node:fs/promises";
2
+ import { readFile } from "node:fs/promises";
3
+ import { spawnSync } from "./background-process.mjs";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath, pathToFileURL } from "node:url";
6
- import { spawn } from "./background-process.mjs";
7
6
  import { pruneReleaseObjects } from "./prune-r2.mjs";
8
7
  import { loadReleaseToken } from "./release-token.mjs";
9
8
  import { registerRightAppsRelease } from "./rightapps-register.mjs";
10
- import { recordPublishedVersion, resolveArtifactInput } from "./artifact-storage.mjs";
9
+ import { recordPublishedVersion } from "./artifact-storage.mjs";
10
+ import { assertPrimaryReleaseCheckout } from "./release-invocation.mjs";
11
+ import { assertCleanSource } from "./source-gate.mjs";
12
+ import { verifyPublicationRelease, PublicationError, assertArtifactUnchanged } from "./publication-artifacts.mjs";
13
+ import { uploadVerifiedR2Artifact } from "./r2-verified-upload.mjs";
14
+ import { verifyPlatformTrust } from "./github-release.mjs";
11
15
 
12
16
  const TOOL_ROOT = path.dirname(fileURLToPath(import.meta.url));
13
- const UPLOAD = path.join(TOOL_ROOT, "upload-large.mjs");
14
17
  const API_BASE = process.env.RIGHTAPPS_API_URL || "https://api.spoares.com";
15
18
  const DOWNLOAD_BASE = process.env.RIGHTAPPS_UPDATE_DOWNLOAD_BASE || "https://rightapps-license-gate.adrdsouza.workers.dev";
16
19
  const PUBLIC_BASE = process.env.RIGHTAPPS_PUBLIC_DOWNLOAD_BASE || "https://pub-6c73208d46c245a9b4881d5e02f6b618.r2.dev";
@@ -20,6 +23,7 @@ const args = process.argv.slice(2);
20
23
  let configName = "right-release.config.mjs";
21
24
  let platform = process.platform === "win32" ? "win" : "mac";
22
25
  let dryRun = false;
26
+ let releaseId = "";
23
27
 
24
28
  for (let i = 0; i < args.length; i++) {
25
29
  const arg = args[i];
@@ -27,12 +31,14 @@ for (let i = 0; i < args.length; i++) {
27
31
  else if (arg === "--platform") platform = args[++i];
28
32
  else if (arg === "--tier") tier = args[++i];
29
33
  else if (arg.startsWith("--tier=")) tier = arg.slice("--tier=".length);
34
+ else if (arg === "--release") releaseId = args[++i];
30
35
  else if (arg === "--dry-run") dryRun = true;
31
36
  else fail(`unknown argument: ${arg}`);
32
37
  }
33
38
 
34
39
  if (tier !== "patch" && tier !== "update") fail("tier is required (--tier patch|update or RIGHT_RELEASE_TIER)");
35
40
 
41
+ if (!["win", "mac"].includes(platform)) throw new PublicationError("PUBLICATION_PLATFORM_INVALID", "--platform must be win or mac");
36
42
  const configPath = path.resolve(configName);
37
43
  const root = path.dirname(configPath);
38
44
  const config = (await import(pathToFileURL(configPath))).default;
@@ -43,13 +49,47 @@ const target = config?.targets?.[platform];
43
49
  const artifacts = target?.updater?.artifacts ?? [];
44
50
  const installers = target?.installer?.artifacts ?? [];
45
51
  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);
52
+ let sealed;
53
+ if (!dryRun || releaseId) {
54
+ if (!/^[A-Za-z0-9][A-Za-z0-9._+-]{0,159}$/.test(releaseId)) throw new PublicationError("PUBLICATION_SEAL_REQUIRED", "--release <sealed-id> is required");
55
+ const repoRoot = git(["rev-parse", "--show-toplevel"]);
56
+ assertPrimaryReleaseCheckout(repoRoot);
57
+ const commit = git(["rev-parse", "HEAD"]);
58
+ assertCleanSource({ status: git(["status", "--porcelain=v1", "-z", "--untracked-files=all"]), commandId: "right-release publish-update" });
59
+ sealed = verifyPublicationRelease({ sealedDir: path.join(repoRoot, ".right-release", "sealed", releaseId, platform === "win" ? "windows" : "mac"), repoRoot, appRoot: root, commit, app: config.app, version: versionForStorage, platform, configPath, releaseId });
60
+ const routes = sealed.manifest.routes?.[tier] ?? [];
61
+ const configured = [...(tier === "patch" ? installers.map((item) => ({ ...item, role: "installer" })) : []), ...artifacts.map((item) => ({ ...item, key: tier === "patch" ? patchInstallerKey(item) : item.key, role: "updater" }))];
62
+ for (const item of configured) {
63
+ if (!routes.some((route) => route.key === item.key && route.role === item.role && route.bucket === (tier === "patch" ? "public" : "private") && route.name === path.basename(item.file) && (!item.signature || route.signature === path.basename(item.signature)))) throw new PublicationError("PUBLICATION_ROUTE_INVALID", `config route is not sealed: ${item.key}`);
64
+ }
65
+ if (!configured.length) throw new PublicationError("PUBLICATION_ROUTE_INVALID", "seal has no configured upload route");
66
+ const installer = sealed.manifest.files.find((item) => item.role.split("+").includes("installer"));
67
+ if (!installer) throw new PublicationError("PUBLICATION_SIGNATURE_MISSING", "sealed installer required for platform trust verification");
68
+ verifyPlatformTrust({ sealed, installer: path.join(sealed.sealedDir, installer.name) });
69
+ for (const artifact of sealed.artifacts.values()) assertArtifactUnchanged(artifact);
70
+ }
71
+ const artifactFile = (declared) => {
72
+ if (!sealed) return path.resolve(root, declared);
73
+ const artifact = sealed.artifacts.get(path.basename(declared));
74
+ if (!artifact) throw new PublicationError("PUBLICATION_ARTIFACT_MISSING", `artifact is not sealed: ${declared}`);
75
+ return artifact.file;
76
+ };
77
+ const verifiedArtifact = (file) => sealed.artifacts.get(path.basename(file));
78
+ const signatureText = (file) => {
79
+ const signature = assertArtifactUnchanged(verifiedArtifact(file)).toString("utf8").trim();
80
+ if (!signature) throw new PublicationError("PUBLICATION_SIGNATURE_MISSING", `sealed updater signature is empty: ${file}`);
81
+ return signature;
82
+ };
83
+ function git(args) {
84
+ const result = spawnSync("git", args, { cwd: root, encoding: "utf8", windowsHide: true });
85
+ if (result.error || result.status !== 0) throw new PublicationError("PUBLICATION_SOURCE_INVALID", "cannot read publication source identity");
86
+ return result.stdout.trim();
87
+ }
49
88
 
50
89
  if (tier === "patch" && !installers.length) fail(`${config?.app ?? "app"} ${platform} patch has no installer.artifacts`);
51
90
  if (tier === "update" && !artifacts.length) fail(`${config?.app ?? "app"} ${platform} update has no updater.artifacts`);
52
91
 
92
+ if (sealed) for (const artifact of artifacts) signatureText(artifactFile(artifact.signature));
53
93
  let releaseToken = null;
54
94
  if (!dryRun && (tier === "update" || artifacts.length > 0)) {
55
95
  try {
@@ -62,8 +102,7 @@ if (!dryRun && (tier === "update" || artifacts.length > 0)) {
62
102
  if (tier === "patch") {
63
103
  for (const installer of installers) {
64
104
  assertCurrentKey(installer.key, "installer");
65
- const file = artifactFile(installer.file);
66
- if (!dryRun) await access(file).catch(() => fail(`missing signed installer: ${installer.file}`));
105
+ artifactFile(installer.file);
67
106
  }
68
107
  const platforms = {};
69
108
  const registrations = new Map();
@@ -76,16 +115,12 @@ if (tier === "patch") {
76
115
  assertCurrentKey(installerKey, "installer");
77
116
  const file = artifactFile(artifact.file);
78
117
  const signatureFile = artifactFile(artifact.signature);
79
- if (!dryRun) {
80
- await access(file).catch(() => fail(`missing patch updater payload: ${artifact.file}`));
81
- await access(signatureFile).catch(() => fail(`missing patch updater signature: ${artifact.signature}`));
82
- }
83
118
  if (!uploadedKeys.has(installerKey)) {
84
119
  console.log(`${dryRun ? "[dry-run] " : ""}UPLOAD public/${installerKey}`);
85
- if (!dryRun) await run(process.execPath, [UPLOAD, file, installerKey, "public"], root);
120
+ if (!dryRun) uploadVerifiedR2Artifact(verifiedArtifact(file), installerKey, "public", { cwd: root });
86
121
  uploadedKeys.add(installerKey);
87
122
  }
88
- const signature = dryRun ? `<signature:${artifact.signature}>` : (await readFile(signatureFile, "utf8")).trim();
123
+ const signature = dryRun ? `<signature:${artifact.signature}>` : signatureText(signatureFile);
89
124
  const artifactKey = artifact.artifactKey || installerKey.replaceAll("/", "__");
90
125
  const metadata = dryRun ? { sha256: null, sizeBytes: null } : await fileMetadata(file);
91
126
  platforms[artifact.platform] = {
@@ -104,7 +139,7 @@ if (tier === "patch") {
104
139
  const file = artifactFile(installer.file);
105
140
  if (!uploadedKeys.has(installer.key)) {
106
141
  console.log(`${dryRun ? "[dry-run] " : ""}UPLOAD public/${installer.key}`);
107
- if (!dryRun) await run(process.execPath, [UPLOAD, file, installer.key, "public"], root);
142
+ if (!dryRun) uploadVerifiedR2Artifact(verifiedArtifact(file), installer.key, "public", { cwd: root });
108
143
  uploadedKeys.add(installer.key);
109
144
  }
110
145
  const metadata = dryRun ? { sha256: null, sizeBytes: null } : await fileMetadata(file);
@@ -126,7 +161,7 @@ if (tier === "patch") {
126
161
  private: target.updater.artifacts.map((artifact) => artifact.key),
127
162
  },
128
163
  });
129
- if (!dryRun) recordPublishedVersion({ app: config.app, version: versionForStorage, platform });
164
+ if (!dryRun) recordPublishedVersion({ app: config.app, version: versionForStorage, platform, configPath, releaseId });
130
165
  process.exit(0);
131
166
  }
132
167
  const version = config.version || target.updater.version;
@@ -164,7 +199,7 @@ if (tier === "patch") {
164
199
  private: [],
165
200
  },
166
201
  });
167
- if (!dryRun) recordPublishedVersion({ app: config.app, version: versionForStorage, platform });
202
+ if (!dryRun) recordPublishedVersion({ app: config.app, version: versionForStorage, platform, configPath, releaseId });
168
203
  process.exit(0);
169
204
  }
170
205
 
@@ -176,16 +211,12 @@ for (const artifact of artifacts) {
176
211
  assertCurrentKey(artifact.key, "updater");
177
212
  const file = artifactFile(artifact.file);
178
213
  const signatureFile = artifactFile(artifact.signature);
179
- if (!dryRun) {
180
- await access(file).catch(() => fail(`missing updater artifact: ${artifact.file}`));
181
- await access(signatureFile).catch(() => fail(`missing updater signature: ${artifact.signature}`));
182
- }
183
214
  if (!uploadedKeys.has(artifact.key)) {
184
215
  console.log(`${dryRun ? "[dry-run] " : ""}UPLOAD private/${artifact.key}`);
185
- if (!dryRun) await run(process.execPath, [UPLOAD, file, artifact.key, "private"], root);
216
+ if (!dryRun) uploadVerifiedR2Artifact(verifiedArtifact(file), artifact.key, "private", { cwd: root });
186
217
  uploadedKeys.add(artifact.key);
187
218
  }
188
- const signature = dryRun ? `<signature:${artifact.signature}>` : (await readFile(signatureFile, "utf8")).trim();
219
+ const signature = dryRun ? `<signature:${artifact.signature}>` : signatureText(signatureFile);
189
220
  const artifactKey = artifact.artifactKey || artifact.key.replaceAll("/", "__");
190
221
  const metadata = dryRun ? { sha256: null, sizeBytes: null } : await fileMetadata(file);
191
222
  platforms[artifact.platform] = {
@@ -238,14 +269,12 @@ await pruneReleaseObjects({
238
269
  private: [...registrations.keys()],
239
270
  },
240
271
  });
241
- if (!dryRun) recordPublishedVersion({ app: config.app, version: versionForStorage, platform });
272
+ if (!dryRun) recordPublishedVersion({ app: config.app, version: versionForStorage, platform, configPath, releaseId });
242
273
 
243
- async function fileMetadata(file) {
244
- const bytes = await readFile(file);
245
- return {
246
- sha256: createHash("sha256").update(bytes).digest("hex"),
247
- sizeBytes: (await stat(file)).size,
248
- };
274
+ function fileMetadata(file) {
275
+ const artifact = verifiedArtifact(file);
276
+ assertArtifactUnchanged(artifact);
277
+ return { sha256: artifact.sha256, sizeBytes: artifact.sizeBytes };
249
278
  }
250
279
 
251
280
  function buildArtifactManifest({ appKey, appVersion, tier, platform, artifacts }) {
@@ -260,13 +289,6 @@ function buildArtifactManifest({ appKey, appVersion, tier, platform, artifacts }
260
289
  };
261
290
  }
262
291
 
263
- function run(cmd, runArgs, cwd) {
264
- return new Promise((resolve, reject) => {
265
- const child = spawn(cmd, runArgs, { cwd, env: process.env, stdio: "inherit", windowsHide: true });
266
- child.on("error", reject);
267
- child.on("exit", (code) => code === 0 ? resolve() : reject(new Error(`${cmd} exited ${code}`)));
268
- });
269
- }
270
292
 
271
293
  function assertCurrentKey(key, kind) {
272
294
  const pattern = kind === "installer"
@@ -0,0 +1,41 @@
1
+ import { mkdtempSync, rmSync } from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { spawnSync } from "./background-process.mjs";
5
+ import { assertArtifactUnchanged, PublicationError, verifyArtifactFile } from "./publication-artifacts.mjs";
6
+
7
+ // Wrangler's pipe input transports the verified buffer, with no path lookup by its child.
8
+ export function uploadVerifiedR2Artifact(artifact, key, bucket, { run = spawnSync, env = process.env, cwd } = {}) {
9
+ if (!["public", "private"].includes(bucket) || !/^[A-Za-z0-9][A-Za-z0-9._/-]*$/.test(key ?? "") || key.includes("..")) throw new PublicationError("PUBLICATION_ROUTE_INVALID", "invalid R2 publication route");
10
+ const input = assertArtifactUnchanged(artifact);
11
+ const cleanEnv = { ...env, CLOUDFLARE_ACCOUNT_ID: env.CLOUDFLARE_ACCOUNT_ID || "03ae77ccd7a07bcbb2dcfde47fa7ba3a" };
12
+ for (const name of ["CLOUDFLARE_API_KEY", "CLOUDFLARE_EMAIL", "CF_API_KEY", "CF_EMAIL"]) delete cleanEnv[name];
13
+ const result = run("pnpm", ["dlx", "wrangler@4", "r2", "object", "put", `${bucket === "public" ? "rightapps-downloads" : "rightapps-updates"}/${key}`, "--pipe", "--remote"], {
14
+ cwd, env: cleanEnv, input, stdio: ["pipe", "inherit", "inherit"], windowsHide: true, shell: process.platform === "win32",
15
+ });
16
+ if (result.error || result.status !== 0) throw new PublicationError("PUBLICATION_UPLOAD_FAILED", `R2 upload failed: ${key}`);
17
+ }
18
+
19
+ // Path-only large-file transport: success requires remote bytes matching captured evidence.
20
+ export function uploadVerifiedR2File(file, evidence, key, bucket, { run = spawnSync, env = process.env, cwd, dryRun = false } = {}) {
21
+ if (!["public", "private"].includes(bucket) || !/^[A-Za-z0-9][A-Za-z0-9._/-]*$/.test(key ?? "") || key.includes("..")) throw new PublicationError("PUBLICATION_ROUTE_INVALID", "invalid R2 publication route");
22
+ const artifact = verifyArtifactFile(file, evidence);
23
+ if (dryRun) return artifact;
24
+ const target = `${bucket === "public" ? "rightapps-downloads" : "rightapps-updates"}/${key}`;
25
+ const cleanEnv = { ...env, CLOUDFLARE_ACCOUNT_ID: env.CLOUDFLARE_ACCOUNT_ID || "03ae77ccd7a07bcbb2dcfde47fa7ba3a" };
26
+ for (const name of ["CLOUDFLARE_API_KEY", "CLOUDFLARE_EMAIL", "CF_API_KEY", "CF_EMAIL"]) delete cleanEnv[name];
27
+ const options = { cwd, env: cleanEnv, windowsHide: true, shell: process.platform === "win32", encoding: "utf8" };
28
+ const prefix = ["dlx", "wrangler@4", "r2", "object"];
29
+ const result = run("pnpm", [...prefix, "put", target, "--file", path.resolve(file), "--remote"], { ...options, stdio: "inherit" });
30
+ if (result.error || result.status !== 0) throw new PublicationError("PUBLICATION_UPLOAD_FAILED", `R2 upload failed; inspect remote key ${target}`);
31
+ const temporary = mkdtempSync(path.join(os.tmpdir(), "rightkit-r2-readback-"));
32
+ try {
33
+ const downloaded = path.join(temporary, "object");
34
+ const readback = run("pnpm", [...prefix, "get", target, "--file", downloaded, "--remote"], options);
35
+ if (readback.error || readback.status !== 0) throw new Error("remote readback failed");
36
+ verifyArtifactFile(downloaded, artifact);
37
+ } catch (error) {
38
+ throw new PublicationError("PUBLICATION_REMOTE_MISMATCH", `R2 verification failed (${error.message}); remove remote key ${target}`);
39
+ } finally { rmSync(temporary, { recursive: true, force: true }); }
40
+ return artifact;
41
+ }
@@ -1,10 +1,11 @@
1
1
  #!/usr/bin/env node
2
+ import { realpathSync } from 'node:fs';
2
3
  import { createHash } from 'node:crypto';
3
4
  import { execFileSync } from './background-process.mjs';
4
5
  import { mkdtemp, mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises';
5
6
  import { tmpdir } from 'node:os';
6
- import { basename, join, relative } from 'node:path';
7
- import { fileURLToPath, pathToFileURL } from 'node:url';
7
+ import { basename, join, relative, resolve } from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
8
9
 
9
10
  const packageRoot = fileURLToPath(new URL('.', import.meta.url));
10
11
 
@@ -108,7 +109,8 @@ export async function verifyRegistryParity({ allowUnpublished = false, fetchImpl
108
109
  }
109
110
  }
110
111
 
111
- if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
112
+ const realOrSelf = (file) => { try { return realpathSync(file); } catch { return resolve(file); } };
113
+ if (process.argv[1] && realOrSelf(process.argv[1]) === realOrSelf(fileURLToPath(import.meta.url))) {
112
114
  const flag = process.argv.indexOf('--package');
113
115
  const allowUnpublished = process.argv.includes('--allow-unpublished');
114
116
  (flag > 0 ? verifyRegistryParity({ allowUnpublished, packageName: process.argv[flag + 1] }) : verifyAllRegistryParity({ allowUnpublished }))
package/release-state.mjs CHANGED
@@ -123,6 +123,7 @@ export function verifySealedRelease(sealedDir) {
123
123
  if (manifest.kind != null && typeof manifest.kind !== "string") throw new Error("sealed manifest kind must be a string when present");
124
124
  if (!manifest.checkpoints?.includes("sealed")) throw new Error("release is not sealed");
125
125
  for (const item of manifest.files ?? []) {
126
+ if (typeof item.name !== "string" || path.basename(item.name) !== item.name || item.name === "." || item.name === "..") throw new Error("unsafe sealed artifact name");
126
127
  const file = path.join(sealedDir, item.name);
127
128
  if (!existsSync(file)) throw new Error(`sealed file missing: ${item.name}`);
128
129
  const bytes = readFileSync(file);
package/release-token.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  import { execFile } from "./background-process.mjs";
2
+ import { refuseManagedEnvSecrets } from "./managed-host-secrets.mjs";
2
3
  import { readFile } from "node:fs/promises";
3
4
  import os from "node:os";
4
5
  import path from "node:path";
@@ -19,6 +20,7 @@ async function readTokenFile(file) {
19
20
  }
20
21
 
21
22
  export async function loadReleaseToken({ env = process.env, platform = process.platform } = {}) {
23
+ refuseManagedEnvSecrets(env, ["RIGHTAPPS_RELEASE_TOKEN", "RIGHTAPPS_RELEASE_TOKEN_FILE"]);
22
24
  const direct = env.RIGHTAPPS_RELEASE_TOKEN?.trim();
23
25
  if (direct) return direct;
24
26
 
@@ -4,7 +4,7 @@
4
4
  // Self-contained (node: built-ins only) so the SessionStart hook can carry a byte copy of it.
5
5
  // node rightkit-drift.mjs [--apply [--all]] [app-root] (--all also adopts breaking bumps)
6
6
  import { spawnSync } from "./background-process.mjs";
7
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
7
+ import { existsSync, readFileSync, writeFileSync, realpathSync } from "node:fs";
8
8
  import path from "node:path";
9
9
  import { fileURLToPath } from "node:url";
10
10
 
@@ -102,7 +102,8 @@ export function formatDrift(drift) {
102
102
  return [`RightKit updates available (${drift.length}):`, ...drift.map((d) => `- ${d.name} ${d.current} -> ${d.latest}${d.breaking ? " (breaking)" : ""} (${d.file})`)].join("\n");
103
103
  }
104
104
 
105
- if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
105
+ const realOrSelf = (file) => { try { return realpathSync(file); } catch { return path.resolve(file); } };
106
+ if (process.argv[1] && realOrSelf(process.argv[1]) === realOrSelf(fileURLToPath(import.meta.url))) {
106
107
  const args = process.argv.slice(2); const apply = args.includes("--apply");
107
108
  const root = path.resolve(args.find((arg) => !arg.startsWith("--")) ?? process.cwd());
108
109
  try {