@rightkit/release 0.2.57 → 0.2.59

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.
@@ -0,0 +1,199 @@
1
+ #!/usr/bin/env node
2
+ import { createHash } from "node:crypto";
3
+ import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { spawnSync } from "node:child_process";
7
+ import { fileURLToPath, pathToFileURL } from "node:url";
8
+ import { addonManifestSha256, addonRoutes, canonicalAddonManifest, createAddonManifest, downloadImmutableAddOn, immutableAddonManifestUrl, validateAddonConfig, validateAddonManifest } from "./addon-contract.mjs";
9
+ import { assertPrimaryReleaseCheckout } from "./release-invocation.mjs";
10
+ import { assertCleanSource } from "./source-gate.mjs";
11
+
12
+ const ROOT = path.dirname(fileURLToPath(import.meta.url));
13
+ const SIGN_WINDOWS = path.join(ROOT, "sign-windows.mjs");
14
+ const HARDEN = path.join(ROOT, "hardeningscan.mjs");
15
+ const [operation, ...args] = process.argv.slice(2);
16
+ const options = parse(args);
17
+ if (!new Set(["doctor", "build", "upload", "adopt"]).has(operation)) usage(2);
18
+ if (!new Set(["mac", "win"]).has(options.platform)) fail("--platform must be mac or win");
19
+
20
+ if (operation === "adopt") await adopt(options);
21
+ else {
22
+ if (!options.config) fail("--config is required");
23
+ const configPath = path.resolve(options.config);
24
+ const root = path.dirname(configPath);
25
+ const repoRoot = git(root, ["rev-parse", "--show-toplevel"]);
26
+ assertPrimaryReleaseCheckout(repoRoot);
27
+ const config = (await import(`${pathToFileURL(configPath).href}?addon=${Date.now()}`)).default;
28
+ if (operation === "doctor") doctor({ config, configPath, root, repoRoot, options });
29
+ else if (operation === "build") await build({ config, configPath, root, repoRoot, options });
30
+ else await upload({ config, root, repoRoot, options });
31
+ }
32
+
33
+ function doctor({ config, configPath, root, repoRoot, options }) {
34
+ validateAddonConfig(config, { root, platform: options.platform, allowMissingExecutables: true });
35
+ console.log(`right-release addon doctor: ${config.addon} ${config.version} ${options.platform}`);
36
+ console.log(`config: ${configPath}`);
37
+ console.log(`repo: ${repoRoot}`);
38
+ }
39
+
40
+ async function build({ config, configPath, root, repoRoot, options }) {
41
+ const commit = git(repoRoot, ["rev-parse", "HEAD"]);
42
+ const { target } = validateAddonConfig(config, { root, platform: options.platform, allowMissingExecutables: true });
43
+ if (options.dryRun) {
44
+ console.log(`right-release addon build dry-run: ${config.addon} ${config.version} ${options.platform}`);
45
+ console.log(`config: ${configPath}`);
46
+ console.log(`commit: ${commit}`);
47
+ console.log(`would run: ${target.build.cmd} ${target.build.args.join(" ")}`);
48
+ return;
49
+ }
50
+ assertCleanSource({ status: git(repoRoot, ["status", "--porcelain=v1", "-z", "--untracked-files=all"]), commandId: "right-release addon build" });
51
+ run(target.build.cmd, target.build.args, root);
52
+ const signing = signExecutableRoles(config, root, options.platform);
53
+ const manifest = createAddonManifest({ config, root, platform: options.platform, commit, signing });
54
+ const digest = addonManifestSha256(manifest);
55
+ const sealed = path.join(repoRoot, ".right-release", "addons", config.addon, config.version, commit.slice(0, 8), options.platform);
56
+ if (existsSync(sealed)) fail(`add-on seal already exists: ${sealed}`);
57
+ const temp = `${sealed}.tmp-${process.pid}`;
58
+ mkdirSync(temp, { recursive: true });
59
+ for (const file of config.targets[options.platform].files ?? config.files) copyFileSync(path.resolve(root, file.source), path.join(temp, file.name));
60
+ writeFileSync(path.join(temp, "addon-manifest.json"), canonicalAddonManifest(manifest));
61
+ run(process.execPath, [HARDEN, ...manifest.files.map((file) => path.join(temp, file.name))], root);
62
+ mkdirSync(path.dirname(sealed), { recursive: true });
63
+ renameSync(temp, sealed);
64
+ console.log(`right-release addon build: sealed ${sealed}`);
65
+ console.log(`manifestSha256: ${digest}`);
66
+ }
67
+
68
+ function signExecutableRoles(config, root, platform) {
69
+ const files = config.targets[platform].files ?? config.files;
70
+ const executable = files.filter((file) => file.executable === true);
71
+ if (platform === "win") run(process.execPath, [SIGN_WINDOWS, ...executable.map((file) => path.resolve(root, file.source))], root);
72
+ else for (const file of executable) {
73
+ const absolute = path.resolve(root, file.source);
74
+ run("codesign", ["--force", "--options", "runtime", "--sign", "Developer ID Application", absolute], root);
75
+ const output = command("codesign", ["-dv", "--verbose=4", absolute], root);
76
+ if (!new RegExp(`TeamIdentifier=${config.targets.mac.signing.teamId}`).test(output)) fail(`codesign team mismatch: ${file.name}`);
77
+ run("codesign", ["--verify", "--strict", "--verbose=2", absolute], root);
78
+ }
79
+ return Object.fromEntries(executable.map((file) => [file.role, { contract: config.targets[platform].signing.contract, status: "verified" }]));
80
+ }
81
+
82
+ async function upload({ config, root, repoRoot, options }) {
83
+ if (config.distribution?.provider === "github-releases") fail("this add-on publishes with right-release github --addon <config> --platform <platform>");
84
+ if (!new Set(["patch", "update"]).has(options.tier)) fail("add-on upload requires --tier patch|update");
85
+ const commit = git(repoRoot, ["rev-parse", "HEAD"]);
86
+ const sealed = path.join(repoRoot, ".right-release", "addons", config.addon, config.version, commit.slice(0, 8), options.platform);
87
+ const manifest = JSON.parse(readFileSync(path.join(sealed, "addon-manifest.json"), "utf8"));
88
+ validateAddonManifest(manifest);
89
+ const routes = addonRoutes(manifest);
90
+ if (options.dryRun) {
91
+ for (const route of routes) console.log(`would upload ${route.key}${route.manifest ? " (last)" : ""}`);
92
+ return;
93
+ }
94
+ if (!process.env.CLOUDFLARE_API_TOKEN) fail("CLOUDFLARE_API_TOKEN is required before R2 mutation");
95
+ for (const route of routes) {
96
+ const source = route.manifest ? path.join(sealed, "addon-manifest.json") : path.join(sealed, route.name);
97
+ await putImmutable({ source, route, repoRoot });
98
+ }
99
+ console.log(`right-release addon upload: verified ${config.addon} ${options.platform} tier=${options.tier}`);
100
+ }
101
+
102
+ async function putImmutable({ source, route, repoRoot }) {
103
+ const target = `rightapps-downloads/${route.key}`;
104
+ const temp = path.join(os.tmpdir(), `right-addon-r2-${process.pid}-${createHash("sha256").update(route.key).digest("hex").slice(0, 12)}`);
105
+ const existing = wrangler(["r2", "object", "get", target, "--file", temp, "--remote"], repoRoot);
106
+ if (existing.status === 0) {
107
+ const actual = sha256(temp); rmSync(temp, { force: true });
108
+ if (actual !== route.sha256) fail(`immutable object exists with different bytes: ${route.key}`);
109
+ return;
110
+ }
111
+ if (!/not found|does not exist|404/i.test(`${existing.stdout}\n${existing.stderr}`)) fail(`cannot inspect immutable object: ${route.key}`);
112
+ const uploaded = wrangler(["r2", "object", "put", target, "--file", source, "--remote"], repoRoot);
113
+ if (uploaded.status !== 0) fail(`immutable upload failed: ${route.key}`);
114
+ }
115
+
116
+ async function adopt(options) {
117
+ if (!options.lock || !options.output) fail("adopt requires --lock and --output");
118
+ const lockPath = path.resolve(options.lock);
119
+ const lock = JSON.parse(readFileSync(lockPath, "utf8"));
120
+ const entry = lock.targets?.[options.platform] ?? lock[options.platform];
121
+ if (!entry?.manifestSha256 || !entry?.manifestUrl) fail("add-on lock needs platform manifestUrl and manifestSha256");
122
+ const temporary = options.source ? null : mkdtempSync(path.join(os.tmpdir(), "right-addon-adopt-"));
123
+ const source = options.source ? path.resolve(options.source) : temporary;
124
+ if (!options.source) await downloadImmutableAddOn(entry.manifestUrl, source);
125
+ const manifest = JSON.parse(readFileSync(path.join(source, "addon-manifest.json"), "utf8"));
126
+ validateAddonManifest(manifest);
127
+ if (addonManifestSha256(manifest) !== entry.manifestSha256) fail("add-on lock manifest SHA mismatch");
128
+ if (!immutableAddonManifestUrl(manifest, entry.manifestUrl, entry.manifestSha256)) fail("add-on lock manifestUrl is not immutable");
129
+ const output = path.resolve(options.output);
130
+ const destination = path.join(output, manifest.addon);
131
+ const stage = `${destination}.tmp-${process.pid}`;
132
+ const adoptedFiles = [];
133
+ rmSync(stage, { recursive: true, force: true }); mkdirSync(stage, { recursive: true });
134
+ for (const file of manifest.files) {
135
+ const sourceFile = path.join(source, file.name);
136
+ if (!existsSync(sourceFile) || statSync(sourceFile).size !== file.sizeBytes || sha256(sourceFile) !== file.sha256) fail(`add-on file verification failed: ${file.name}`);
137
+ verifyAdoptedSignature({ file, sourceFile, platform: manifest.platform, sourceFixture: Boolean(options.source) });
138
+ const subdir = file.role === "command" || file.role === "service" ? "bin" : file.role === "icon" || LEGAL(file.role) ? "resources" : "resources";
139
+ mkdirSync(path.join(stage, subdir), { recursive: true });
140
+ const installed = path.join(stage, subdir, file.name);
141
+ copyFileSync(sourceFile, installed);
142
+ // Tauri resolves externalBin artifacts by Rust target triple. Keep the
143
+ // portable role name too, then provide the required target-qualified alias.
144
+ if (file.role === "command" || file.role === "service") {
145
+ const base = file.name.replace(/\.exe$/i, "");
146
+ const suffix = manifest.platform === "win" ? `${base}-${manifest.targetTriple}.exe` : `${base}-${manifest.targetTriple}`;
147
+ const alias = path.join(stage, "bin", suffix);
148
+ copyFileSync(sourceFile, alias);
149
+ const adoptedSha256 = sha256(alias);
150
+ if (adoptedSha256 !== file.sha256) fail(`adopted alias hash mismatch: ${file.name}`);
151
+ adoptedFiles.push({ role: file.role, name: file.name, sealedSha256: file.sha256, adoptedName: `bin/${suffix}`, adoptedSha256 });
152
+ }
153
+ }
154
+ copyFileSync(path.join(source, "addon-manifest.json"), path.join(stage, "addon-manifest.json"));
155
+ // Directory rename is atomic on one filesystem. Move any previous add-on
156
+ // aside first, restore it if replacement fails, then clean it only after the
157
+ // new verified tree is in place.
158
+ const previous = `${destination}.previous-${process.pid}`;
159
+ mkdirSync(output, { recursive: true });
160
+ if (existsSync(destination)) renameSync(destination, previous);
161
+ try {
162
+ renameSync(stage, destination);
163
+ } catch (error) {
164
+ if (existsSync(previous)) renameSync(previous, destination);
165
+ throw error;
166
+ }
167
+ rmSync(previous, { recursive: true, force: true });
168
+ if (temporary) rmSync(temporary, { recursive: true, force: true });
169
+ const repoRoot = git(path.dirname(lockPath), ["rev-parse", "--show-toplevel"]);
170
+ const receipt = path.join(repoRoot, ".right-release", "addons", "adoptions", `${manifest.addon}-${manifest.platform}.json`);
171
+ mkdirSync(path.dirname(receipt), { recursive: true });
172
+ writeFileSync(receipt, `${JSON.stringify({ schema: 1, addon: manifest.addon, platform: manifest.platform, manifestSha256: entry.manifestSha256, manifestUrl: entry.manifestUrl, output: destination, files: adoptedFiles, adoptedAt: new Date().toISOString() }, null, 2)}\n`);
173
+ console.log(`right-release addon adopt: ${destination}`);
174
+ console.log(`receipt: ${receipt}`);
175
+ }
176
+
177
+ function verifyAdoptedSignature({ file, sourceFile, platform, sourceFixture }) {
178
+ if (!file.executable) return;
179
+ if (file.signing.contract === "test-fixture-v1" && sourceFixture && process.env.RIGHT_RELEASE_TEST_FIXTURE === "1") return;
180
+ if (platform === "mac") {
181
+ if (file.signing.contract !== "apple-developer-id-executable-v1") fail(`unexpected mac signing contract: ${file.name}`);
182
+ const output = command("codesign", ["-dv", "--verbose=4", sourceFile], path.dirname(sourceFile));
183
+ if (!/TeamIdentifier=6KLGD3LLKF/.test(output)) fail(`codesign team mismatch: ${file.name}`);
184
+ run("codesign", ["--verify", "--strict", "--verbose=2", sourceFile], path.dirname(sourceFile));
185
+ } else {
186
+ if (file.signing.contract !== "azure-artifact-signing-v1") fail(`unexpected Windows signing contract: ${file.name}`);
187
+ run(process.execPath, [SIGN_WINDOWS, "--verify-only", sourceFile], path.dirname(sourceFile));
188
+ }
189
+ }
190
+
191
+ function LEGAL(role) { return new Set(["license", "eula", "privacy", "third-party-notices"]).has(role); }
192
+ function parse(args) { const value = { platform: process.platform === "win32" ? "win" : "mac", dryRun: false }; for (let i = 0; i < args.length; i += 1) { const key = args[i]; if (key === "--dry-run") value.dryRun = true; else if (["--config", "--platform", "--tier", "--lock", "--output", "--source"].includes(key)) value[key.slice(2).replace(/-([a-z])/g, (_, c) => c.toUpperCase())] = args[++i]; else fail(`unknown argument: ${key}`); } return value; }
193
+ function git(cwd, args) { return command("git", args, cwd).trim(); }
194
+ function command(cmd, args, cwd) { const result = spawnSync(cmd, args, { cwd, encoding: "utf8", windowsHide: true }); if (result.status !== 0) throw new Error(`${cmd} ${args.join(" ")} failed: ${result.stderr || result.stdout}`); return `${result.stdout ?? ""}${result.stderr ?? ""}`; }
195
+ function run(cmd, args, cwd) { command(cmd, args, cwd); }
196
+ function wrangler(args, cwd) { return spawnSync("pnpm", ["dlx", "wrangler@4", ...args], { cwd, encoding: "utf8", windowsHide: true }); }
197
+ function sha256(file) { return createHash("sha256").update(readFileSync(file)).digest("hex"); }
198
+ function usage(code) { console.log("usage: right-release addon doctor|build|upload|adopt --platform mac|win ..."); process.exit(code); }
199
+ function fail(message) { console.error(`right-release addon: ${message}`); process.exit(1); }
@@ -0,0 +1,54 @@
1
+ import assert from "node:assert/strict";
2
+ import { existsSync, mkdtempSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { execFileSync, spawnSync } from "node:child_process";
6
+ import { fileURLToPath, pathToFileURL } from "node:url";
7
+ import test from "node:test";
8
+ import { createAddonManifest, addonManifestSha256, canonicalAddonManifest, downloadImmutableAddOn, immutableAddonManifestUrl } from "./addon-contract.mjs";
9
+
10
+ const command = fileURLToPath(new URL("./addon-command.mjs", import.meta.url));
11
+ function fixture() {
12
+ const root = mkdtempSync(path.join(os.tmpdir(), "right-addon-command-"));
13
+ mkdirSync(path.join(root, "legal")); mkdirSync(path.join(root, "assets"));
14
+ for (const name of ["LICENSE", "EULA.txt", "PRIVACY.md", "THIRD-PARTY-NOTICES.txt"]) writeFileSync(path.join(root, "legal", name), name);
15
+ writeFileSync(path.join(root, "assets", "tab.png"), "icon");
16
+ const files = [["command", "membrane", "out/membrane", true], ["service", "crypt-service", "out/crypt-service", true], ["icon", "membrane-tab-icon.png", "assets/tab.png", false], ["license", "LICENSE", "legal/LICENSE", false], ["eula", "EULA.txt", "legal/EULA.txt", false], ["privacy", "PRIVACY.md", "legal/PRIVACY.md", false], ["third-party-notices", "THIRD-PARTY-NOTICES.txt", "legal/THIRD-PARTY-NOTICES.txt", false]].map(([role, name, source, executable]) => ({ role, name, source, executable }));
17
+ writeFileSync(path.join(root, "right-addon.config.mjs"), `export default ${JSON.stringify({ schema: 1, kind: "headless-addon", addon: "membrane", version: "0.1.0", packageManager: "pnpm", checks: [], buildInputs: { include: ["right-addon.config.mjs"] }, consumer: { contract: "orthic-product-v1" }, targets: { mac: { targetTriple: "aarch64-apple-darwin", build: { cmd: "false", args: [] }, signing: { contract: "apple-developer-id-executable-v1", teamId: "6KLGD3LLKF" }, files }, win: { targetTriple: "x86_64-pc-windows-msvc", build: { cmd: "false", args: [] }, signing: { contract: "azure-artifact-signing-v1" }, files } } }, null, 2)};\n`);
18
+ execFileSync("git", ["init", "--initial-branch", "main"], { cwd: root }); execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: root }); execFileSync("git", ["config", "user.name", "Test"], { cwd: root }); execFileSync("git", ["add", "."], { cwd: root }); execFileSync("git", ["commit", "-m", "init"], { cwd: root });
19
+ return root;
20
+ }
21
+ test("add-on dry-run creates no release state and never executes build", () => {
22
+ const root = fixture(); const result = spawnSync(process.execPath, [command, "build", "--config", "right-addon.config.mjs", "--platform", "mac", "--dry-run"], { cwd: root, encoding: "utf8" });
23
+ assert.equal(result.status, 0, result.stderr); assert.match(result.stdout, /would run: false/); assert.equal(existsSync(path.join(root, ".right-release")), false);
24
+ });
25
+
26
+ test("local adoption verifies lock and stages portable plus Tauri-qualified binaries", async () => {
27
+ const root = fixture(); const config = (await import(`${pathToFileURL(path.join(root, "right-addon.config.mjs")).href}?test=${Date.now()}`)).default;
28
+ mkdirSync(path.join(root, "out")); writeFileSync(path.join(root, "out", "membrane"), "command"); writeFileSync(path.join(root, "out", "crypt-service"), "service");
29
+ const manifest = createAddonManifest({ config, root, platform: "mac", commit: "d".repeat(40), signing: { command: { contract: "test-fixture-v1", status: "verified" }, service: { contract: "test-fixture-v1", status: "verified" } } });
30
+ const sealed = path.join(root, "sealed"); mkdirSync(sealed);
31
+ for (const file of manifest.files) writeFileSync(path.join(sealed, file.name), readFileSync(path.join(root, config.targets.mac.files.find((entry) => entry.role === file.role).source)));
32
+ writeFileSync(path.join(sealed, "addon-manifest.json"), canonicalAddonManifest(manifest));
33
+ const digest = addonManifestSha256(manifest); const lock = path.join(root, "membrane.lock.json"); writeFileSync(lock, JSON.stringify({ targets: { mac: { manifestUrl: `https://example.test/membrane/addons/mac/sha256/${digest}/addon-manifest.json`, manifestSha256: digest } } }));
34
+ const output = path.join(root, "addons"); const unsigned = spawnSync(process.execPath, [command, "adopt", "--platform", "mac", "--lock", lock, "--output", `${output}-unsigned`, "--source", sealed], { cwd: root, encoding: "utf8", env: { ...process.env, RIGHT_RELEASE_TEST_FIXTURE: "" } });
35
+ assert.notEqual(unsigned.status, 0); assert.match(unsigned.stderr, /unexpected mac signing contract/);
36
+ const result = spawnSync(process.execPath, [command, "adopt", "--platform", "mac", "--lock", lock, "--output", output, "--source", sealed], { cwd: root, encoding: "utf8", env: { ...process.env, RIGHT_RELEASE_TEST_FIXTURE: "1" } });
37
+ assert.equal(result.status, 0, result.stderr);
38
+ assert.equal(readFileSync(path.join(output, "membrane", "bin", "membrane-aarch64-apple-darwin"), "utf8"), "command");
39
+ assert.equal(readFileSync(path.join(output, "membrane", "resources", "LICENSE"), "utf8"), "LICENSE");
40
+ const receipt = JSON.parse(readFileSync(path.join(root, ".right-release", "addons", "adoptions", "membrane-mac.json"), "utf8"));
41
+ assert.equal(receipt.manifestSha256, digest);
42
+ assert.deepEqual(receipt.files.map((file) => [file.role, file.sealedSha256, file.adoptedSha256]), [["command", manifest.files.find((file) => file.role === "command").sha256, manifest.files.find((file) => file.role === "command").sha256], ["service", manifest.files.find((file) => file.role === "service").sha256, manifest.files.find((file) => file.role === "service").sha256]]);
43
+ });
44
+
45
+ test("remote downloader accepts only immutable manifest plus file routes", async () => {
46
+ const root = fixture(); const config = (await import(`${pathToFileURL(path.join(root, "right-addon.config.mjs")).href}?remote=${Date.now()}`)).default;
47
+ mkdirSync(path.join(root, "out")); writeFileSync(path.join(root, "out", "membrane"), "command"); writeFileSync(path.join(root, "out", "crypt-service"), "service");
48
+ const manifest = createAddonManifest({ config, root, platform: "mac", commit: "e".repeat(40), signing: { command: { contract: "test-fixture-v1", status: "verified" }, service: { contract: "test-fixture-v1", status: "verified" } } });
49
+ const digest = addonManifestSha256(manifest); const base = `https://example.test/membrane/addons/mac/sha256/${digest}/`; const bytes = new Map([[`${base}addon-manifest.json`, Buffer.from(canonicalAddonManifest(manifest))], ...manifest.files.map((file) => [base + encodeURIComponent(file.name), readFileSync(path.join(root, config.targets.mac.files.find((entry) => entry.role === file.role).source))])]);
50
+ const destination = path.join(root, "remote"); mkdirSync(destination);
51
+ await downloadImmutableAddOn(`${base}addon-manifest.json`, destination, { fetchImpl: async (url) => ({ ok: bytes.has(url), status: bytes.has(url) ? 200 : 404, arrayBuffer: async () => bytes.get(url) }) });
52
+ assert.equal(readFileSync(path.join(destination, "crypt-service"), "utf8"), "service");
53
+ assert.equal(immutableAddonManifestUrl(manifest, `https://github.com/Orthic-Labs/Membrane/releases/download/addon-membrane-v0.1.0-mac-sha256-${digest}/addon-manifest.json`, digest), true);
54
+ });
@@ -0,0 +1,110 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
3
+ import path from "node:path";
4
+
5
+ const SAFE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
6
+ const ROLES = new Set(["command", "service", "icon", "license", "eula", "privacy", "third-party-notices"]);
7
+ const LEGAL_ROLES = new Set(["license", "eula", "privacy", "third-party-notices"]);
8
+ const PLATFORM = new Set(["mac", "win"]);
9
+ const GITHUB_REPOSITORY = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})\/[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/;
10
+
11
+ export function validateAddonConfig(config, { root, platform, allowMissingExecutables = false }) {
12
+ if (!PLATFORM.has(platform)) throw new Error("add-on platform must be mac or win");
13
+ if (!config || config.schema !== 1 || config.kind !== "headless-addon") throw new Error("add-on config must use schema 1 headless-addon");
14
+ for (const key of ["addon", "version", "packageManager"]) if (typeof config[key] !== "string" || !SAFE_NAME.test(config[key])) throw new Error(`unsafe add-on ${key}`);
15
+ if (!Array.isArray(config.checks)) throw new Error("add-on checks must be an array");
16
+ if (config.distribution && (config.distribution.provider !== "github-releases" || !GITHUB_REPOSITORY.test(config.distribution.repository ?? ""))) throw new Error("add-on distribution must name a GitHub Releases repository");
17
+ validateBuildInputs(config.buildInputs);
18
+ const target = config.targets?.[platform];
19
+ if (!target?.targetTriple || !target?.build?.cmd || !Array.isArray(target.build.args)) throw new Error(`add-on ${platform} target requires targetTriple and build command`);
20
+ if (!target.signing?.contract) throw new Error(`add-on ${platform} target requires signing contract`);
21
+ if (platform === "mac" && target.signing.contract === "apple-developer-id-executable-v1" && !/^[A-Z0-9]{10}$/.test(target.signing.teamId ?? "")) throw new Error("mac add-on requires Developer ID teamId");
22
+ const files = resolveAddonFiles(config, root, platform);
23
+ const seenRoles = new Set();
24
+ const seenNames = new Set();
25
+ for (const file of files) {
26
+ if (!ROLES.has(file.role) || seenRoles.has(file.role)) throw new Error(`duplicate or unsafe add-on role: ${file.role}`);
27
+ if (!SAFE_NAME.test(file.name) || file.name.includes("..")) throw new Error(`unsafe add-on file name: ${file.name}`);
28
+ if (path.isAbsolute(file.source) || file.source.split(/[\\/]/).includes("..")) throw new Error(`unsafe add-on source: ${file.source}`);
29
+ if (seenNames.has(file.name)) throw new Error(`duplicate add-on file name: ${file.name}`);
30
+ if (!existsSync(path.resolve(root, file.source)) && !(allowMissingExecutables && file.executable === true)) throw new Error(`missing add-on file: ${file.source}`);
31
+ seenRoles.add(file.role); seenNames.add(file.name);
32
+ }
33
+ for (const role of ["command", "service", "icon", ...LEGAL_ROLES]) if (!seenRoles.has(role)) throw new Error(`missing required add-on role: ${role}`);
34
+ return { target, files };
35
+ }
36
+
37
+ export function resolveAddonFiles(config, root, platform) {
38
+ const configured = config.targets?.[platform]?.files ?? config.files;
39
+ if (!Array.isArray(configured)) throw new Error("add-on config requires files with role, name, and source");
40
+ return configured.map((file) => ({ ...file, executable: file.executable === true, source: String(file.source ?? "") }));
41
+ }
42
+
43
+ export function createAddonManifest({ config, root, platform, commit, signedAt = new Date().toISOString(), signing = {} }) {
44
+ const { target, files } = validateAddonConfig(config, { root, platform });
45
+ if (!/^[0-9a-f]{40}$/i.test(commit)) throw new Error("add-on commit must be a full Git SHA");
46
+ const manifestFiles = files.map((file) => {
47
+ const source = path.resolve(root, file.source);
48
+ const executable = file.executable === true;
49
+ if ((file.role === "command" || file.role === "service") && !executable) throw new Error(`executable role must declare executable: ${file.role}`);
50
+ const signature = signing[file.role] ?? (executable ? { contract: target.signing.contract, status: "verified" } : null);
51
+ if (executable && (!signature || signature.status !== "verified")) throw new Error(`unsigned executable role: ${file.role}`);
52
+ return { role: file.role, name: file.name, sha256: sha256(source), sizeBytes: statSync(source).size, executable, signing: signature };
53
+ });
54
+ const manifest = { schema: 1, kind: "headless-addon", addon: config.addon, version: config.version, commit, platform, targetTriple: target.targetTriple, consumer: config.consumer, files: manifestFiles, signedAt };
55
+ validateAddonManifest(manifest);
56
+ return manifest;
57
+ }
58
+
59
+ export function validateAddonManifest(manifest) {
60
+ const expected = ["schema", "kind", "addon", "version", "commit", "platform", "targetTriple", "consumer", "files", "signedAt"];
61
+ assertExactKeys(manifest, expected, "add-on manifest");
62
+ if (manifest.schema !== 1 || manifest.kind !== "headless-addon" || !SAFE_NAME.test(manifest.addon) || !SAFE_NAME.test(manifest.version)) throw new Error("invalid add-on manifest identity");
63
+ if (!PLATFORM.has(manifest.platform) || !/^[0-9a-f]{40}$/i.test(manifest.commit) || !Array.isArray(manifest.files)) throw new Error("invalid add-on manifest platform or files");
64
+ const roles = new Set(); const names = new Set();
65
+ for (const file of manifest.files) {
66
+ assertExactKeys(file, ["role", "name", "sha256", "sizeBytes", "executable", "signing"], "add-on file");
67
+ if (!ROLES.has(file.role) || roles.has(file.role) || !SAFE_NAME.test(file.name) || names.has(file.name)) throw new Error("duplicate or unsafe add-on manifest file");
68
+ if (!/^[0-9a-f]{64}$/i.test(file.sha256) || !Number.isSafeInteger(file.sizeBytes) || file.sizeBytes < 0 || typeof file.executable !== "boolean") throw new Error("invalid add-on file integrity");
69
+ if (file.executable && (!file.signing || file.signing.status !== "verified")) throw new Error(`unsigned executable role: ${file.role}`);
70
+ roles.add(file.role); names.add(file.name);
71
+ }
72
+ for (const role of ["command", "service", "icon", ...LEGAL_ROLES]) if (!roles.has(role)) throw new Error(`missing required add-on role: ${role}`);
73
+ return manifest;
74
+ }
75
+
76
+ export function canonicalAddonManifest(manifest) { validateAddonManifest(manifest); return `${JSON.stringify(manifest)}\n`; }
77
+ export function addonManifestSha256(manifest) { return createHash("sha256").update(canonicalAddonManifest(manifest)).digest("hex"); }
78
+ export function addonRoutes(manifest) {
79
+ const digest = addonManifestSha256(manifest);
80
+ const root = `${manifest.addon}/addons/${manifest.platform}/sha256/${digest}`;
81
+ return [...manifest.files.map((file) => ({ name: file.name, key: `${root}/${file.name}`, sha256: file.sha256, manifest: false })), { name: "addon-manifest.json", key: `${root}/addon-manifest.json`, sha256: digest, manifest: true }];
82
+ }
83
+
84
+ export function immutableAddonManifestUrl(manifest, manifestUrl, digest = addonManifestSha256(manifest)) {
85
+ if (/[?#]/.test(manifestUrl)) return false;
86
+ const r2Suffix = `/${manifest.addon}/addons/${manifest.platform}/sha256/${digest}/addon-manifest.json`;
87
+ const githubTag = `addon-${manifest.addon}-v${manifest.version}-${manifest.platform}-sha256-${digest}`;
88
+ const githubSuffix = `/releases/download/${githubTag}/addon-manifest.json`;
89
+ return manifestUrl.endsWith(r2Suffix) || (/^https:\/\/github\.com\//.test(manifestUrl) && manifestUrl.endsWith(githubSuffix));
90
+ }
91
+
92
+ export async function downloadImmutableAddOn(manifestUrl, destination, { fetchImpl = fetch } = {}) {
93
+ if (!/^https:\/\//.test(manifestUrl) || !manifestUrl.endsWith("/addon-manifest.json") || /[?#]/.test(manifestUrl)) throw new Error("add-on lock manifestUrl must be an immutable HTTPS manifest URL");
94
+ const download = async (url, file) => {
95
+ const response = await fetchImpl(url, { cache: "no-store" });
96
+ if (!response.ok) throw new Error(`immutable add-on download failed: ${response.status} ${url}`);
97
+ writeFileSync(file, Buffer.from(await response.arrayBuffer()));
98
+ };
99
+ await download(manifestUrl, path.join(destination, "addon-manifest.json"));
100
+ const manifest = JSON.parse(readFileSync(path.join(destination, "addon-manifest.json"), "utf8"));
101
+ validateAddonManifest(manifest);
102
+ const base = manifestUrl.slice(0, -"addon-manifest.json".length);
103
+ for (const file of manifest.files) await download(`${base}${encodeURIComponent(file.name)}`, path.join(destination, file.name));
104
+ }
105
+
106
+ function validateBuildInputs(inputs) {
107
+ if (!Array.isArray(inputs?.include) || !inputs.include.length || inputs.include.some((entry) => typeof entry !== "string" || path.isAbsolute(entry) || entry.split(/[\\/]/).includes(".."))) throw new Error("add-on buildInputs.include must be safe and non-empty");
108
+ }
109
+ function sha256(file) { return createHash("sha256").update(readFileSync(file)).digest("hex"); }
110
+ function assertExactKeys(value, keys, label) { if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).sort().join("\0") !== [...keys].sort().join("\0")) throw new Error(`${label} has unsupported fields`); }
@@ -0,0 +1,48 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtempSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import test from "node:test";
6
+ import { addonManifestSha256, addonRoutes, createAddonManifest, validateAddonConfig, validateAddonManifest } from "./addon-contract.mjs";
7
+
8
+ function fixture() {
9
+ const root = mkdtempSync(path.join(os.tmpdir(), "right-addon-contract-"));
10
+ mkdirSync(path.join(root, "bin")); mkdirSync(path.join(root, "assets")); mkdirSync(path.join(root, "legal"));
11
+ for (const [file, bytes] of [["bin/membrane", "membrane"], ["bin/crypt-service", "service"], ["assets/tab.png", "icon"], ["legal/LICENSE", "license"], ["legal/EULA.txt", "eula"], ["legal/PRIVACY.md", "privacy"], ["legal/THIRD-PARTY-NOTICES.txt", "notice"]]) writeFileSync(path.join(root, file), bytes);
12
+ const files = [
13
+ ["command", "membrane", "bin/membrane", true], ["service", "crypt-service", "bin/crypt-service", true], ["icon", "membrane-tab-icon.png", "assets/tab.png", false],
14
+ ["license", "LICENSE", "legal/LICENSE", false], ["eula", "EULA.txt", "legal/EULA.txt", false], ["privacy", "PRIVACY.md", "legal/PRIVACY.md", false], ["third-party-notices", "THIRD-PARTY-NOTICES.txt", "legal/THIRD-PARTY-NOTICES.txt", false],
15
+ ].map(([role, name, source, executable]) => ({ role, name, source, executable }));
16
+ return { root, config: { schema: 1, kind: "headless-addon", addon: "membrane", version: "0.1.0", packageManager: "pnpm", checks: ["test"], buildInputs: { include: ["right-addon.config.mjs"] }, consumer: { contract: "orthic-product-v1" }, targets: { mac: { targetTriple: "aarch64-apple-darwin", build: { cmd: "cargo", args: ["build"] }, signing: { contract: "apple-developer-id-executable-v1", teamId: "6KLGD3LLKF" }, files }, win: { targetTriple: "x86_64-pc-windows-msvc", build: { cmd: "cargo", args: ["build"] }, signing: { contract: "azure-artifact-signing-v1" }, files } } } };
17
+ }
18
+
19
+ test("add-on manifest has portable immutable identity and routes", () => {
20
+ const { root, config } = fixture();
21
+ const signing = { command: { contract: "test", status: "verified" }, service: { contract: "test", status: "verified" } };
22
+ const manifest = createAddonManifest({ config, root, platform: "mac", commit: "a".repeat(40), signedAt: "2026-08-11T00:00:00.000Z", signing });
23
+ assert.deepEqual(Object.keys(manifest), ["schema", "kind", "addon", "version", "commit", "platform", "targetTriple", "consumer", "files", "signedAt"]);
24
+ const digest = addonManifestSha256(manifest); const routes = addonRoutes(manifest);
25
+ assert.equal(routes.at(-1).manifest, true);
26
+ assert.match(routes[0].key, new RegExp(`^membrane/addons/mac/sha256/${digest}/`));
27
+ assert.ok(routes.slice(0, -1).every((route) => !route.manifest));
28
+ });
29
+
30
+ test("add-on contract rejects unsafe, incomplete, duplicate, and unsigned inputs", () => {
31
+ const { root, config } = fixture();
32
+ const broken = structuredClone(config); broken.targets.mac.files[0].source = "../escape";
33
+ assert.throws(() => validateAddonConfig(broken, { root, platform: "mac" }), /unsafe add-on source/);
34
+ const duplicate = structuredClone(config); duplicate.targets.mac.files[1].role = "command";
35
+ assert.throws(() => validateAddonConfig(duplicate, { root, platform: "mac" }), /duplicate/);
36
+ const signing = { command: { contract: "test", status: "verified" }, service: { contract: "test", status: "verified" } };
37
+ const manifest = createAddonManifest({ config, root, platform: "mac", commit: "b".repeat(40), signing });
38
+ manifest.files.find((file) => file.role === "command").signing = null;
39
+ assert.throws(() => validateAddonManifest(manifest), /unsigned executable/);
40
+ });
41
+
42
+ test("manifest excludes source and mutable transport fields", () => {
43
+ const { root, config } = fixture();
44
+ const manifest = createAddonManifest({ config, root, platform: "win", commit: "c".repeat(40), signing: { command: { contract: "test", status: "verified" }, service: { contract: "test", status: "verified" } } });
45
+ const text = JSON.stringify(manifest);
46
+ assert.doesNotMatch(text, /source|path|url|token/i);
47
+ assert.equal(readFileSync(path.join(root, "bin", "membrane"), "utf8"), "membrane");
48
+ });
package/build-release.mjs CHANGED
@@ -99,6 +99,23 @@ let cacheLease;
99
99
  let heavySlot;
100
100
  let interrupted;
101
101
 
102
+ // A dry-run is an inspection contract, not a partial build. It must finish
103
+ // before locks, cache directories, receipts, dependency installation, or any
104
+ // child build/sign process can exist.
105
+ if (dryRun) {
106
+ const commit = git(invocationRoot, ["rev-parse", "HEAD"]);
107
+ const dirtyConfig = dirtyBuildInputs(repoRoot, { include: [relativeConfig], required: [relativeConfig] }, commit);
108
+ if (dirtyConfig.length > 0) fail(`dirty release config cannot be executed:\n${dirtyConfig.map((file) => `- ${file}`).join("\n")}`);
109
+ const config = (await import(`${pathToFileURL(configPath).href}?dry-run=${commit}`)).default;
110
+ if (!config?.app || !config?.version) fail("release config must expose app and version");
111
+ const target = config.targets?.[platform];
112
+ if (!target?.package) fail(`${config.app} has no ${platform} package command`);
113
+ console.log(`right-release build dry-run: ${config.app} ${config.version} ${platform}`);
114
+ console.log(`config: ${configPath}`);
115
+ console.log(`commit: ${commit}`);
116
+ process.exit(0);
117
+ }
118
+
102
119
  for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
103
120
  process.once(signal, () => {
104
121
  if (child?.pid) killTree(child.pid);
@@ -1,10 +1,24 @@
1
1
  import assert from "node:assert/strict";
2
- import { readFileSync } from "node:fs";
2
+ import { mkdtempSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
3
+ import os from "node:os";
3
4
  import path from "node:path";
5
+ import { execFileSync, spawnSync } from "node:child_process";
4
6
  import test from "node:test";
5
7
  import { fileURLToPath } from "node:url";
6
8
 
7
9
  const source = readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), "build-release.mjs"), "utf8");
10
+ const build = path.join(path.dirname(fileURLToPath(import.meta.url)), "build-release.mjs");
11
+
12
+ test("general build dry-run is filesystem-pure and never starts its package command", () => {
13
+ const root = mkdtempSync(path.join(os.tmpdir(), "right-build-dry-run-"));
14
+ writeFileSync(path.join(root, "right-release.config.mjs"), "export default { app: 'fixture', version: '1.0.0', targets: { mac: { package: { cmd: 'false' } } } };\n");
15
+ for (const args of [["init", "--initial-branch", "main"], ["config", "user.email", "test@example.com"], ["config", "user.name", "Test"], ["add", "."], ["commit", "-m", "init"]]) execFileSync("git", args, { cwd: root });
16
+ const before = readdirSync(root).sort();
17
+ const result = spawnSync(process.execPath, [build, "--config", "right-release.config.mjs", "--platform", "mac", "--dry-run"], { cwd: root, encoding: "utf8" });
18
+ assert.equal(result.status, 0, result.stderr);
19
+ assert.match(result.stdout, /build dry-run/);
20
+ assert.deepEqual(readdirSync(root).sort(), before);
21
+ });
8
22
 
9
23
  test("macOS and Windows builds default to Cache V2 while explicit legacy mode remains available", () => {
10
24
  assert.match(source, /assertCleanSource/);
@@ -85,6 +85,15 @@ function readCargoManifestDependencies(manifestPath, cargoHome, label) {
85
85
  windowsHide: true,
86
86
  },
87
87
  );
88
+ if (result.error) {
89
+ // spawnSync failed to launch the process at all (no exit code, no stderr) -
90
+ // a missing/unspawnable binary, not cargo rejecting the manifest. Reporting
91
+ // this as a version-contract violation sent a real diagnosis (a Windows
92
+ // rustup-proxy binary that can't be spawned from a non-console session)
93
+ // down the wrong path: it read as "fix your Cargo.toml" instead of "cargo
94
+ // could not be launched here".
95
+ throw new Error(`${label} could not launch ${cargoExecutable()} to read Cargo metadata: ${result.error.message}`);
96
+ }
88
97
  if (result.status !== 0) {
89
98
  throw new Error(`${label} Cargo metadata rejected manifest; RightKit dependencies must use exact crates.io versions: ${String(result.stderr ?? "").trim()}`);
90
99
  }
@@ -61,6 +61,8 @@ if (first === "--version" || first === "-v") {
61
61
  process.exit(2);
62
62
  }
63
63
  run("model-promote.mjs", rest.slice(1));
64
+ } else if (first === "addon") {
65
+ run("addon-command.mjs", args.slice(1));
64
66
  } else if (first === "lsclean") {
65
67
  runBinary("bash", [path.join(packageRoot, "lsclean.sh"), ...args.slice(1)]);
66
68
  } else if (first === "generate-dmg-background") {
@@ -133,6 +135,7 @@ Commands:
133
135
  publish swift [--scope <s>] [--url <u>] [--dry-run] Test, archive, scan, and publish RightKitSwift to a Swift registry
134
136
  model promote --authority heardright --config <file> [--dry-run]
135
137
  Sign and promote one runtime/model artifact
138
+ addon doctor|build|upload|adopt <options> Manage a signed headless add-on
136
139
  doctor [--platform mac|win] Inspect one app's release config
137
140
  doctor --all Verify all Right Suite app release contracts
138
141
  suite-doctor Verify all local Right Suite repositories
@@ -1,10 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import { createHash } from "node:crypto";
3
- import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
4
4
  import path from "node:path";
5
5
  import { spawnSync } from "node:child_process";
6
- import { fileURLToPath } from "node:url";
6
+ import { fileURLToPath, pathToFileURL } from "node:url";
7
7
  import { verifySealedRelease } from "./release-state.mjs";
8
+ import { addonManifestSha256, validateAddonConfig, validateAddonManifest } from "./addon-contract.mjs";
8
9
 
9
10
  const REPO_RE = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})\/[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/;
10
11
  const RELEASE_RE = /^[A-Za-z0-9][A-Za-z0-9._+-]{0,159}$/;
@@ -37,24 +38,69 @@ export function prepareGitHubRelease({ repoRoot, releaseId, platform, repo, stat
37
38
  `Build commit: \`${sealed.manifest.commit}\``,
38
39
  `SHA-256: \`${installers[0].sha256}\``,
39
40
  ].join("\n"),
41
+ notesFile: path.join(releaseState, "release-notes.md"),
42
+ };
43
+ }
44
+
45
+ export function prepareGitHubAddonRelease({ repoRoot, config, configRoot = repoRoot, platform, repo }) {
46
+ if (platform !== "mac" && platform !== "win") throw new Error("platform must be mac or win");
47
+ if (!REPO_RE.test(repo)) throw new Error(`invalid GitHub repository: ${repo}`);
48
+ validateAddonConfig(config, { root: configRoot, platform, allowMissingExecutables: true });
49
+ const commit = runCommand("git", ["rev-parse", "HEAD"], { cwd: repoRoot });
50
+ const sealedDir = path.join(repoRoot, ".right-release", "addons", config.addon, config.version, commit.slice(0, 8), platform);
51
+ const manifestPath = path.join(sealedDir, "addon-manifest.json");
52
+ if (!existsSync(manifestPath)) throw new Error(`sealed add-on manifest not found: ${manifestPath}`);
53
+ const manifest = validateAddonManifest(JSON.parse(readFileSync(manifestPath, "utf8")));
54
+ if (manifest.addon !== config.addon || manifest.version !== config.version || manifest.platform !== platform || manifest.commit !== commit) throw new Error("sealed add-on identity mismatch");
55
+ const assets = manifest.files.map((file) => {
56
+ const asset = path.join(sealedDir, file.name);
57
+ if (!existsSync(asset) || statSync(asset).size !== file.sizeBytes || sha256(asset) !== file.sha256) throw new Error(`sealed add-on file mismatch: ${file.name}`);
58
+ return asset;
59
+ });
60
+ const digest = addonManifestSha256(manifest);
61
+ const tag = `addon-${manifest.addon}-v${manifest.version}-${platform}-sha256-${digest}`;
62
+ const stateDir = path.join(repoRoot, ".right-release", "state", "addons", manifest.addon, manifest.version, commit.slice(0, 8), platform, "github");
63
+ mkdirSync(stateDir, { recursive: true });
64
+ return {
65
+ kind: "addon", manifest, manifestPath, sealedDir, assets: [...assets, manifestPath], tag,
66
+ title: `${manifest.addon} ${manifest.version} ${platform} add-on`,
67
+ notes: [`Signed ${platform === "mac" ? "macOS" : "Windows"} add-on component set.`, "", `Build commit: \`${manifest.commit}\``, `Manifest SHA-256: \`${digest}\``].join("\n"),
68
+ notesFile: path.join(stateDir, "release-notes.md"),
69
+ manifestUrl: `https://github.com/${repo}/releases/download/${tag}/addon-manifest.json`,
40
70
  };
41
71
  }
42
72
 
43
73
  export function publishGitHubRelease(plan, { repo, dryRun = false, run = runCommand } = {}) {
44
74
  const visibility = JSON.parse(run("gh", ["repo", "view", repo, "--json", "visibility"]));
45
75
  if (visibility.visibility !== "PUBLIC") throw new Error(`GitHub releases require a public repository: ${repo}`);
46
- verifyPlatformTrust(plan);
76
+ if (plan.kind === "addon") verifyAddonTrust(plan);
77
+ else verifyPlatformTrust(plan);
47
78
  const existing = run("gh", ["release", "view", plan.tag, "--repo", repo, "--json", "tagName"], { allowFailure: true });
48
- if (dryRun) return { status: existing.ok ? "would-update" : "would-create", tag: plan.tag, assets: plan.assets };
79
+ if (dryRun) return { status: existing.ok ? plan.kind === "addon" ? "would-verify" : "would-update" : "would-create", tag: plan.tag, assets: plan.assets };
80
+ if (plan.kind === "addon" && existing.ok) {
81
+ for (const asset of plan.assets) verifyRemoteAsset(plan.tag, repo, asset, run);
82
+ return { status: "already-verified", tag: plan.tag, assets: plan.assets, manifestUrl: plan.manifestUrl };
83
+ }
49
84
  if (!existing.ok) {
50
- const notesFile = `${plan.assets[1]}.notes.md`;
85
+ const notesFile = plan.notesFile;
51
86
  writeFileSync(notesFile, `${plan.notes}\n`);
52
87
  run("gh", ["release", "create", plan.tag, "--repo", repo, "--title", plan.title, "--notes-file", notesFile]);
53
88
  rmSync(notesFile, { force: true });
54
89
  }
55
90
  run("gh", ["release", "upload", plan.tag, ...plan.assets, "--repo", repo, "--clobber"]);
56
91
  for (const asset of plan.assets) verifyRemoteAsset(plan.tag, repo, asset, run);
57
- return { status: "verified", tag: plan.tag, assets: plan.assets };
92
+ return { status: "verified", tag: plan.tag, assets: plan.assets, manifestUrl: plan.manifestUrl };
93
+ }
94
+
95
+ function verifyAddonTrust(plan) {
96
+ for (const file of plan.manifest.files.filter((entry) => entry.executable)) {
97
+ const executable = path.join(plan.sealedDir, file.name);
98
+ if (plan.manifest.platform === "mac") {
99
+ const output = runCombined("codesign", ["-dv", "--verbose=4", executable]);
100
+ if (!/TeamIdentifier=6KLGD3LLKF/.test(output)) throw new Error(`codesign team mismatch: ${file.name}`);
101
+ runCommand("codesign", ["--verify", "--strict", "--verbose=2", executable]);
102
+ } else runCommand(process.execPath, [path.join(path.dirname(fileURLToPath(import.meta.url)), "sign-windows.mjs"), "--verify-only", executable]);
103
+ }
58
104
  }
59
105
 
60
106
  function verifyPlatformTrust(plan) {
@@ -77,8 +123,8 @@ function sha256(file) {
77
123
  return createHash("sha256").update(readFileSync(file)).digest("hex");
78
124
  }
79
125
 
80
- function runCommand(command, args, { allowFailure = false } = {}) {
81
- const result = spawnSync(command, args, { encoding: "utf8", windowsHide: true });
126
+ function runCommand(command, args, { allowFailure = false, cwd } = {}) {
127
+ const result = spawnSync(command, args, { cwd, encoding: "utf8", windowsHide: true });
82
128
  const output = result.stdout?.trim() ?? "";
83
129
  if (result.status !== 0) {
84
130
  if (allowFailure) return { ok: false, output, error: result.stderr?.trim() ?? "" };
@@ -87,25 +133,43 @@ function runCommand(command, args, { allowFailure = false } = {}) {
87
133
  return allowFailure ? { ok: true, output } : output;
88
134
  }
89
135
 
136
+ function runCombined(command, args) {
137
+ const result = spawnSync(command, args, { encoding: "utf8", windowsHide: true });
138
+ if (result.status !== 0) throw new Error(`${command} ${args.join(" ")} failed: ${result.stderr?.trim() || `exit ${result.status}`}`);
139
+ return `${result.stdout ?? ""}${result.stderr ?? ""}`;
140
+ }
141
+
90
142
  function usage(code) {
91
- console.log("usage: right-release github --release <sealed-id> --platform mac|win --repo owner/repo [--dry-run]");
143
+ console.log("usage: right-release github (--release <sealed-id> | --addon <config>) --platform mac|win [--repo owner/repo] [--dry-run]");
92
144
  process.exit(code);
93
145
  }
94
146
 
95
147
  if (process.argv[1] === fileURLToPath(import.meta.url)) {
96
148
  const args = process.argv.slice(2);
97
- const options = { platform: process.platform === "win32" ? "win" : "mac", releaseId: "", repo: "", dryRun: false };
149
+ const options = { platform: process.platform === "win32" ? "win" : "mac", releaseId: "", addon: "", repo: "", dryRun: false };
98
150
  for (let index = 0; index < args.length; index += 1) {
99
151
  if (args[index] === "--platform") options.platform = args[++index];
100
152
  else if (args[index] === "--release") options.releaseId = args[++index];
153
+ else if (args[index] === "--addon") options.addon = args[++index];
101
154
  else if (args[index] === "--repo") options.repo = args[++index];
102
155
  else if (args[index] === "--dry-run") options.dryRun = true;
103
156
  else if (args[index] === "-h" || args[index] === "--help") usage(0);
104
157
  else throw new Error(`unknown argument: ${args[index]}`);
105
158
  }
106
- if (!options.releaseId || !options.repo) usage(2);
159
+ if (Boolean(options.releaseId) === Boolean(options.addon)) usage(2);
107
160
  const repoRoot = runCommand("git", ["rev-parse", "--show-toplevel"]);
108
- const plan = prepareGitHubRelease({ repoRoot, ...options });
161
+ let plan;
162
+ if (options.addon) {
163
+ const configPath = path.resolve(options.addon);
164
+ const config = (await import(`${pathToFileURL(configPath).href}?github=${Date.now()}`)).default;
165
+ options.repo ||= config.distribution?.repository ?? "";
166
+ if (config.distribution?.provider !== "github-releases") throw new Error("add-on config must select github-releases distribution");
167
+ plan = prepareGitHubAddonRelease({ repoRoot, config, configRoot: path.dirname(configPath), ...options });
168
+ } else {
169
+ if (!options.repo) usage(2);
170
+ plan = prepareGitHubRelease({ repoRoot, ...options });
171
+ }
109
172
  const result = publishGitHubRelease(plan, options);
110
173
  console.log(`right-release github: ${result.status} ${result.tag}`);
174
+ if (result.manifestUrl) console.log(`manifestUrl: ${result.manifestUrl}`);
111
175
  }
@@ -4,7 +4,9 @@ import { mkdtempSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
4
  import os from "node:os";
5
5
  import path from "node:path";
6
6
  import test from "node:test";
7
- import { prepareGitHubRelease, publishGitHubRelease } from "./github-release.mjs";
7
+ import { execFileSync } from "node:child_process";
8
+ import { canonicalAddonManifest, createAddonManifest } from "./addon-contract.mjs";
9
+ import { prepareGitHubAddonRelease, prepareGitHubRelease, publishGitHubRelease } from "./github-release.mjs";
8
10
 
9
11
  const sha256 = (value) => createHash("sha256").update(value).digest("hex");
10
12
 
@@ -22,6 +24,24 @@ function fixture() {
22
24
  return { root, releaseId };
23
25
  }
24
26
 
27
+ function addonFixture() {
28
+ const root = mkdtempSync(path.join(os.tmpdir(), "right-release-github-addon-"));
29
+ mkdirSync(path.join(root, "out"));
30
+ for (const [name, bytes] of [["membrane", "command"], ["crypt-service", "service"], ["icon.png", "icon"], ["LICENSE", "license"], ["EULA.txt", "eula"], ["PRIVACY.md", "privacy"], ["THIRD-PARTY-NOTICES.txt", "notices"]]) writeFileSync(path.join(root, "out", name), bytes);
31
+ execFileSync("git", ["init", "--initial-branch", "main"], { cwd: root });
32
+ execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: root });
33
+ execFileSync("git", ["config", "user.name", "Test"], { cwd: root });
34
+ writeFileSync(path.join(root, "tracked"), "source"); execFileSync("git", ["add", "."], { cwd: root }); execFileSync("git", ["commit", "-m", "source"], { cwd: root });
35
+ const files = [["command", "membrane", true], ["service", "crypt-service", true], ["icon", "icon.png", false], ["license", "LICENSE", false], ["eula", "EULA.txt", false], ["privacy", "PRIVACY.md", false], ["third-party-notices", "THIRD-PARTY-NOTICES.txt", false]].map(([role, name, executable]) => ({ role, name, source: `out/${name}`, executable }));
36
+ const config = { schema: 1, kind: "headless-addon", addon: "membrane", version: "0.1.0", packageManager: "pnpm", distribution: { provider: "github-releases", repository: "Orthic-Labs/Membrane" }, checks: [], buildInputs: { include: ["tracked"] }, consumer: { contract: "orthic-product-v1" }, targets: { win: { targetTriple: "x86_64-pc-windows-msvc", build: { cmd: "false", args: [] }, signing: { contract: "azure-artifact-signing-v1" }, files } } };
37
+ const commit = execFileSync("git", ["rev-parse", "HEAD"], { cwd: root, encoding: "utf8" }).trim();
38
+ const manifest = createAddonManifest({ config, root, platform: "win", commit, signing: { command: { contract: "test-fixture-v1", status: "verified" }, service: { contract: "test-fixture-v1", status: "verified" } } });
39
+ const sealed = path.join(root, ".right-release", "addons", "membrane", "0.1.0", commit.slice(0, 8), "win"); mkdirSync(sealed, { recursive: true });
40
+ for (const file of manifest.files) writeFileSync(path.join(sealed, file.name), readFileSync(path.join(root, "out", file.name)));
41
+ writeFileSync(path.join(sealed, "addon-manifest.json"), canonicalAddonManifest(manifest));
42
+ return { root, config };
43
+ }
44
+
25
45
  test("GitHub plan derives tag, notes, installer, manifest & checksums only from sealed bytes", () => {
26
46
  const fx = fixture();
27
47
  const plan = prepareGitHubRelease({ repoRoot: fx.root, releaseId: fx.releaseId, platform: "win", repo: "Orthic-Labs/CutRight" });
@@ -44,3 +64,26 @@ test("dry-run checks public visibility without creating or uploading a release",
44
64
  assert.equal(publishGitHubRelease(plan, { repo: "Orthic-Labs/CutRight", dryRun: true, run }).status, "would-create");
45
65
  assert.equal(calls.length, 2);
46
66
  });
67
+
68
+ test("GitHub add-on plan uses a content-addressed platform tag and manifest URL", () => {
69
+ const fx = addonFixture();
70
+ const plan = prepareGitHubAddonRelease({ repoRoot: fx.root, config: fx.config, platform: "win", repo: "Orthic-Labs/Membrane" });
71
+ assert.match(plan.tag, /^addon-membrane-v0\.1\.0-win-sha256-[0-9a-f]{64}$/);
72
+ assert.equal(plan.assets.at(-1), plan.manifestPath);
73
+ assert.equal(plan.manifestUrl, `https://github.com/Orthic-Labs/Membrane/releases/download/${plan.tag}/addon-manifest.json`);
74
+ });
75
+
76
+ test("existing GitHub add-on release is verified without upload mutation", () => {
77
+ const asset = path.join(mkdtempSync(path.join(os.tmpdir(), "right-release-existing-")), "addon-manifest.json"); writeFileSync(asset, "sealed");
78
+ const calls = [];
79
+ const run = (_command, args, options = {}) => {
80
+ calls.push(args);
81
+ if (args[0] === "repo") return JSON.stringify({ visibility: "PUBLIC" });
82
+ if (args[0] === "release" && args[1] === "view") return options.allowFailure ? { ok: true } : "";
83
+ if (args[0] === "release" && args[1] === "download") { mkdirSync(args[args.indexOf("--dir") + 1], { recursive: true }); writeFileSync(path.join(args[args.indexOf("--dir") + 1], path.basename(asset)), "sealed"); return ""; }
84
+ throw new Error(`unexpected mutation: ${args.join(" ")}`);
85
+ };
86
+ const result = publishGitHubRelease({ kind: "addon", manifest: { files: [] }, sealedDir: path.dirname(asset), tag: "immutable", assets: [asset], manifestUrl: "https://example.test/manifest" }, { repo: "Orthic-Labs/Membrane", run });
87
+ assert.equal(result.status, "already-verified");
88
+ assert.equal(calls.some((args) => args[0] === "release" && args[1] === "upload"), false);
89
+ });
package/package.json CHANGED
@@ -1,32 +1,31 @@
1
- {
2
- "name": "@rightkit/release",
3
- "version": "0.2.57",
4
- "description": "Portable Right Suite release CLI/SDK: signed installers, updater artifacts, patch/update routing, hardening, R2 publish, and RightApps registration.",
5
- "type": "module",
6
- "bin": {
7
- "right-release": "cli/right-release.mjs"
8
- },
9
- "files": [
10
- "cli",
11
- "*.mjs",
12
- "*.json",
13
- "*.sh",
14
- "*.py"
15
- ],
16
- "sideEffects": false,
17
- "scripts": {
18
- "test": "node --test *.test.mjs",
19
- "doctor:all": "node --test right-suite-contract.test.mjs",
20
- "verify:standalone": "node standalone-clone-verify.mjs"
21
- },
22
- "publishConfig": {
23
- "registry": "https://registry.npmjs.org/",
24
- "access": "public"
25
- },
26
- "repository": {
27
- "type": "git",
28
- "url": "git+https://github.com/adrdsouza/claude.git",
29
- "directory": "tools/rightkit/packages/release"
30
- },
31
- "packageManager": "pnpm@11.18.0"
32
- }
1
+ {
2
+ "name": "@rightkit/release",
3
+ "version": "0.2.59",
4
+ "description": "Portable Right Suite release CLI/SDK: signed installers, updater artifacts, patch/update routing, hardening, R2 publish, and RightApps registration.",
5
+ "type": "module",
6
+ "bin": {
7
+ "right-release": "cli/right-release.mjs"
8
+ },
9
+ "files": [
10
+ "cli",
11
+ "*.mjs",
12
+ "*.json",
13
+ "*.sh",
14
+ "*.py"
15
+ ],
16
+ "sideEffects": false,
17
+ "publishConfig": {
18
+ "registry": "https://registry.npmjs.org/",
19
+ "access": "public"
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/adrdsouza/claude.git",
24
+ "directory": "tools/rightkit/packages/release"
25
+ },
26
+ "scripts": {
27
+ "test": "node --test *.test.mjs",
28
+ "doctor:all": "node --test right-suite-contract.test.mjs",
29
+ "verify:standalone": "node standalone-clone-verify.mjs"
30
+ }
31
+ }
package/release-state.mjs CHANGED
@@ -93,6 +93,7 @@ export function verifySealedRelease(sealedDir) {
93
93
  if (!existsSync(manifestPath)) throw new Error(`sealed manifest missing: ${manifestPath}`);
94
94
  const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
95
95
  if (manifest.schema !== 1) throw new Error(`unsupported sealed manifest schema: ${manifest.schema}`);
96
+ if (manifest.kind != null && typeof manifest.kind !== "string") throw new Error("sealed manifest kind must be a string when present");
96
97
  if (!manifest.checkpoints?.includes("sealed")) throw new Error("release is not sealed");
97
98
  for (const item of manifest.files ?? []) {
98
99
  const file = path.join(sealedDir, item.name);
@@ -464,7 +464,7 @@ test("RightKit exposes one current version manifest", () => {
464
464
  "@rightkit/legal": "0.3.0",
465
465
  "@rightkit/legal-ui": "0.1.1",
466
466
  "@rightkit/license": "0.1.6",
467
- "@rightkit/release": "0.2.57",
467
+ "@rightkit/release": "0.2.59",
468
468
  "@rightkit/qa": "0.2.0",
469
469
  });
470
470
  assert.deepEqual(versions.legacyNpm, {
@@ -1,67 +1,67 @@
1
- {
2
- "schema": 2,
3
- "packageManager": "pnpm@11.18.0",
4
- "npm": {
5
- "@rightkit/git": "0.2.0",
6
- "@rightkit/legal": "0.2.0",
7
- "@rightkit/license": "0.1.5",
8
- "@rightkit/logs": "0.1.3",
9
- "@rightkit/platform-ui": "0.1.0",
10
- "@rightkit/qa": "0.1.0",
11
- "@rightkit/release": "0.2.47",
12
- "@rightkit/tauri": "0.1.0",
13
- "@rightkit/updates": "0.2.3"
14
- },
15
- "stagedNpm": {
16
- "@rightkit/ax": "0.2.0",
17
- "@rightkit/git": "0.2.0",
18
- "@rightkit/legal": "0.3.0",
19
- "@rightkit/legal-ui": "0.1.1",
20
- "@rightkit/license": "0.1.6",
21
- "@rightkit/release": "0.2.57",
22
- "@rightkit/qa": "0.2.0"
23
- },
24
- "legacyNpm": {
25
- "@rightkit/legal-ui": [
26
- "0.1.0"
27
- ],
28
- "@rightkit/release": [
29
- "0.2.22",
30
- "0.2.29",
31
- "0.2.30",
32
- "0.2.31",
33
- "0.2.41",
34
- "0.2.42",
35
- "0.2.43",
36
- "0.2.44",
37
- "0.2.45",
38
- "0.2.46",
39
- "0.2.49",
40
- "0.2.50",
41
- "0.2.51",
42
- "0.2.53",
43
- "0.2.54",
44
- "0.2.55",
45
- "0.2.56"
46
- ],
47
- "@rightkit/qa": [
48
- "0.1.0"
49
- ]
50
- },
51
- "cargo": {
52
- "rightkit-license": "0.1.2",
53
- "rightkit-logs": "0.1.0",
54
- "rightkit-process": "0.1.0",
55
- "rightkit-tauri": "0.1.0"
56
- },
57
- "stagedCargo": {
58
- "rightkit-license": "0.1.3",
59
- "rightkit-tauri": "0.1.1"
60
- },
61
- "swift": {
62
- "rightkit-swift": {
63
- "url": "https://github.com/bogusyogi/rightkit-swift.git",
64
- "version": "0.1.0"
65
- }
66
- }
67
- }
1
+ {
2
+ "schema": 2,
3
+ "packageManager": "pnpm@11.18.0",
4
+ "npm": {
5
+ "@rightkit/git": "0.2.0",
6
+ "@rightkit/legal": "0.2.0",
7
+ "@rightkit/license": "0.1.5",
8
+ "@rightkit/logs": "0.1.3",
9
+ "@rightkit/platform-ui": "0.1.0",
10
+ "@rightkit/qa": "0.1.0",
11
+ "@rightkit/release": "0.2.47",
12
+ "@rightkit/tauri": "0.1.0",
13
+ "@rightkit/updates": "0.2.3"
14
+ },
15
+ "stagedNpm": {
16
+ "@rightkit/ax": "0.2.0",
17
+ "@rightkit/git": "0.2.0",
18
+ "@rightkit/legal": "0.3.0",
19
+ "@rightkit/legal-ui": "0.1.1",
20
+ "@rightkit/license": "0.1.6",
21
+ "@rightkit/release": "0.2.59",
22
+ "@rightkit/qa": "0.2.0"
23
+ },
24
+ "legacyNpm": {
25
+ "@rightkit/legal-ui": [
26
+ "0.1.0"
27
+ ],
28
+ "@rightkit/release": [
29
+ "0.2.22",
30
+ "0.2.29",
31
+ "0.2.30",
32
+ "0.2.31",
33
+ "0.2.41",
34
+ "0.2.42",
35
+ "0.2.43",
36
+ "0.2.44",
37
+ "0.2.45",
38
+ "0.2.46",
39
+ "0.2.49",
40
+ "0.2.50",
41
+ "0.2.51",
42
+ "0.2.53",
43
+ "0.2.54",
44
+ "0.2.55",
45
+ "0.2.56"
46
+ ],
47
+ "@rightkit/qa": [
48
+ "0.1.0"
49
+ ]
50
+ },
51
+ "cargo": {
52
+ "rightkit-license": "0.1.2",
53
+ "rightkit-logs": "0.1.0",
54
+ "rightkit-process": "0.1.0",
55
+ "rightkit-tauri": "0.1.0"
56
+ },
57
+ "stagedCargo": {
58
+ "rightkit-license": "0.1.3",
59
+ "rightkit-tauri": "0.1.1"
60
+ },
61
+ "swift": {
62
+ "rightkit-swift": {
63
+ "url": "https://github.com/bogusyogi/rightkit-swift.git",
64
+ "version": "0.1.0"
65
+ }
66
+ }
67
+ }
@@ -1,32 +0,0 @@
1
- {
2
- "schemaVersion": 1,
3
- "generatedAt": "2026-08-10T04:42:36.444Z",
4
- "workRoot": "C:\\Users\\adrds\\AppData\\Local\\Temp\\rightkit-standalone-sBI0iR",
5
- "apps": [
6
- {
7
- "key": "viewright",
8
- "remote": "https://github.com/bogusyogi/viewright.git",
9
- "appDir": ".",
10
- "revision": "21a4171fa8add2d8114bc5f07498b60d7e8eafe5",
11
- "packageManager": "pnpm@11.18.0",
12
- "clone": {
13
- "command": "git clone --depth 1 --single-branch https://github.com/bogusyogi/viewright.git C:\\Users\\adrds\\AppData\\Local\\Temp\\rightkit-standalone-sBI0iR\\viewright",
14
- "status": 0,
15
- "stdout": "",
16
- "stderr": "Cloning into 'C:\\Users\\adrds\\AppData\\Local\\Temp\\rightkit-standalone-sBI0iR\\viewright'...\nUpdating files: 91% (1902/2080)\rUpdating files: 92% (1914/2080)\rUpdating files: 93% (1935/2080)\rUpdating files: 94% (1956/2080)\rUpdating files: 95% (1976/2080)\rUpdating files: 96% (1997/2080)\rUpdating files: 97% (2018/2080)\rUpdating files: 98% (2039/2080)\rUpdating files: 99% (2060/2080)\rUpdating files: 100% (2080/2080)\rUpdating files: 100% (2080/2080), done."
17
- },
18
- "install": {
19
- "command": "C:\\nvm4w\\nodejs\\node.exe C:\\nvm4w\\nodejs\\node_modules\\pnpm\\bin\\pnpm.mjs install --frozen-lockfile",
20
- "status": 0,
21
- "stdout": "✓ Lockfile passes supply-chain policies (verified 8h ago)\nLockfile is up to date, resolution step is skipped\nProgress: resolved 1, reused 0, downloaded 0, added 0\nPackages: +619\n++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\nProgress: resolved 619, reused 0, downloaded 0, added 0\nProgress: resolved 619, reused 60, downloaded 0, added 0\nPackages are hard linked from the content-addressable store to the virtual store.\n Content-addressable store is at: C:\\Users\\adrds\\AppData\\Local\\pnpm\\store\\v11\n Virtual store is at: node_modules/.pnpm\nProgress: resolved 619, reused 573, downloaded 7, added 7\nProgress: resolved 619, reused 574, downloaded 9, added 10\nProgress: resolved 619, reused 574, downloaded 22, added 11\nProgress: resolved 619, reused 574, downloaded 23, added 11\nProgress: resolved 619, reused 574, downloaded 26, added 14\nProgress: resolved 619, reused 574, downloaded 30, added 24\nProgress: resolved 619, reused 574, downloaded 31, added 57\nProgress: resolved 619, reused 574, downloaded 34, added 128\nProgress: resolved 619, reused 574, downloaded 38, added 135\nProgress: resolved 619, reused 574, downloaded 38, added 136\nProgress: resolved 619, reused 574, downloaded 39, added 141\nProgress: resolved 619, reused 574, downloaded 39, added 148\nProgress: resolved 619, reused 574, downloaded 39, added 182\nProgress: resolved 619, reused 574, downloaded 40, added 201\nProgress: resolved 619, reused 574, downloaded 41, added 218\nProgress: resolved 619, reused 574, downloaded 41, added 242\nProgress: resolved 619, reused 574, downloaded 42, added 312\nProgress: resolved 619, reused 574, downloaded 42, added 371\nProgress: resolved 619, reused 574, downloaded 42, added 430\nProgress: resolved 619, reused 574, downloaded 42, added 486\nProgress: resolved 619, reused 574, downloaded 43, added 543\nProgress: resolved 619, reused 574, downloaded 43, added 578\nProgress: resolved 619, reused 574, downloaded 44, added 596\nProgress: resolved 619, reused 574, downloaded 44, added 611\nProgress: resolved 619, reused 574, downloaded 44, added 612\nProgress: resolved 619, reused 574, downloaded 44, added 613\nProgress: resolved 619, reused 574, downloaded 44, added 615\nProgress: resolved 619, reused 574, downloaded 44, added 616\nProgress: resolved 619, reused 574, downloaded 44, added 617\nProgress: resolved 619, reused 574, downloaded 44, added 618\nProgress: resolved 619, reused 574, downloaded 45, added 618\nProgress: resolved 619, reused 574, downloaded 45, added 619\nProgress: resolved 619, reused 574, downloaded 45, added 619, done\n\ndependencies:\n+ @codemirror/autocomplete 6.20.3\n+ @codemirror/commands 6.10.4\n+ @codemirror/lang-html 6.4.11\n+ @codemirror/lang-javascript 6.2.5\n+ @codemirror/lang-markdown 6.5.0\n+ @codemirror/language 6.12.4\n+ @codemirror/lint 6.9.7\n+ @codemirror/search 6.7.1\n+ @codemirror/state 6.7.1\n+ @codemirror/view 6.43.6\n+ @eigenpal/docx-editor-agents @eigenpal/docx-editor-agents@file:vendor/docx-editor/packages/agents(react@19.2.7)\n+ @eigenpal/docx-editor-core @eigenpal/docx-editor-core@file:vendor/docx-editor/packages/core(prosemirror-commands@1.7.1)(prosemirror-dropcursor@1.8.2)(prosemirror-history@1.5.0)(prosemirror-keymap@1.2.3)(prosemirror-model@1.25.10)(prosemirror-state@1.4.4)(prosemirror-tables@1.8.5)(prosemirror-transform@1.12.0)(prosemirror-view@1.42.0)\n+ @eigenpal/docx-editor-i18n @eigenpal/docx-editor-i18n@file:vendor/docx-editor/packages/i18n\n+ @eigenpal/docx-editor-react @eigenpal/docx-editor-react@file:vendor/docx-editor/packages/react(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(prosemirror-commands@1.7.1)(prosemirror-dropcursor@1.8.2)(prosemirror-history@1.5.0)(prosemirror-keymap@1.2.3)(prosemirror-model@1.25.10)(prosemirror-state@1.4.4)(prosemirror-tables@1.8.5)(prosemirror-transform@1.12.0)(prosemirror-view@1.42.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)\n+ @lezer/highlight 1.2.3\n+ @mdx-js/mdx 3.1.1\n+ @phosphor-icons/react 2.1.10\n+ @radix-ui/react-select 2.3.2\n+ @rightkit/legal-ui 0.1.0\n+ @rightkit/license 0.1.6\n+ @rightkit/logs 0.1.3\n+ @rightkit/tauri 0.1.0\n+ @rightkit/updates 0.2.3\n+ @tauri-apps/api 2.11.1\n+ @tauri-apps/plugin-http 2.5.9\n+ @tauri-apps/plugin-process 2.3.1\n+ @tauri-apps/plugin-updater 2.10.1\n+ clsx 2.1.1\n+ docxtemplater 3.69.0\n+ dompurify 3.4.13\n+ fabric 7.4.0\n+ github-slugger 2.0.0\n+ jszip 3.10.1\n+ katex 0.17.0\n+ mermaid 11.16.1\n+ pdfjs-dist 6.2.108\n+ pizzip 3.2.0\n+ prosemirror-commands 1.7.1\n+ prosemirror-dropcursor 1.8.2\n+ prosemirror-history 1.5.0\n+ prosemirror-keymap 1.2.3\n+ prosemirror-model 1.25.10\n+ prosemirror-state 1.4.4\n+ prosemirror-tables 1.8.5\n+ prosemirror-transform 1.12.0\n+ prosemirror-view 1.42.0\n+ react 19.2.7\n+ react-dom 19.2.7\n+ react-image-crop 11.1.2\n+ rehype-stringify 10.0.1\n+ remark-frontmatter 5.0.0\n+ remark-gfm 4.0.1\n+ remark-math 6.0.0\n+ remark-parse 11.0.0\n+ remark-rehype 11.1.2\n+ remark-smartypants 3.0.2\n+ shiki 4.3.1\n+ sonner 2.0.7\n+ sucrase 3.35.1\n+ unified 11.0.5\n+ xml-js 1.6.11\n+ yaml 2.9.0\n\ndevDependencies:\n+ @biomejs/biome 2.5.3\n+ @rightkit/legal 0.3.0\n+ @rightkit/release 0.2.50\n+ @tailwindcss/vite 4.3.2\n+ @tauri-apps/cli 2.11.4\n+ @testing-library/dom 10.4.1\n+ @testing-library/jest-dom 6.9.1\n+ @testing-library/react 16.3.2\n+ @types/mdast 4.0.4\n+ @types/react 19.2.17\n+ @types/react-dom 19.2.3\n+ @types/ws 8.18.1\n+ @vitejs/plugin-react 6.0.3\n+ happy-dom 20.10.6\n+ jscpd 5.0.12\n+ jsdom 29.1.1\n+ knip 6.25.0\n+ tailwindcss 4.3.2\n+ typescript 6.0.3\n+ vite 8.1.4\n+ vitest 4.1.10\n+ ws 8.21.0\n\nDone in 40.8s using pnpm v11.18.0",
22
- "stderr": ""
23
- },
24
- "doctor": {
25
- "command": "C:\\nvm4w\\nodejs\\node.exe C:\\nvm4w\\nodejs\\node_modules\\pnpm\\bin\\pnpm.mjs release:doctor",
26
- "status": 0,
27
- "stdout": "right-release 0.2.50\nconfig: C:\\Users\\adrds\\AppData\\Local\\Temp\\rightkit-standalone-sBI0iR\\viewright\\right-release.config.mjs\napp: viewright\nplatform: win\ntier: <required for release/publish>\npackageManager: pnpm\nworkdir: C:\\Users\\adrds\\AppData\\Local\\Temp\\rightkit-standalone-sBI0iR\\viewright\nhardeningscan: C:\\Users\\adrds\\AppData\\Local\\Temp\\rightkit-standalone-sBI0iR\\viewright\\node_modules\\.pnpm\\@rightkit+release@0.2.50\\node_modules\\@rightkit\\release\\hardeningscan.mjs\nlegal: C:\\Users\\adrds\\AppData\\Local\\Temp\\rightkit-standalone-sBI0iR\\viewright\\legal\\legal-manifest.json\nlegalAcceptance: viewright-2026-07-17-v3\nlegalManifestSha256: 035ea7cbd040ef001dbdc1385f114c8bb126178fea9a9f705016a726f96a7830\nsign: src-tauri/target/release/bundle/nsis/ViewRight_0.1.60_x64-setup.exe\npreflight:\n [ok ] target-bridge: src-tauri/target is ready for the shared cache bridge\n [ok ] version: 0.1.60 is free to build\n [ok ] windows-sdk: makeappx.exe from SDK 10.0.26100.0\n [ok ] signtool: signtool.exe from SDK 10.0.26100.0\n [ok ] sccache: sccache 0.17.0\n [ok ] disk: 628.7GB free",
28
- "stderr": "$ right-release doctor"
29
- }
30
- }
31
- ]
32
- }