@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.
@@ -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 expected = published.get(dependency.name);
38
- if (!expected) throw new Error(`${label} ${dependency.name} is not a published RightKit crate`);
39
- if (!CRATES_IO_SOURCES.has(dependency.source) || dependency.path || dependency.registry || dependency.req !== `=${expected}`) {
40
- throw new Error(`${label} ${dependency.name} must use the exact crates.io version "=${expected}"`);
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;
@@ -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 !== "string" || !configured.trim()) {
145
- fail(appName, "Windows Tauri bundle.licenseFile must point to the snapshot EULA");
146
- }
147
- const configuredPath = path.resolve(path.dirname(tauriPath), configured);
148
- const eula = resolvedDocuments.find((document) => document.role === "eula");
149
- if (!eula || path.normalize(configuredPath).toLowerCase() !== path.normalize(eula.absolutePath).toLowerCase()) {
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
 
@@ -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: { licenseFile: "../legal/EULA.md" } }, null, 2)}\n`,
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/LICENSE.md" } }));
104
- assert.throws(() => assertLegalReleaseContract(root, legal, "fixture", "win"), /licenseFile.*EULA/i);
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.26",
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 RIGHTKIT_EXPECTED = new Map(Object.entries(RIGHTKIT_VERSIONS.npm));
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, RIGHTKIT_VERSIONS.cargo, config.app ?? path.basename(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 expected = RIGHTKIT_EXPECTED.get(name);
241
- if (expected && specifier !== expected) {
242
- fail(`${appName ?? pkg.name ?? "app"} ${name} must be ${expected}, got ${specifier}`);
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
- if (pkg.devDependencies?.["@rightkit/release"] !== RIGHTKIT_EXPECTED.get("@rightkit/release")) {
246
- fail(`${appName ?? pkg.name ?? "app"} must depend on @rightkit/release ${RIGHTKIT_EXPECTED.get("@rightkit/release")} in devDependencies`);
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 ?? {})) {