@rightkit/release 0.2.13 → 0.2.15

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rightkit/release",
3
- "version": "0.2.13",
3
+ "version": "0.2.15",
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": {
@@ -5,6 +5,7 @@ import path from "node:path";
5
5
  import { fileURLToPath, pathToFileURL } from "node:url";
6
6
  import { spawn } from "node:child_process";
7
7
  import { pruneReleaseObjects } from "./prune-r2.mjs";
8
+ import { loadReleaseToken } from "./release-token.mjs";
8
9
 
9
10
  const TOOL_ROOT = path.dirname(fileURLToPath(import.meta.url));
10
11
  const UPLOAD = path.join(TOOL_ROOT, "upload-large.mjs");
@@ -37,8 +38,19 @@ const target = config?.targets?.[platform];
37
38
  const artifacts = target?.updater?.artifacts ?? [];
38
39
  const installers = target?.installer?.artifacts ?? [];
39
40
 
41
+ if (tier === "patch" && !installers.length) fail(`${config?.app ?? "app"} ${platform} patch has no installer.artifacts`);
42
+ if (tier === "update" && !artifacts.length) fail(`${config?.app ?? "app"} ${platform} update has no updater.artifacts`);
43
+
44
+ let releaseToken = null;
45
+ if (!dryRun && (tier === "update" || artifacts.length > 0)) {
46
+ try {
47
+ releaseToken = await loadReleaseToken();
48
+ } catch (error) {
49
+ fail(error.message);
50
+ }
51
+ }
52
+
40
53
  if (tier === "patch") {
41
- if (!installers.length) fail(`${config?.app ?? "app"} ${platform} patch has no installer.artifacts`);
42
54
  for (const installer of installers) {
43
55
  assertCurrentKey(installer.key, "installer");
44
56
  const file = path.resolve(root, installer.file);
@@ -49,7 +61,6 @@ if (tier === "patch") {
49
61
  const updaterManifestArtifacts = new Map();
50
62
  const installerManifestArtifacts = new Map();
51
63
  const uploadedKeys = new Set();
52
- const uploadedSignatures = new Set();
53
64
  for (const artifact of artifacts) {
54
65
  assertCurrentKey(artifact.key, "updater");
55
66
  const installerKey = patchInstallerKey(artifact);
@@ -65,11 +76,6 @@ if (tier === "patch") {
65
76
  if (!dryRun) await run(process.execPath, [UPLOAD, file, installerKey, "public"], root);
66
77
  uploadedKeys.add(installerKey);
67
78
  }
68
- if (!uploadedSignatures.has(`${installerKey}.sig`)) {
69
- console.log(`${dryRun ? "[dry-run] " : ""}UPLOAD public/${installerKey}.sig`);
70
- if (!dryRun) await run(process.execPath, [UPLOAD, signatureFile, `${installerKey}.sig`, "public"], root);
71
- uploadedSignatures.add(`${installerKey}.sig`);
72
- }
73
79
  const signature = dryRun ? `<signature:${artifact.signature}>` : (await readFile(signatureFile, "utf8")).trim();
74
80
  const artifactKey = artifact.artifactKey || installerKey.replaceAll("/", "__");
75
81
  const metadata = dryRun ? { sha256: null, sizeBytes: null } : await fileMetadata(file);
@@ -108,7 +114,7 @@ if (tier === "patch") {
108
114
  dryRun,
109
115
  keep: {
110
116
  public: installers.map((artifact) => artifact.key),
111
- private: target.updater.artifacts.flatMap((artifact) => [artifact.key, `${artifact.key}.sig`]),
117
+ private: target.updater.artifacts.map((artifact) => artifact.key),
112
118
  },
113
119
  });
114
120
  process.exit(0);
@@ -144,20 +150,17 @@ if (tier === "patch") {
144
150
  platform: platform === "mac" ? "mac" : "windows",
145
151
  dryRun,
146
152
  keep: {
147
- public: [...installers.map((artifact) => artifact.key), ...registrations.keys(), ...[...registrations.keys()].map((key) => `${key}.sig`)],
153
+ public: [...installers.map((artifact) => artifact.key), ...registrations.keys()],
148
154
  private: [],
149
155
  },
150
156
  });
151
157
  process.exit(0);
152
158
  }
153
159
 
154
- if (!artifacts.length) fail(`${config?.app ?? "app"} ${platform} update has no updater.artifacts`);
155
-
156
160
  const platforms = {};
157
161
  const registrations = new Map();
158
162
  const updaterManifestArtifacts = new Map();
159
163
  const uploadedKeys = new Set();
160
- const uploadedSignatures = new Set();
161
164
  for (const artifact of artifacts) {
162
165
  assertCurrentKey(artifact.key, "updater");
163
166
  const file = path.resolve(root, artifact.file);
@@ -171,12 +174,6 @@ for (const artifact of artifacts) {
171
174
  if (!dryRun) await run(process.execPath, [UPLOAD, file, artifact.key, "private"], root);
172
175
  uploadedKeys.add(artifact.key);
173
176
  }
174
- if (!uploadedSignatures.has(`${artifact.key}.sig`)) {
175
- console.log(`${dryRun ? "[dry-run] " : ""}UPLOAD private/${artifact.key}.sig`);
176
- if (!dryRun) await run(process.execPath, [UPLOAD, signatureFile, `${artifact.key}.sig`, "private"], root);
177
- uploadedSignatures.add(`${artifact.key}.sig`);
178
- }
179
-
180
177
  const signature = dryRun ? `<signature:${artifact.signature}>` : (await readFile(signatureFile, "utf8")).trim();
181
178
  const artifactKey = artifact.artifactKey || artifact.key.replaceAll("/", "__");
182
179
  const metadata = dryRun ? { sha256: null, sizeBytes: null } : await fileMetadata(file);
@@ -227,7 +224,7 @@ await pruneReleaseObjects({
227
224
  dryRun,
228
225
  keep: {
229
226
  public: installers.map((artifact) => artifact.key),
230
- private: [...registrations.keys(), ...[...registrations.keys()].map((key) => `${key}.sig`)],
227
+ private: [...registrations.keys()],
231
228
  },
232
229
  });
233
230
 
@@ -275,12 +272,11 @@ function patchInstallerKey(artifact) {
275
272
  }
276
273
 
277
274
  async function register(route, body) {
278
- const token = process.env.RIGHTAPPS_ADMIN_TOKEN;
279
- if (!token) fail("RIGHTAPPS_ADMIN_TOKEN is required to register an update");
275
+ if (!releaseToken) fail("RIGHTAPPS_RELEASE_TOKEN is required to register an update");
280
276
  const response = await fetch(`${API_BASE}${route}`, {
281
277
  method: "POST",
282
278
  headers: {
283
- authorization: `Bearer ${token}`,
279
+ authorization: `Bearer ${releaseToken}`,
284
280
  "content-type": "application/json",
285
281
  "x-store-slug": process.env.RIGHTAPPS_STORE_SLUG || "rightapps",
286
282
  },
@@ -8,7 +8,7 @@ import test from "node:test";
8
8
 
9
9
  const publisher = fileURLToPath(new URL("./publish-update.mjs", import.meta.url));
10
10
 
11
- function fixture() {
11
+ function fixture({ materialize = false } = {}) {
12
12
  const dir = mkdtempSync(path.join(os.tmpdir(), "right-publish-test-"));
13
13
  const config = path.join(dir, "right-release.config.mjs");
14
14
  writeFileSync(config, `export default ${JSON.stringify({
@@ -27,6 +27,11 @@ function fixture() {
27
27
  },
28
28
  },
29
29
  })};\n`);
30
+ if (materialize) {
31
+ writeFileSync(path.join(dir, "Fixture-Setup.exe"), "signed installer");
32
+ writeFileSync(path.join(dir, "Fixture.exe"), "signed updater");
33
+ writeFileSync(path.join(dir, "Fixture.exe.sig"), "updater signature");
34
+ }
30
35
  return config;
31
36
  }
32
37
 
@@ -100,7 +105,8 @@ test("patch registers a free updater manifest backed by public installer-path ob
100
105
  assert.match(result.stdout, /POST .*\/v1\/admin\/apps\/patches/);
101
106
  assert.match(result.stdout, /"tier":"patch"/);
102
107
  assert.match(result.stdout, /"platform":"windows"/);
103
- assert.equal((result.stdout.match(/UPLOAD public\/fixture\/installers\/windows\/current\/Fixture\.exe\.sig/g) ?? []).length, 1);
108
+ assert.doesNotMatch(result.stdout, /UPLOAD .*\.sig/);
109
+ assert.equal((result.stdout.match(/UPLOAD /g) ?? []).length, 2, "patch may upload only one installer and one updater object per OS");
104
110
  assert.match(result.stdout, /PRUNE rightapps-downloads\/fixture\/windows/);
105
111
  assert.match(result.stdout, /PRUNE rightapps-updates\/fixture\/windows keep=<none>/);
106
112
  const body = registrationBody(result);
@@ -129,7 +135,8 @@ test("routes feature updates to the Pro-gated release endpoint", () => {
129
135
  assert.match(result.stdout, /"tier":"update"/);
130
136
  assert.match(result.stdout, /"platform":"windows"/);
131
137
  assert.match(result.stdout, /UPLOAD private\/fixture\/updates\/windows\/current\/Fixture\.exe/);
132
- assert.equal((result.stdout.match(/UPLOAD private\/fixture\/updates\/windows\/current\/Fixture\.exe\.sig/g) ?? []).length, 1);
138
+ assert.doesNotMatch(result.stdout, /UPLOAD .*\.sig/);
139
+ assert.equal((result.stdout.match(/UPLOAD /g) ?? []).length, 1, "feature update may upload only one updater object per OS");
133
140
  assert.match(result.stdout, /PRUNE rightapps-downloads\/fixture\/windows/);
134
141
  assert.match(result.stdout, /PRUNE rightapps-updates\/fixture\/windows/);
135
142
  assert.doesNotMatch(result.stdout, /UPLOAD .*Fixture-Setup\.exe/);
@@ -179,3 +186,17 @@ test("refuses versioned updater object keys for gated feature updates", () => {
179
186
  assert.notEqual(result.status, 0);
180
187
  assert.match(result.stderr, /current R2 object/);
181
188
  });
189
+
190
+ test("requires the durable release credential before the first patch upload", () => {
191
+ const config = fixture({ materialize: true });
192
+ const env = { ...process.env, RIGHT_RELEASE_TIER: "patch", RIGHTAPPS_ADMIN_TOKEN: "expired-admin-session" };
193
+ delete env.RIGHTAPPS_RELEASE_TOKEN;
194
+ delete env.RIGHTAPPS_RELEASE_TOKEN_FILE;
195
+ const result = spawnSync(process.execPath, [publisher, "--config", config, "--platform", "win"], {
196
+ encoding: "utf8",
197
+ env,
198
+ });
199
+ assert.notEqual(result.status, 0);
200
+ assert.match(result.stderr, /RIGHTAPPS_RELEASE_TOKEN/);
201
+ assert.doesNotMatch(result.stdout, /UPLOAD /, "credential preflight must run before R2 mutation");
202
+ });
@@ -0,0 +1,54 @@
1
+ import { execFile } from "node:child_process";
2
+ import { readFile } from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { promisify } from "node:util";
6
+
7
+ const execFileAsync = promisify(execFile);
8
+ const KEYCHAIN_SERVICE = "rightkit-rightapps-release-token";
9
+
10
+ export function defaultWindowsReleaseTokenFile(env = process.env) {
11
+ const appData = env.APPDATA || path.join(os.homedir(), "AppData", "Roaming");
12
+ return path.join(appData, "RightKit", "rightapps-release.token");
13
+ }
14
+
15
+ async function readTokenFile(file) {
16
+ const token = (await readFile(file, "utf8")).trim();
17
+ if (!token) throw new Error(`release token file is empty: ${file}`);
18
+ return token;
19
+ }
20
+
21
+ export async function loadReleaseToken({ env = process.env, platform = process.platform } = {}) {
22
+ const direct = env.RIGHTAPPS_RELEASE_TOKEN?.trim();
23
+ if (direct) return direct;
24
+
25
+ if (env.RIGHTAPPS_RELEASE_TOKEN_FILE) {
26
+ return readTokenFile(path.resolve(env.RIGHTAPPS_RELEASE_TOKEN_FILE));
27
+ }
28
+
29
+ if (platform === "win32") {
30
+ try {
31
+ return await readTokenFile(defaultWindowsReleaseTokenFile(env));
32
+ } catch (error) {
33
+ throw new Error(`RIGHTAPPS_RELEASE_TOKEN is unavailable (${error.message})`);
34
+ }
35
+ }
36
+
37
+ if (platform === "darwin") {
38
+ try {
39
+ const { stdout } = await execFileAsync("security", [
40
+ "find-generic-password",
41
+ "-a", env.USER || os.userInfo().username,
42
+ "-s", KEYCHAIN_SERVICE,
43
+ "-w",
44
+ ], { windowsHide: true });
45
+ const token = stdout.trim();
46
+ if (!token) throw new Error("Keychain item is empty");
47
+ return token;
48
+ } catch (error) {
49
+ throw new Error(`RIGHTAPPS_RELEASE_TOKEN is unavailable (macOS Keychain service ${KEYCHAIN_SERVICE}: ${error.message})`);
50
+ }
51
+ }
52
+
53
+ throw new Error("RIGHTAPPS_RELEASE_TOKEN or RIGHTAPPS_RELEASE_TOKEN_FILE is required");
54
+ }
@@ -0,0 +1,21 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtempSync, writeFileSync } from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import test from "node:test";
6
+ import { defaultWindowsReleaseTokenFile, loadReleaseToken } from "./release-token.mjs";
7
+
8
+ test("loads an explicit release token without exposing admin-session fallback", async () => {
9
+ assert.equal(await loadReleaseToken({ env: { RIGHTAPPS_RELEASE_TOKEN: " durable-token ", RIGHTAPPS_ADMIN_TOKEN: "admin-session" } }), "durable-token");
10
+ });
11
+
12
+ test("loads the release token from an explicit protected-file path", async () => {
13
+ const dir = mkdtempSync(path.join(os.tmpdir(), "right-release-token-"));
14
+ const file = path.join(dir, "token");
15
+ writeFileSync(file, "file-token\n");
16
+ assert.equal(await loadReleaseToken({ env: { RIGHTAPPS_RELEASE_TOKEN_FILE: file }, platform: "linux" }), "file-token");
17
+ });
18
+
19
+ test("uses the portable Windows token location", () => {
20
+ assert.equal(defaultWindowsReleaseTokenFile({ APPDATA: "C:\\Users\\fixture\\AppData\\Roaming" }), path.join("C:\\Users\\fixture\\AppData\\Roaming", "RightKit", "rightapps-release.token"));
21
+ });
@@ -19,10 +19,10 @@ const apps = [
19
19
  test("RightKit exposes one current version manifest", () => {
20
20
  assert.equal(existsSync(versionsPath), true, "rightkit-versions.json must be the single current-version source");
21
21
  assert.match(versions.packageManager, /^pnpm@\d+\.\d+\.\d+$/);
22
- assert.equal(versions.npm["@rightkit/release"], "0.2.13");
22
+ assert.equal(versions.npm["@rightkit/release"], "0.2.15");
23
23
  assert.equal(versions.npm["@rightkit/license"], "0.1.5");
24
24
  assert.equal(versions.npm["@rightkit/logs"], "0.1.3");
25
- assert.equal(versions.npm["@rightkit/updates"], "0.1.1");
25
+ assert.equal(versions.npm["@rightkit/updates"], "0.2.1");
26
26
  assert.equal(versions.cargo["rightkit-license"], "0.1.1");
27
27
  assert.equal(versions.cargo["rightkit-logs"], "0.1.0");
28
28
  for (const [crate, version] of Object.entries(versions.cargo)) {
@@ -4,8 +4,8 @@
4
4
  "npm": {
5
5
  "@rightkit/license": "0.1.5",
6
6
  "@rightkit/logs": "0.1.3",
7
- "@rightkit/release": "0.2.13",
8
- "@rightkit/updates": "0.1.1"
7
+ "@rightkit/release": "0.2.15",
8
+ "@rightkit/updates": "0.2.1"
9
9
  },
10
10
  "cargo": {
11
11
  "rightkit-license": "0.1.1",