@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 +1 -1
- package/publish-update.mjs +18 -22
- package/publish-update.test.mjs +24 -3
- package/release-token.mjs +54 -0
- package/release-token.test.mjs +21 -0
- package/right-suite-contract.test.mjs +2 -2
- package/rightkit-versions.json +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rightkit/release",
|
|
3
|
-
"version": "0.2.
|
|
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": {
|
package/publish-update.mjs
CHANGED
|
@@ -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.
|
|
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()
|
|
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()
|
|
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
|
-
|
|
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 ${
|
|
279
|
+
authorization: `Bearer ${releaseToken}`,
|
|
284
280
|
"content-type": "application/json",
|
|
285
281
|
"x-store-slug": process.env.RIGHTAPPS_STORE_SLUG || "rightapps",
|
|
286
282
|
},
|
package/publish-update.test.mjs
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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)) {
|
package/rightkit-versions.json
CHANGED