@rightkit/release 0.2.27 → 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.
@@ -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;
@@ -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.27",
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
+ }