@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.
- package/README.md +62 -0
- package/addon-command.mjs +53 -34
- package/apple-profile-renew.mjs +30 -12
- package/artifact-storage.mjs +47 -10
- package/background-process.mjs +18 -0
- package/build-release.mjs +1 -1
- 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-environment.mjs +26 -2
- package/codesign-shim.mjs +2 -1
- package/committed-source.mjs +3 -3
- package/development.mjs +7 -7
- 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-signing-session.mjs +3 -0
- 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/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 +20 -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 +2 -2
- 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/rightkit-drift.mjs +3 -2
- package/rightkit-ownership.json +2028 -139
- package/rightkit-signing-agent.mjs +5 -3
- package/rightkit-sync.mjs +6 -2
- package/rightkit-versions.json +215 -103
- package/runtime-artifact-manifest.mjs +20 -6
- package/sign-updater.mjs +5 -0
- package/signing-agent-client.mjs +2 -1
- package/signing-agent-install.mjs +3 -2
- 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/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 =
|
|
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(
|
|
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,
|
|
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(["
|
|
143
|
-
git(["
|
|
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:
|
|
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
|
-
|
|
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");
|
package/publish-update.mjs
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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)
|
|
120
|
+
if (!dryRun) uploadVerifiedR2Artifact(verifiedArtifact(file), installerKey, "public", { cwd: root });
|
|
86
121
|
uploadedKeys.add(installerKey);
|
|
87
122
|
}
|
|
88
|
-
const signature = dryRun ? `<signature:${artifact.signature}>` : (
|
|
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)
|
|
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)
|
|
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}>` : (
|
|
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
|
-
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
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
|
+
}
|
package/registry-parity.mjs
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
package/rightkit-drift.mjs
CHANGED
|
@@ -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
|
-
|
|
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 {
|