@rightkit/release 0.2.27 → 0.2.29

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.
@@ -39,7 +39,7 @@ export function assertPublishedRightKitCargoDependencies(dependencies, published
39
39
  const allowed = configured instanceof Set ? configured : new Set(Array.isArray(configured) ? configured : [configured]);
40
40
  const exactVersions = new Set([...allowed].map((version) => `=${version}`));
41
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 an exact supported crates.io version (${[...exactVersions].join(" or ")})`);
42
+ throw new Error(`${label} ${dependency.name} must use exact crates.io versions (${[...exactVersions].join(" or ")})`);
43
43
  }
44
44
  }
45
45
  return checked;
@@ -135,20 +135,26 @@ export function assertLegalReleaseContract(root, legal, appName, platform) {
135
135
  appendices.push({ ...appendix, absolutePath: realFile });
136
136
  }
137
137
 
138
- if (platform === "win") {
139
- if (typeof legal.tauriConfig !== "string") fail(appName, "Windows legal gate requires legal.tauriConfig");
140
- const tauriPath = path.resolve(appRoot, legal.tauriConfig);
141
- if (!within(appRoot, tauriPath) || !existsSync(tauriPath)) fail(appName, `missing Tauri config: ${legal.tauriConfig}`);
142
- const tauri = readJson(tauriPath, appName, "Tauri config");
143
- 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");
151
- }
138
+ // The in-app gate is the SINGLE assent surface (Adrian, 2026-07-17). An
139
+ // installer/DMG can only PRESENT the text: it cannot record who agreed, cannot ask
140
+ // the individual-vs-enterprise basis, and passive updaters reach neither — so a
141
+ // license page is duplicate presentation that captures nothing. This gate
142
+ // previously REQUIRED bundle.licenseFile; it now requires its absence.
143
+ //
144
+ // Checked on EVERY platform, not just Windows: one bundle.licenseFile drives BOTH
145
+ // the NSIS license page and the macOS DMG SLA (branded-dmg.mjs derives its --eula
146
+ // from it), so a win-only check would let a mac build reintroduce the SLA.
147
+ if (typeof legal.tauriConfig !== "string") fail(appName, "the legal gate requires legal.tauriConfig");
148
+ const tauriPath = path.resolve(appRoot, legal.tauriConfig);
149
+ if (!within(appRoot, tauriPath) || !existsSync(tauriPath)) fail(appName, `missing Tauri config: ${legal.tauriConfig}`);
150
+ const tauri = readJson(tauriPath, appName, "Tauri config");
151
+ const configured = tauri.bundle?.licenseFile;
152
+ if (typeof configured === "string" && configured.trim()) {
153
+ fail(
154
+ appName,
155
+ "Tauri bundle.licenseFile must be absent: the in-app legal gate is the only assent surface, and an " +
156
+ "installer license page (NSIS) or DMG SLA duplicates it without recording acceptance",
157
+ );
152
158
  }
153
159
 
154
160
  return {
@@ -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,
@@ -98,10 +100,25 @@ test("rejects an explicitly unresolved release blocker even without a checkbox",
98
100
  assert.throws(() => assertLegalReleaseContract(root, legal, "fixture", "win"), /release blocker/i);
99
101
  });
100
102
 
101
- test("requires Windows installer clickthrough to use the exact EULA snapshot", () => {
103
+ test("rejects a Windows installer license page: the in-app gate is the only assent surface", () => {
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);
107
+ });
108
+
109
+ test("rejects a macOS DMG SLA too — one licenseFile drives both bundlers", () => {
110
+ // branded-dmg.mjs derives its --eula from bundle.licenseFile, so a Windows-only
111
+ // check would let a mac release quietly reintroduce the mount-time SLA.
112
+ const { root, legal } = fixture();
113
+ writeFileSync(path.join(root, "src-tauri", "tauri.conf.json"), JSON.stringify({ bundle: { licenseFile: "../legal/EULA.md" } }));
114
+ assert.throws(() => assertLegalReleaseContract(root, legal, "fixture", "mac"), /licenseFile must be absent/i);
115
+ });
116
+
117
+ test("accepts an app whose installer carries no license page", () => {
118
+ const { root, legal } = fixture();
119
+ for (const platform of ["win", "mac"]) {
120
+ assert.doesNotThrow(() => assertLegalReleaseContract(root, legal, "fixture", platform));
121
+ }
105
122
  });
106
123
 
107
124
  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.27",
3
+ "version": "0.2.29",
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
+ }