@rightkit/release 0.2.26 → 0.2.28
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/cargo-contract.mjs +6 -4
- package/legal-contract.mjs +11 -7
- package/legal-contract.test.mjs +5 -3
- package/package.json +1 -1
- package/pub-upload.mjs +100 -0
- package/release.mjs +20 -8
- package/right-suite-contract.test.mjs +530 -514
- package/rightkit-versions.json +36 -27
- package/standalone-clone-verify.mjs +1 -7
- package/upload-large.test.mjs +1 -0
package/cargo-contract.mjs
CHANGED
|
@@ -34,10 +34,12 @@ export function assertPublishedRightKitCargoDependencies(dependencies, published
|
|
|
34
34
|
let checked = 0;
|
|
35
35
|
for (const dependency of dependencies.filter(({ name }) => name.startsWith("rightkit-"))) {
|
|
36
36
|
checked += 1;
|
|
37
|
-
const
|
|
38
|
-
if (!
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
const configured = published.get(dependency.name);
|
|
38
|
+
if (!configured) throw new Error(`${label} ${dependency.name} is not a published RightKit crate`);
|
|
39
|
+
const allowed = configured instanceof Set ? configured : new Set(Array.isArray(configured) ? configured : [configured]);
|
|
40
|
+
const exactVersions = new Set([...allowed].map((version) => `=${version}`));
|
|
41
|
+
if (!CRATES_IO_SOURCES.has(dependency.source) || dependency.path || dependency.registry || !exactVersions.has(dependency.req)) {
|
|
42
|
+
throw new Error(`${label} ${dependency.name} must use exact crates.io versions (${[...exactVersions].join(" or ")})`);
|
|
41
43
|
}
|
|
42
44
|
}
|
|
43
45
|
return checked;
|
package/legal-contract.mjs
CHANGED
|
@@ -140,14 +140,18 @@ export function assertLegalReleaseContract(root, legal, appName, platform) {
|
|
|
140
140
|
const tauriPath = path.resolve(appRoot, legal.tauriConfig);
|
|
141
141
|
if (!within(appRoot, tauriPath) || !existsSync(tauriPath)) fail(appName, `missing Tauri config: ${legal.tauriConfig}`);
|
|
142
142
|
const tauri = readJson(tauriPath, appName, "Tauri config");
|
|
143
|
+
// The in-app gate is the SINGLE assent surface (Adrian, 2026-07-17). An
|
|
144
|
+
// installer/DMG can only PRESENT the text: it cannot record who agreed, cannot
|
|
145
|
+
// ask the individual-vs-enterprise basis, and passive updaters reach neither —
|
|
146
|
+
// so a license page is duplicate presentation that captures nothing. This gate
|
|
147
|
+
// previously REQUIRED bundle.licenseFile; it now requires its absence.
|
|
143
148
|
const configured = tauri.bundle?.licenseFile;
|
|
144
|
-
if (typeof configured
|
|
145
|
-
fail(
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
fail(appName, "Windows Tauri bundle.licenseFile must point to the exact EULA snapshot");
|
|
149
|
+
if (typeof configured === "string" && configured.trim()) {
|
|
150
|
+
fail(
|
|
151
|
+
appName,
|
|
152
|
+
"Windows Tauri bundle.licenseFile must be absent: the in-app legal gate is the only assent surface, " +
|
|
153
|
+
"and an installer license page duplicates it without recording acceptance",
|
|
154
|
+
);
|
|
151
155
|
}
|
|
152
156
|
}
|
|
153
157
|
|
package/legal-contract.test.mjs
CHANGED
|
@@ -46,9 +46,11 @@ function fixture() {
|
|
|
46
46
|
})),
|
|
47
47
|
};
|
|
48
48
|
writeFileSync(path.join(legalDir, "legal-manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
49
|
+
// No bundle.licenseFile: the in-app gate is the single assent surface, so a
|
|
50
|
+
// compliant app ships an installer with no license page (Adrian, 2026-07-17).
|
|
49
51
|
writeFileSync(
|
|
50
52
|
path.join(tauriDir, "tauri.conf.json"),
|
|
51
|
-
`${JSON.stringify({ bundle: {
|
|
53
|
+
`${JSON.stringify({ bundle: { publisher: "Damned Ventures LLC" } }, null, 2)}\n`,
|
|
52
54
|
);
|
|
53
55
|
return {
|
|
54
56
|
root,
|
|
@@ -100,8 +102,8 @@ test("rejects an explicitly unresolved release blocker even without a checkbox",
|
|
|
100
102
|
|
|
101
103
|
test("requires Windows installer clickthrough to use the exact EULA snapshot", () => {
|
|
102
104
|
const { root, legal } = fixture();
|
|
103
|
-
writeFileSync(path.join(root, "src-tauri", "tauri.conf.json"), JSON.stringify({ bundle: { licenseFile: "../legal/
|
|
104
|
-
assert.throws(() => assertLegalReleaseContract(root, legal, "fixture", "win"), /licenseFile
|
|
105
|
+
writeFileSync(path.join(root, "src-tauri", "tauri.conf.json"), JSON.stringify({ bundle: { licenseFile: "../legal/EULA.md" } }));
|
|
106
|
+
assert.throws(() => assertLegalReleaseContract(root, legal, "fixture", "win"), /licenseFile must be absent/i);
|
|
105
107
|
});
|
|
106
108
|
|
|
107
109
|
test("requires third-party build provenance", () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rightkit/release",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.28",
|
|
4
4
|
"description": "Portable Right Suite release CLI/SDK: signed installers, updater artifacts, patch/update routing, hardening, R2 publish, and RightApps registration.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/pub-upload.mjs
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { accessSync, statSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
|
|
6
|
+
const ACCOUNT_ID = "03ae77ccd7a07bcbb2dcfde47fa7ba3a";
|
|
7
|
+
const BUCKETS = new Map([
|
|
8
|
+
["public", "rightapps-downloads"],
|
|
9
|
+
["private", "rightapps-updates"],
|
|
10
|
+
]);
|
|
11
|
+
// Wrangler always runs remotely through a package runner; which one exists is machine-specific
|
|
12
|
+
// (a node install without npm/npx is normal when pnpm is the pinned package manager).
|
|
13
|
+
const RUNNERS = [
|
|
14
|
+
{ cmd: "npx", prefix: [] },
|
|
15
|
+
{ cmd: "pnpm", prefix: ["dlx"] },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const [, , localFile, r2Key, bucketAlias = "public"] = process.argv;
|
|
19
|
+
if (!localFile || !r2Key) {
|
|
20
|
+
console.error("Usage: node upload-large.mjs <localFile> <r2Key> [public|private]");
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const bucket = BUCKETS.get(bucketAlias);
|
|
25
|
+
if (!bucket) {
|
|
26
|
+
console.error(`upload-large: invalid bucket alias ${bucketAlias}; expected public|private`);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
accessSync(localFile);
|
|
32
|
+
} catch {
|
|
33
|
+
console.error(`upload-large: missing local file: ${localFile}`);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const fileSize = statSync(localFile).size;
|
|
38
|
+
const object = `${bucket}/${r2Key}`;
|
|
39
|
+
const wranglerArgs = ["wrangler@4", "r2", "object", "put", object, "--file", path.resolve(localFile), "--remote"];
|
|
40
|
+
const env = cleanCloudflareEnv(process.env);
|
|
41
|
+
|
|
42
|
+
console.log(`Uploading ${path.basename(localFile)} (${(fileSize / 1024 / 1024).toFixed(1)} MB) -> ${object}`);
|
|
43
|
+
console.log(`wrangler r2 object put ${object} --file ${path.resolve(localFile)} --remote`);
|
|
44
|
+
|
|
45
|
+
// Resolve the package runner before the dry-run exit: a release box without one cannot upload,
|
|
46
|
+
// and that must fail here rather than after a signed build has already been produced.
|
|
47
|
+
const runner = resolveRunner(env);
|
|
48
|
+
if (!runner) {
|
|
49
|
+
console.error(
|
|
50
|
+
"upload-large: no package runner found. Tried: " +
|
|
51
|
+
`${RUNNERS.map((entry) => [entry.cmd, ...entry.prefix].join(" ")).join(", ")}. ` +
|
|
52
|
+
"Install one, or set RIGHT_RELEASE_WRANGLER_RUNNER (e.g. \"pnpm dlx\").",
|
|
53
|
+
);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
console.log(`runner: ${[runner.cmd, ...runner.prefix].join(" ")}`);
|
|
57
|
+
|
|
58
|
+
if (process.env.RIGHT_RELEASE_UPLOAD_DRY_RUN === "1") {
|
|
59
|
+
process.exit(0);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const result = spawnSync(runner.cmd, [...runner.prefix, ...wranglerArgs], {
|
|
63
|
+
env,
|
|
64
|
+
stdio: "inherit",
|
|
65
|
+
shell: process.platform === "win32",
|
|
66
|
+
windowsHide: true,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
if (result.error) {
|
|
70
|
+
console.error(`upload-large: failed to start wrangler: ${result.error.message}`);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
process.exit(result.status ?? 1);
|
|
74
|
+
|
|
75
|
+
function resolveRunner(env) {
|
|
76
|
+
const override = process.env.RIGHT_RELEASE_WRANGLER_RUNNER?.trim();
|
|
77
|
+
if (override) {
|
|
78
|
+
const [cmd, ...prefix] = override.split(/\s+/);
|
|
79
|
+
return { cmd, prefix };
|
|
80
|
+
}
|
|
81
|
+
for (const candidate of RUNNERS) {
|
|
82
|
+
const probe = spawnSync(candidate.cmd, ["--version"], {
|
|
83
|
+
env,
|
|
84
|
+
stdio: "ignore",
|
|
85
|
+
shell: process.platform === "win32",
|
|
86
|
+
windowsHide: true,
|
|
87
|
+
});
|
|
88
|
+
if (!probe.error && probe.status === 0) return candidate;
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function cleanCloudflareEnv(source) {
|
|
94
|
+
const env = { ...source, CLOUDFLARE_ACCOUNT_ID: source.CLOUDFLARE_ACCOUNT_ID || ACCOUNT_ID };
|
|
95
|
+
delete env.CLOUDFLARE_API_KEY;
|
|
96
|
+
delete env.CLOUDFLARE_EMAIL;
|
|
97
|
+
delete env.CF_API_KEY;
|
|
98
|
+
delete env.CF_EMAIL;
|
|
99
|
+
return env;
|
|
100
|
+
}
|
package/release.mjs
CHANGED
|
@@ -14,9 +14,10 @@ const UPLOAD_LARGE = path.resolve(TOOL_ROOT, "upload-large.mjs");
|
|
|
14
14
|
const SIGN_WINDOWS = path.resolve(TOOL_ROOT, "sign-windows.mjs");
|
|
15
15
|
const SIGN_UPDATER = path.resolve(TOOL_ROOT, "sign-updater.mjs");
|
|
16
16
|
const RIGHTKIT_VERSIONS = JSON.parse(readFileSync(path.resolve(TOOL_ROOT, "rightkit-versions.json"), "utf8"));
|
|
17
|
-
const VERSION = RIGHTKIT_VERSIONS.npm["@rightkit/release"];
|
|
17
|
+
const VERSION = RIGHTKIT_VERSIONS.stagedNpm?.["@rightkit/release"] ?? RIGHTKIT_VERSIONS.npm["@rightkit/release"];
|
|
18
18
|
const TIERS = new Set(["patch", "update"]);
|
|
19
|
-
const
|
|
19
|
+
const RIGHTKIT_ALLOWED = buildAllowedVersions(RIGHTKIT_VERSIONS.npm, RIGHTKIT_VERSIONS.stagedNpm);
|
|
20
|
+
const RIGHTKIT_CARGO_ALLOWED = buildAllowedVersions(RIGHTKIT_VERSIONS.cargo, RIGHTKIT_VERSIONS.stagedCargo);
|
|
20
21
|
const FORBIDDEN_RIGHTKIT_SPEC = /^(?:git|file|link|workspace):|github|github\.com/i;
|
|
21
22
|
|
|
22
23
|
const args = process.argv.slice(2);
|
|
@@ -65,7 +66,7 @@ if (config.schema !== 1) fail(`unsupported config schema: ${config.schema ?? "<m
|
|
|
65
66
|
const root = path.dirname(configPath);
|
|
66
67
|
const workdir = path.resolve(root, config.workdir ?? ".");
|
|
67
68
|
await validateRightKitPackageContract(root, config.app);
|
|
68
|
-
validateRightKitCargoContract(root,
|
|
69
|
+
validateRightKitCargoContract(root, RIGHTKIT_CARGO_ALLOWED, config.app ?? path.basename(root));
|
|
69
70
|
const target = config.targets?.[opts.platform];
|
|
70
71
|
if (!target) fail(`${config.app ?? "app"} has no ${opts.platform} release target`);
|
|
71
72
|
const legalContract = config.legal
|
|
@@ -237,16 +238,27 @@ async function validateRightKitPackageContract(root, appName) {
|
|
|
237
238
|
if (FORBIDDEN_RIGHTKIT_SPEC.test(String(specifier))) {
|
|
238
239
|
fail(`${appName ?? pkg.name ?? "app"} ${name} must come from the published npm package, not ${specifier}`);
|
|
239
240
|
}
|
|
240
|
-
const
|
|
241
|
-
if (
|
|
242
|
-
fail(`${appName ?? pkg.name ?? "app"} ${name} must be ${
|
|
241
|
+
const allowed = RIGHTKIT_ALLOWED.get(name);
|
|
242
|
+
if (allowed && !allowed.has(specifier)) {
|
|
243
|
+
fail(`${appName ?? pkg.name ?? "app"} ${name} must be one of ${[...allowed].join(", ")}, got ${specifier}`);
|
|
243
244
|
}
|
|
244
245
|
}
|
|
245
|
-
|
|
246
|
-
|
|
246
|
+
const releaseAllowed = RIGHTKIT_ALLOWED.get("@rightkit/release");
|
|
247
|
+
if (!releaseAllowed?.has(pkg.devDependencies?.["@rightkit/release"])) {
|
|
248
|
+
fail(`${appName ?? pkg.name ?? "app"} must depend on a supported @rightkit/release version in devDependencies`);
|
|
247
249
|
}
|
|
248
250
|
}
|
|
249
251
|
|
|
252
|
+
function buildAllowedVersions(published = {}, staged = {}) {
|
|
253
|
+
const allowed = new Map();
|
|
254
|
+
for (const [name, version] of [...Object.entries(published), ...Object.entries(staged)]) {
|
|
255
|
+
const versions = allowed.get(name) ?? new Set();
|
|
256
|
+
versions.add(version);
|
|
257
|
+
allowed.set(name, versions);
|
|
258
|
+
}
|
|
259
|
+
return allowed;
|
|
260
|
+
}
|
|
261
|
+
|
|
250
262
|
async function commandEnv(command, root) {
|
|
251
263
|
const env = { ...(command.env ?? {}) };
|
|
252
264
|
for (const [name, rel] of Object.entries(command.envFiles ?? {})) {
|