@rightkit/release 0.2.20 → 0.2.21

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.
@@ -47,6 +47,13 @@ if (first === "--version" || first === "-v") {
47
47
  } else {
48
48
  run("release.mjs", rest.includes("--upload") || rest.includes("--publish") ? rest : [...rest, "--upload"]);
49
49
  }
50
+ } else if (first === "model") {
51
+ const rest = args.slice(1);
52
+ if (rest[0] !== "promote") {
53
+ console.error("right-release model: expected promote");
54
+ process.exit(2);
55
+ }
56
+ run("model-promote.mjs", rest.slice(1));
50
57
  } else if (first === "lsclean") {
51
58
  runBinary("bash", [path.join(packageRoot, "lsclean.sh"), ...args.slice(1)]);
52
59
  } else if (first === "generate-dmg-background") {
@@ -113,6 +120,8 @@ Commands:
113
120
  release [--platform mac|win] --tier patch|update Build/package through the signed release lane
114
121
  publish [--platform mac|win] --tier patch|update Release plus R2 upload + RightApps registration
115
122
  publish cargo --crate <name> [--dry-run] Test, inspect, scan, and publish one crates.io package
123
+ model promote --authority heardright --config <file> [--dry-run]
124
+ Sign and promote one runtime/model artifact
116
125
  doctor [--platform mac|win] Inspect one app's release config
117
126
  doctor --all Verify all Right Suite app release contracts
118
127
  suite-doctor Verify all local Right Suite repositories
@@ -0,0 +1,262 @@
1
+ #!/usr/bin/env node
2
+ import { createHash } from "node:crypto";
3
+ import { createReadStream } from "node:fs";
4
+ import { mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
5
+ import os from "node:os";
6
+ import path from "node:path";
7
+ import { spawn } from "node:child_process";
8
+ import { fileURLToPath, pathToFileURL } from "node:url";
9
+
10
+ import {
11
+ buildRuntimeArtifactManifest,
12
+ signRuntimeArtifactManifest,
13
+ } from "./runtime-artifact-manifest.mjs";
14
+ import { loadReleaseToken } from "./release-token.mjs";
15
+ import { registerRightAppsRelease } from "./rightapps-register.mjs";
16
+
17
+ export const RUNTIME_ARTIFACT_TRUSTED_KEY_ID = "rightkit-runtime-artifacts-2026-07";
18
+ const TOOL_ROOT = path.dirname(fileURLToPath(import.meta.url));
19
+ const UPLOAD = path.join(TOOL_ROOT, "upload-large.mjs");
20
+
21
+ export async function promoteRuntimeArtifact(input, deps = {}) {
22
+ const metadata = deps.metadata ?? fileMetadata;
23
+ const upload = deps.upload;
24
+ const register = deps.register;
25
+ const { config } = input;
26
+ assertPromotionConfig(config);
27
+ if (input.authority !== "heardright") throw new Error("model promotion authority must be heardright");
28
+ if (input.keyId !== RUNTIME_ARTIFACT_TRUSTED_KEY_ID) throw new Error(`untrusted runtime artifact key id: ${input.keyId}`);
29
+ if (!input.privateKey) throw new Error("runtime artifact signing key is required");
30
+
31
+ const [artifact, evidence] = await Promise.all([
32
+ metadata(input.artifactFile),
33
+ metadata(input.evidenceFile),
34
+ ]);
35
+ assertMetadata(artifact, config.artifact, "artifact");
36
+ if (evidence.sha256 !== config.evidence.sha256) throw new Error("evidence SHA-256 does not match promotion config");
37
+
38
+ const manifest = buildRuntimeArtifactManifest({
39
+ ...config.manifest,
40
+ filename: config.artifact.filename,
41
+ sha256: artifact.sha256,
42
+ sizeBytes: artifact.sizeBytes,
43
+ promotion: {
44
+ ...config.manifest.promotion,
45
+ authorityAppKey: input.authority,
46
+ evidenceSha256: evidence.sha256,
47
+ },
48
+ });
49
+ const envelope = signRuntimeArtifactManifest(manifest, input.privateKey, input.keyId);
50
+ const isR2 = manifest.distribution.delivery !== "bundled";
51
+ const steps = isR2
52
+ ? ["upload immutable object", "register signed envelope", "replace stable pointer"]
53
+ : ["register signed envelope"];
54
+ if (input.dryRun) return { envelope, steps };
55
+ if (typeof register !== "function") throw new Error("RightApps registration boundary is required");
56
+ if (isR2 && typeof upload !== "function") throw new Error("R2 upload boundary is required");
57
+
58
+ if (isR2) {
59
+ await upload({
60
+ purpose: "object",
61
+ file: input.artifactFile,
62
+ key: manifest.object.r2Key,
63
+ lane: manifest.distribution.delivery,
64
+ });
65
+ }
66
+ await register(envelope);
67
+ if (isR2) {
68
+ await upload({
69
+ purpose: "pointer",
70
+ content: `${JSON.stringify(envelope)}\n`,
71
+ key: manifest.pointerKey,
72
+ lane: manifest.distribution.delivery,
73
+ });
74
+ }
75
+ return { envelope, steps };
76
+ }
77
+
78
+ export function defaultWindowsRuntimeArtifactSigningKeyFile(env = process.env) {
79
+ const appData = env.APPDATA || path.join(os.homedir(), "AppData", "Roaming");
80
+ return path.join(appData, "RightKit", "runtime-artifact-signing-key.pem");
81
+ }
82
+
83
+ export async function loadRuntimeArtifactSigningKey({ env = process.env, platform = process.platform, explicitFile } = {}) {
84
+ const direct = env.RIGHTKIT_RUNTIME_ARTIFACT_PRIVATE_KEY;
85
+ if (direct?.trim()) return direct.replace(/\\n/g, "\n");
86
+ const configuredFile = explicitFile || env.RIGHTKIT_RUNTIME_ARTIFACT_PRIVATE_KEY_FILE;
87
+ if (configuredFile) return readNonemptySecret(path.resolve(configuredFile));
88
+ if (platform === "win32") return readNonemptySecret(defaultWindowsRuntimeArtifactSigningKeyFile(env));
89
+ throw new Error("RIGHTKIT_RUNTIME_ARTIFACT_PRIVATE_KEY or RIGHTKIT_RUNTIME_ARTIFACT_PRIVATE_KEY_FILE is required");
90
+ }
91
+
92
+ export function runtimeArtifactUploadEnv(source = process.env) {
93
+ const env = { ...source };
94
+ delete env.RIGHTKIT_RUNTIME_ARTIFACT_PRIVATE_KEY;
95
+ delete env.RIGHTKIT_RUNTIME_ARTIFACT_PRIVATE_KEY_FILE;
96
+ delete env.RIGHTAPPS_RELEASE_TOKEN;
97
+ delete env.RIGHTAPPS_RELEASE_TOKEN_FILE;
98
+ return env;
99
+ }
100
+
101
+ async function main() {
102
+ if (process.argv.slice(2).some((arg) => arg === "--help" || arg === "-h")) {
103
+ printHelp();
104
+ return;
105
+ }
106
+ const args = parseArgs(process.argv.slice(2));
107
+ const configPath = path.resolve(args.config);
108
+ const configRoot = path.dirname(configPath);
109
+ const raw = JSON.parse(await readFile(configPath, "utf8"));
110
+ assertExactKeys(raw, ["artifact", "evidence", "manifest"], "promotion config");
111
+ assertExactKeys(raw.artifact, ["file", "filename", "sha256", "sizeBytes"], "artifact");
112
+ assertExactKeys(raw.evidence, ["file", "sha256"], "evidence");
113
+ const config = {
114
+ artifact: {
115
+ filename: raw.artifact.filename,
116
+ sha256: raw.artifact.sha256,
117
+ sizeBytes: raw.artifact.sizeBytes,
118
+ },
119
+ evidence: { sha256: raw.evidence.sha256 },
120
+ manifest: raw.manifest,
121
+ };
122
+ const privateKey = await loadRuntimeArtifactSigningKey({ explicitFile: args.signingKeyFile });
123
+ const releaseToken = args.dryRun ? null : await loadReleaseToken();
124
+ const runtime = {
125
+ metadata: fileMetadata,
126
+ upload: defaultUpload,
127
+ register: (envelope) => registerRuntimeArtifact(envelope, releaseToken),
128
+ };
129
+ const result = await promoteRuntimeArtifact({
130
+ authority: args.authority,
131
+ keyId: args.keyId,
132
+ privateKey,
133
+ artifactFile: path.resolve(configRoot, raw.artifact.file),
134
+ evidenceFile: path.resolve(configRoot, raw.evidence.file),
135
+ config,
136
+ dryRun: args.dryRun,
137
+ }, runtime);
138
+ const manifest = result.envelope.manifest;
139
+ console.log(`${args.dryRun ? "[dry-run] " : ""}runtime artifact promotion validated`);
140
+ console.log(` app/kind: ${manifest.entitlement.appKey}/${manifest.artifactKind}`);
141
+ console.log(` target: ${manifest.target.os}/${manifest.target.arch}`);
142
+ console.log(` delivery: ${manifest.distribution.delivery}`);
143
+ console.log(` object sha256: ${manifest.object.sha256}`);
144
+ console.log(` pointer: ${manifest.pointerKey ?? "bundled"}`);
145
+ console.log(` order: ${result.steps.join(" -> ")}`);
146
+ }
147
+
148
+ function printHelp() {
149
+ console.log(`right-release model promote --authority heardright --config <promotion.json> [options]
150
+
151
+ The JSON config has exactly:
152
+ artifact: { file, filename, sha256, sizeBytes }
153
+ evidence: { file, sha256 }
154
+ manifest: { artifactKind, entitlement, distribution, target, versions, provenance,
155
+ promotion: { promotionId, promotedAt } }
156
+ artifact.file and evidence.file resolve relative to the config; artifact.sha256,
157
+ artifact.sizeBytes, and evidence.sha256 must exactly match those files.
158
+
159
+ Options:
160
+ --dry-run Validate real files and sign, without R2 or RightApps mutations
161
+ --signing-key-file <pem> Override the protected PEM path
162
+ --key-id <id> Must equal ${RUNTIME_ARTIFACT_TRUSTED_KEY_ID}
163
+
164
+ Signing key inputs (never printed): RIGHTKIT_RUNTIME_ARTIFACT_PRIVATE_KEY or
165
+ RIGHTKIT_RUNTIME_ARTIFACT_PRIVATE_KEY_FILE. Windows defaults to
166
+ %APPDATA%/RightKit/runtime-artifact-signing-key.pem. Live promotion also uses
167
+ the durable RIGHTAPPS_RELEASE_TOKEN boundary.`);
168
+ }
169
+
170
+ function parseArgs(args) {
171
+ let authority = "";
172
+ let config = "";
173
+ let signingKeyFile;
174
+ let keyId = process.env.RIGHTKIT_RUNTIME_ARTIFACT_KEY_ID || RUNTIME_ARTIFACT_TRUSTED_KEY_ID;
175
+ let dryRun = false;
176
+ for (let i = 0; i < args.length; i++) {
177
+ const arg = args[i];
178
+ if (arg === "--authority") authority = args[++i] ?? "";
179
+ else if (arg === "--config") config = args[++i] ?? "";
180
+ else if (arg === "--signing-key-file") signingKeyFile = args[++i] ?? "";
181
+ else if (arg === "--key-id") keyId = args[++i] ?? "";
182
+ else if (arg === "--dry-run") dryRun = true;
183
+ else throw new Error(`unknown model promote argument: ${arg}`);
184
+ }
185
+ if (!config) throw new Error("model promote requires --config <path>");
186
+ if (!authority) throw new Error("model promote requires --authority heardright");
187
+ return { authority, config, signingKeyFile, keyId, dryRun };
188
+ }
189
+
190
+ async function fileMetadata(file) {
191
+ const info = await stat(file);
192
+ if (!info.isFile() || info.size <= 0) throw new Error(`promotion input is not a nonempty file: ${file}`);
193
+ const hash = createHash("sha256");
194
+ await new Promise((resolve, reject) => {
195
+ const stream = createReadStream(file);
196
+ stream.on("data", (chunk) => hash.update(chunk));
197
+ stream.on("error", reject);
198
+ stream.on("end", resolve);
199
+ });
200
+ return { sha256: hash.digest("hex"), sizeBytes: info.size };
201
+ }
202
+
203
+ async function defaultUpload({ file, content, key, lane }) {
204
+ const bucketAlias = lane === "private-r2" ? "private" : "public";
205
+ const env = runtimeArtifactUploadEnv();
206
+ if (file) return run(process.execPath, [UPLOAD, file, key, bucketAlias], env);
207
+ const tempRoot = await mkdtemp(path.join(os.tmpdir(), "rightkit-runtime-pointer-"));
208
+ const pointerFile = path.join(tempRoot, "manifest.json");
209
+ try {
210
+ await writeFile(pointerFile, content, { encoding: "utf8", mode: 0o600 });
211
+ await run(process.execPath, [UPLOAD, pointerFile, key, bucketAlias], env);
212
+ } finally {
213
+ await rm(tempRoot, { recursive: true, force: true });
214
+ }
215
+ }
216
+
217
+ async function registerRuntimeArtifact(envelope, releaseToken) {
218
+ return registerRightAppsRelease("/v1/admin/apps/runtime-artifacts", envelope, { token: releaseToken });
219
+ }
220
+
221
+ function run(command, args, env = process.env) {
222
+ return new Promise((resolve, reject) => {
223
+ const child = spawn(command, args, { cwd: process.cwd(), env, stdio: "inherit", windowsHide: true });
224
+ child.on("error", reject);
225
+ child.on("exit", (code) => code === 0 ? resolve() : reject(new Error(`${command} exited ${code}`)));
226
+ });
227
+ }
228
+
229
+ async function readNonemptySecret(file) {
230
+ const value = await readFile(file, "utf8");
231
+ if (!value.trim()) throw new Error(`runtime artifact signing key file is empty: ${file}`);
232
+ return value;
233
+ }
234
+
235
+ function assertPromotionConfig(value) {
236
+ assertExactKeys(value, ["artifact", "evidence", "manifest"], "promotion config");
237
+ assertExactKeys(value.artifact, ["filename", "sha256", "sizeBytes"], "artifact");
238
+ assertExactKeys(value.evidence, ["sha256"], "evidence");
239
+ assertExactKeys(value.manifest, [
240
+ "artifactKind", "entitlement", "distribution", "target", "versions", "provenance", "promotion",
241
+ ], "manifest config");
242
+ assertExactKeys(value.manifest.promotion, ["promotionId", "promotedAt"], "manifest config promotion");
243
+ }
244
+
245
+ function assertMetadata(actual, expected, label) {
246
+ if (actual.sha256 !== expected.sha256) throw new Error(`${label} SHA-256 does not match promotion config`);
247
+ if (actual.sizeBytes !== expected.sizeBytes) throw new Error(`${label} size does not match promotion config`);
248
+ }
249
+
250
+ function assertExactKeys(value, expected, label) {
251
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object`);
252
+ const allowed = new Set(expected);
253
+ for (const key of Object.keys(value)) if (!allowed.has(key)) throw new Error(`${label} has unknown field ${key}`);
254
+ for (const key of expected) if (!Object.hasOwn(value, key)) throw new Error(`${label}.${key} is required`);
255
+ }
256
+
257
+ if (process.argv[1] && pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url) {
258
+ main().catch((error) => {
259
+ console.error(`right-release model promote: ${error.message}`);
260
+ process.exitCode = 1;
261
+ });
262
+ }
@@ -0,0 +1,240 @@
1
+ import assert from "node:assert/strict";
2
+ import { createHash, generateKeyPairSync } from "node:crypto";
3
+ import { spawnSync } from "node:child_process";
4
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
5
+ import os from "node:os";
6
+ import path from "node:path";
7
+ import test from "node:test";
8
+ import { fileURLToPath } from "node:url";
9
+
10
+ import {
11
+ RUNTIME_ARTIFACT_TRUSTED_KEY_ID,
12
+ defaultWindowsRuntimeArtifactSigningKeyFile,
13
+ loadRuntimeArtifactSigningKey,
14
+ promoteRuntimeArtifact,
15
+ runtimeArtifactUploadEnv,
16
+ } from "./model-promote.mjs";
17
+
18
+ const { privateKey } = generateKeyPairSync("ed25519");
19
+ const artifactDigest = "a".repeat(64);
20
+ const evidenceDigest = "b".repeat(64);
21
+ const packageRoot = path.dirname(fileURLToPath(import.meta.url));
22
+
23
+ function request(overrides = {}) {
24
+ return {
25
+ authority: "heardright",
26
+ privateKey,
27
+ keyId: RUNTIME_ARTIFACT_TRUSTED_KEY_ID,
28
+ artifactFile: "C:/fixtures/parakeet.onnx",
29
+ evidenceFile: "C:/fixtures/eval.json",
30
+ config: {
31
+ artifact: {
32
+ filename: "parakeet.onnx",
33
+ sha256: artifactDigest,
34
+ sizeBytes: 42,
35
+ },
36
+ evidence: { sha256: evidenceDigest },
37
+ manifest: {
38
+ artifactKind: "asr-model",
39
+ entitlement: { appKey: "scraperight", tier: "pro" },
40
+ distribution: { delivery: "private-r2", bucket: "rightapps-updates" },
41
+ target: { os: "windows", arch: "x86_64" },
42
+ versions: {
43
+ runtime: "rightkit-asr-0.1.0",
44
+ model: "parakeet-tdt-0.6b-v3",
45
+ tokenizer: "sentencepiece-2026-07-14",
46
+ preprocessing: "rightkit-asr-0.1.0",
47
+ license: "cc-by-4.0",
48
+ provenance: "heardright-approved-2026-07-14",
49
+ },
50
+ provenance: {
51
+ source: "https://huggingface.co/nvidia/parakeet-tdt-0.6b-v3",
52
+ sourceRevision: "0123456789abcdef",
53
+ licenseId: "CC-BY-4.0",
54
+ noticeSha256: "c".repeat(64),
55
+ },
56
+ promotion: {
57
+ promotionId: "hr-asr-2026-07-14-001",
58
+ promotedAt: "2026-07-14T12:00:00.000Z",
59
+ },
60
+ },
61
+ },
62
+ ...overrides,
63
+ };
64
+ }
65
+
66
+ function boundaries(events) {
67
+ return {
68
+ metadata: async (file) => file.endsWith("eval.json")
69
+ ? { sha256: evidenceDigest, sizeBytes: 10 }
70
+ : { sha256: artifactDigest, sizeBytes: 42 },
71
+ upload: async ({ purpose }) => { events.push(`upload:${purpose}`); },
72
+ register: async () => { events.push("register"); },
73
+ };
74
+ }
75
+
76
+ test("uploads immutable object, registers signed envelope, then replaces stable pointer", async () => {
77
+ const events = [];
78
+ const result = await promoteRuntimeArtifact(request(), boundaries(events));
79
+
80
+ assert.deepEqual(events, ["upload:object", "register", "upload:pointer"]);
81
+ assert.equal(result.envelope.signature.keyId, RUNTIME_ARTIFACT_TRUSTED_KEY_ID);
82
+ assert.equal(result.envelope.manifest.object.sha256, artifactDigest);
83
+ assert.equal(result.envelope.manifest.promotion.evidenceSha256, evidenceDigest);
84
+ assert.match(result.envelope.manifest.pointerKey, /\/current\/manifest\.json$/);
85
+ });
86
+
87
+ test("dry-run validates and signs without invoking process or network boundaries", async () => {
88
+ const events = [];
89
+ const result = await promoteRuntimeArtifact(request({ dryRun: true }), boundaries(events));
90
+ assert.deepEqual(events, []);
91
+ assert.deepEqual(result.steps, ["upload immutable object", "register signed envelope", "replace stable pointer"]);
92
+ assert.equal(result.envelope.signature.algorithm, "Ed25519");
93
+ });
94
+
95
+ test("never registers or publishes a pointer after immutable upload failure", async () => {
96
+ const events = [];
97
+ const deps = boundaries(events);
98
+ deps.upload = async ({ purpose }) => {
99
+ events.push(`upload:${purpose}`);
100
+ throw new Error("R2 failed");
101
+ };
102
+ await assert.rejects(promoteRuntimeArtifact(request(), deps), /R2 failed/);
103
+ assert.deepEqual(events, ["upload:object"]);
104
+ });
105
+
106
+ test("never publishes a pointer when RightApps registration fails", async () => {
107
+ const events = [];
108
+ const deps = boundaries(events);
109
+ deps.register = async () => {
110
+ events.push("register");
111
+ throw new Error("registration failed");
112
+ };
113
+ await assert.rejects(promoteRuntimeArtifact(request(), deps), /registration failed/);
114
+ assert.deepEqual(events, ["upload:object", "register"]);
115
+ });
116
+
117
+ test("fails before mutation on digest, authority, key-id, or lane contradiction", async () => {
118
+ const badRequests = [
119
+ request({ authority: "scraperight" }),
120
+ request({ keyId: "untrusted-key" }),
121
+ request({ config: { ...request().config, artifact: { ...request().config.artifact, sha256: "d".repeat(64) } } }),
122
+ request({ config: {
123
+ ...request().config,
124
+ manifest: {
125
+ ...request().config.manifest,
126
+ entitlement: { appKey: "scraperight", tier: "public" },
127
+ distribution: { delivery: "public-r2", bucket: "rightapps-downloads" },
128
+ },
129
+ } }),
130
+ request({ config: {
131
+ ...request().config,
132
+ manifest: {
133
+ ...request().config.manifest,
134
+ promotion: {
135
+ ...request().config.manifest.promotion,
136
+ authorityAppKey: "scraperight",
137
+ },
138
+ },
139
+ } }),
140
+ ];
141
+ for (const input of badRequests) {
142
+ const events = [];
143
+ await assert.rejects(promoteRuntimeArtifact(input, boundaries(events)));
144
+ assert.deepEqual(events, []);
145
+ }
146
+ });
147
+
148
+ test("right-release routes model promote to the portable promotion lane", () => {
149
+ const result = spawnSync(process.execPath, [path.join(packageRoot, "cli/right-release.mjs"), "model", "promote", "--authority", "heardright"], {
150
+ cwd: packageRoot,
151
+ encoding: "utf8",
152
+ windowsHide: true,
153
+ });
154
+ assert.equal(result.status, 1);
155
+ assert.match(result.stderr, /model promote requires --config/);
156
+ assert.doesNotMatch(result.stderr, /unknown command/);
157
+ });
158
+
159
+ test("model promote help documents the portable config and secure key inputs", () => {
160
+ const result = spawnSync(process.execPath, [path.join(packageRoot, "cli/right-release.mjs"), "model", "promote", "--help"], {
161
+ cwd: packageRoot,
162
+ encoding: "utf8",
163
+ windowsHide: true,
164
+ });
165
+ assert.equal(result.status, 0, result.stderr);
166
+ assert.match(result.stdout, /artifact\.file/);
167
+ assert.match(result.stdout, /evidence\.sha256/);
168
+ assert.match(result.stdout, /RIGHTKIT_RUNTIME_ARTIFACT_PRIVATE_KEY_FILE/);
169
+ assert.match(result.stdout, /RIGHTAPPS_RELEASE_TOKEN/);
170
+ });
171
+
172
+ test("portable CLI dry-run validates real files and signs without network mutation or secret output", async () => {
173
+ const root = await mkdtemp(path.join(os.tmpdir(), "rightkit-model-promote-test-"));
174
+ try {
175
+ const artifact = Buffer.from("tiny model fixture");
176
+ const evidence = Buffer.from('{"certified":true}');
177
+ const testKeys = generateKeyPairSync("ed25519");
178
+ const keyPem = testKeys.privateKey.export({ type: "pkcs8", format: "pem" }).toString();
179
+ await Promise.all([
180
+ writeFile(path.join(root, "model.onnx"), artifact),
181
+ writeFile(path.join(root, "evidence.json"), evidence),
182
+ writeFile(path.join(root, "key.pem"), keyPem, { mode: 0o600 }),
183
+ ]);
184
+ const config = {
185
+ artifact: {
186
+ file: "model.onnx",
187
+ filename: "model.onnx",
188
+ sha256: createHash("sha256").update(artifact).digest("hex"),
189
+ sizeBytes: artifact.length,
190
+ },
191
+ evidence: {
192
+ file: "evidence.json",
193
+ sha256: createHash("sha256").update(evidence).digest("hex"),
194
+ },
195
+ manifest: request().config.manifest,
196
+ };
197
+ await writeFile(path.join(root, "promotion.json"), JSON.stringify(config));
198
+
199
+ const result = spawnSync(process.execPath, [
200
+ path.join(packageRoot, "cli/right-release.mjs"), "model", "promote",
201
+ "--authority", "heardright",
202
+ "--config", path.join(root, "promotion.json"),
203
+ "--signing-key-file", path.join(root, "key.pem"),
204
+ "--dry-run",
205
+ ], { cwd: root, encoding: "utf8", windowsHide: true });
206
+ assert.equal(result.status, 0, result.stderr);
207
+ assert.match(result.stdout, /\[dry-run\] runtime artifact promotion validated/);
208
+ assert.match(result.stdout, /upload immutable object -> register signed envelope -> replace stable pointer/);
209
+ assert.doesNotMatch(`${result.stdout}${result.stderr}`, /BEGIN PRIVATE KEY/);
210
+ assert.doesNotMatch(`${result.stdout}${result.stderr}`, /Uploading|POST https:/);
211
+ } finally {
212
+ await rm(root, { recursive: true, force: true });
213
+ }
214
+ });
215
+
216
+ test("uses the canonical Windows signing-key path and supports a Keychain-fed PEM environment value", async () => {
217
+ assert.equal(
218
+ defaultWindowsRuntimeArtifactSigningKeyFile({ APPDATA: "C:/Users/test/AppData/Roaming" }),
219
+ path.join("C:/Users/test/AppData/Roaming", "RightKit", "runtime-artifact-signing-key.pem"),
220
+ );
221
+ assert.equal(
222
+ await loadRuntimeArtifactSigningKey({ env: { RIGHTKIT_RUNTIME_ARTIFACT_PRIVATE_KEY: "line1\\nline2" }, platform: "darwin" }),
223
+ "line1\nline2",
224
+ );
225
+ });
226
+
227
+ test("does not forward signing or RightApps credentials into the Wrangler uploader process", () => {
228
+ const env = runtimeArtifactUploadEnv({
229
+ CLOUDFLARE_API_TOKEN: "cloudflare-token",
230
+ RIGHTKIT_RUNTIME_ARTIFACT_PRIVATE_KEY: "private-pem",
231
+ RIGHTKIT_RUNTIME_ARTIFACT_PRIVATE_KEY_FILE: "key.pem",
232
+ RIGHTAPPS_RELEASE_TOKEN: "release-token",
233
+ RIGHTAPPS_RELEASE_TOKEN_FILE: "release.token",
234
+ });
235
+ assert.equal(env.CLOUDFLARE_API_TOKEN, "cloudflare-token");
236
+ assert.equal(env.RIGHTKIT_RUNTIME_ARTIFACT_PRIVATE_KEY, undefined);
237
+ assert.equal(env.RIGHTKIT_RUNTIME_ARTIFACT_PRIVATE_KEY_FILE, undefined);
238
+ assert.equal(env.RIGHTAPPS_RELEASE_TOKEN, undefined);
239
+ assert.equal(env.RIGHTAPPS_RELEASE_TOKEN_FILE, undefined);
240
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rightkit/release",
3
- "version": "0.2.20",
3
+ "version": "0.2.21",
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": {
@@ -14,6 +14,11 @@
14
14
  "*.py"
15
15
  ],
16
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
+ },
17
22
  "publishConfig": {
18
23
  "registry": "https://registry.npmjs.org/",
19
24
  "access": "public"
@@ -23,8 +28,5 @@
23
28
  "url": "git+https://github.com/adrdsouza/claude.git",
24
29
  "directory": "tools/rightkit/packages/release"
25
30
  },
26
- "scripts": {
27
- "test": "node --test *.test.mjs",
28
- "doctor:all": "node --test right-suite-contract.test.mjs"
29
- }
30
- }
31
+ "packageManager": "pnpm@11.12.0"
32
+ }
@@ -6,6 +6,7 @@ import { fileURLToPath, pathToFileURL } from "node:url";
6
6
  import { spawn } from "node:child_process";
7
7
  import { pruneReleaseObjects } from "./prune-r2.mjs";
8
8
  import { loadReleaseToken } from "./release-token.mjs";
9
+ import { registerRightAppsRelease } from "./rightapps-register.mjs";
9
10
 
10
11
  const TOOL_ROOT = path.dirname(fileURLToPath(import.meta.url));
11
12
  const UPLOAD = path.join(TOOL_ROOT, "upload-large.mjs");
@@ -272,18 +273,12 @@ function patchInstallerKey(artifact) {
272
273
  }
273
274
 
274
275
  async function register(route, body) {
275
- if (!releaseToken) fail("RIGHTAPPS_RELEASE_TOKEN is required to register an update");
276
- const response = await fetch(`${API_BASE}${route}`, {
277
- method: "POST",
278
- headers: {
279
- authorization: `Bearer ${releaseToken}`,
280
- "content-type": "application/json",
281
- "x-store-slug": process.env.RIGHTAPPS_STORE_SLUG || "rightapps",
282
- },
283
- body: JSON.stringify(body),
284
- });
285
- if (!response.ok) fail(`registration failed: ${response.status} ${await response.text()}`);
286
- console.log(await response.text());
276
+ try {
277
+ const response = await registerRightAppsRelease(route, body, { token: releaseToken });
278
+ if (response !== null) console.log(typeof response === "string" ? response : JSON.stringify(response));
279
+ } catch (error) {
280
+ fail(error.message);
281
+ }
287
282
  }
288
283
 
289
284
  function fail(message) {
@@ -310,7 +310,7 @@ test("Cargo version contract rejects staged versions that mismatch canonical man
310
310
  test("RightKit exposes one current version manifest", () => {
311
311
  assert.equal(existsSync(versionsPath), true, "rightkit-versions.json must be the single current-version source");
312
312
  assert.match(versions.packageManager, /^pnpm@\d+\.\d+\.\d+$/);
313
- assert.equal(versions.npm["@rightkit/release"], "0.2.20");
313
+ assert.equal(versions.npm["@rightkit/release"], "0.2.21");
314
314
  assert.equal(versions.npm["@rightkit/license"], "0.1.5");
315
315
  assert.equal(versions.npm["@rightkit/logs"], "0.1.3");
316
316
  assert.equal(versions.npm["@rightkit/tauri"], "0.1.0");
@@ -0,0 +1,31 @@
1
+ const DEFAULT_API_BASE = "https://api.spoares.com";
2
+ const REGISTRATION_ROUTES = new Set([
3
+ "/v1/admin/apps/patches",
4
+ "/v1/admin/apps/releases",
5
+ "/v1/admin/apps/runtime-artifacts",
6
+ ]);
7
+
8
+ export async function registerRightAppsRelease(route, body, {
9
+ token,
10
+ env = process.env,
11
+ fetchImpl = fetch,
12
+ } = {}) {
13
+ if (!token?.trim()) throw new Error("RIGHTAPPS_RELEASE_TOKEN release token is required for RightApps registration");
14
+ if (!REGISTRATION_ROUTES.has(route)) {
15
+ throw new Error("RightApps registration route is not approved");
16
+ }
17
+ const apiBase = (env.RIGHTAPPS_API_URL || DEFAULT_API_BASE).replace(/\/$/, "");
18
+ const response = await fetchImpl(`${apiBase}${route}`, {
19
+ method: "POST",
20
+ headers: {
21
+ authorization: `Bearer ${token.trim()}`,
22
+ "content-type": "application/json",
23
+ "x-store-slug": env.RIGHTAPPS_STORE_SLUG || "rightapps",
24
+ },
25
+ body: JSON.stringify(body),
26
+ });
27
+ const text = await response.text();
28
+ if (!response.ok) throw new Error(`RightApps registration failed: ${response.status} ${text}`);
29
+ if (!text) return null;
30
+ try { return JSON.parse(text); } catch { return text; }
31
+ }
@@ -0,0 +1,28 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+
4
+ import { registerRightAppsRelease } from "./rightapps-register.mjs";
5
+
6
+ test("registers through the shared durable release-token boundary", async () => {
7
+ const calls = [];
8
+ const response = await registerRightAppsRelease("/v1/admin/apps/runtime-artifacts", { schema: 1 }, {
9
+ token: "durable-token",
10
+ env: { RIGHTAPPS_API_URL: "https://rightapps.example", RIGHTAPPS_STORE_SLUG: "rightapps" },
11
+ fetchImpl: async (url, init) => {
12
+ calls.push({ url, init });
13
+ return { ok: true, text: async () => '{"id":"registered"}' };
14
+ },
15
+ });
16
+ assert.deepEqual(response, { id: "registered" });
17
+ assert.equal(calls[0].url, "https://rightapps.example/v1/admin/apps/runtime-artifacts");
18
+ assert.equal(calls[0].init.headers.authorization, "Bearer durable-token");
19
+ });
20
+
21
+ test("rejects absent durable token and non-admin registration paths before network", async () => {
22
+ let called = false;
23
+ const fetchImpl = async () => { called = true; };
24
+ await assert.rejects(registerRightAppsRelease("/v1/admin/apps/runtime-artifacts", {}, { token: "", fetchImpl }), /release token/i);
25
+ await assert.rejects(registerRightAppsRelease("https://evil.invalid", {}, { token: "token", fetchImpl }), /route/i);
26
+ await assert.rejects(registerRightAppsRelease("/v1/admin/apps/../../evil", {}, { token: "token", fetchImpl }), /route/i);
27
+ assert.equal(called, false);
28
+ });
@@ -4,7 +4,7 @@
4
4
  "npm": {
5
5
  "@rightkit/license": "0.1.5",
6
6
  "@rightkit/logs": "0.1.3",
7
- "@rightkit/release": "0.2.20",
7
+ "@rightkit/release": "0.2.21",
8
8
  "@rightkit/tauri": "0.1.0",
9
9
  "@rightkit/updates": "0.2.3"
10
10
  },
@@ -0,0 +1,247 @@
1
+ import { sign as edSign, verify as edVerify } from "node:crypto";
2
+
3
+ export const RUNTIME_ARTIFACT_SCHEMA = 1;
4
+ export const RUNTIME_ARTIFACT_KINDS = Object.freeze([
5
+ "asr-model",
6
+ "ocr-model",
7
+ "ort-runtime",
8
+ "media-runtime",
9
+ "tokenizer",
10
+ "preprocessing",
11
+ ]);
12
+ export const RIGHT_SUITE_APP_KEYS = Object.freeze([
13
+ "viewright",
14
+ "heardright",
15
+ "mailright",
16
+ "scraperight",
17
+ "coderight",
18
+ ]);
19
+
20
+ const TARGET_OSES = new Set(["windows", "darwin", "linux", "ios"]);
21
+ const TARGET_ARCHES = new Set(["x86_64", "aarch64", "universal"]);
22
+ const VERSION_FIELDS = Object.freeze([
23
+ "runtime",
24
+ "model",
25
+ "tokenizer",
26
+ "preprocessing",
27
+ "license",
28
+ "provenance",
29
+ ]);
30
+ const SHA256 = /^[0-9a-f]{64}$/;
31
+
32
+ export function buildRuntimeArtifactManifest(input) {
33
+ const entitlement = cloneObject(input.entitlement);
34
+ const target = cloneObject(input.target);
35
+ const manifest = {
36
+ schema: RUNTIME_ARTIFACT_SCHEMA,
37
+ artifactKind: input.artifactKind,
38
+ entitlement,
39
+ distribution: cloneObject(input.distribution),
40
+ target,
41
+ object: {
42
+ r2Key: "",
43
+ filename: input.filename,
44
+ sha256: input.sha256,
45
+ sizeBytes: input.sizeBytes,
46
+ },
47
+ pointerKey: "",
48
+ versions: cloneObject(input.versions),
49
+ provenance: cloneObject(input.provenance),
50
+ promotion: cloneObject(input.promotion),
51
+ };
52
+ if (manifest.distribution?.delivery === "bundled") {
53
+ manifest.object.r2Key = null;
54
+ manifest.pointerKey = null;
55
+ } else {
56
+ manifest.object.r2Key = runtimeArtifactObjectKey(manifest);
57
+ manifest.pointerKey = runtimeArtifactManifestPointerKey(manifest);
58
+ }
59
+ return validateRuntimeArtifactManifest(manifest);
60
+ }
61
+
62
+ export function runtimeArtifactObjectKey(value) {
63
+ const appKey = value?.entitlement?.appKey;
64
+ const digest = value?.object?.sha256 ?? value?.sha256;
65
+ const filename = value?.object?.filename ?? value?.filename;
66
+ assertAppKey(appKey, "entitlement.appKey");
67
+ assertSha256(digest, "object.sha256");
68
+ assertFilename(filename);
69
+ return `${appKey}/runtime-artifacts/objects/sha256/${digest}/${filename}`;
70
+ }
71
+
72
+ export function runtimeArtifactManifestPointerKey(value) {
73
+ const appKey = value?.entitlement?.appKey;
74
+ const kind = value?.artifactKind;
75
+ const os = value?.target?.os;
76
+ const arch = value?.target?.arch;
77
+ assertAppKey(appKey, "entitlement.appKey");
78
+ if (!RUNTIME_ARTIFACT_KINDS.includes(kind)) fail("artifactKind is unsupported");
79
+ if (!TARGET_OSES.has(os)) fail("target.os is unsupported");
80
+ if (!TARGET_ARCHES.has(arch)) fail("target.arch is unsupported");
81
+ return `${appKey}/runtime-artifacts/${kind}/${os}/${arch}/current/manifest.json`;
82
+ }
83
+
84
+ export function validateRuntimeArtifactManifest(value) {
85
+ assertPlainObject(value, "manifest");
86
+ assertExactKeys(value, [
87
+ "schema", "artifactKind", "entitlement", "distribution", "target", "object", "pointerKey",
88
+ "versions", "provenance", "promotion",
89
+ ], "manifest");
90
+ if (value.schema !== RUNTIME_ARTIFACT_SCHEMA) fail("schema must be 1");
91
+ if (!RUNTIME_ARTIFACT_KINDS.includes(value.artifactKind)) fail("artifactKind is unsupported");
92
+
93
+ assertPlainObject(value.entitlement, "entitlement");
94
+ assertExactKeys(value.entitlement, ["appKey", "tier"], "entitlement");
95
+ assertAppKey(value.entitlement.appKey, "entitlement.appKey");
96
+ if (!["pro", "public", "bundled"].includes(value.entitlement.tier)) fail("entitlement.tier is unsupported");
97
+
98
+ assertPlainObject(value.distribution, "distribution");
99
+ assertExactKeys(value.distribution, ["delivery", "bucket"], "distribution");
100
+ const laneContracts = {
101
+ "private-r2": { tier: "pro", bucket: "rightapps-updates" },
102
+ "public-r2": { tier: "public", bucket: "rightapps-downloads" },
103
+ bundled: { tier: "bundled", bucket: null },
104
+ };
105
+ const lane = laneContracts[value.distribution.delivery];
106
+ if (!lane) fail("distribution.delivery is unsupported");
107
+ if (value.entitlement.tier !== lane.tier || value.distribution.bucket !== lane.bucket) {
108
+ fail("distribution lane contradicts entitlement tier or bucket");
109
+ }
110
+ if (["asr-model", "ocr-model", "tokenizer"].includes(value.artifactKind)
111
+ && (value.distribution.delivery !== "private-r2" || value.entitlement.tier !== "pro")) {
112
+ fail("model artifacts must use private-r2 with Pro entitlement");
113
+ }
114
+
115
+ assertPlainObject(value.target, "target");
116
+ assertExactKeys(value.target, ["os", "arch"], "target");
117
+ if (!TARGET_OSES.has(value.target.os)) fail("target.os is unsupported");
118
+ if (!TARGET_ARCHES.has(value.target.arch)) fail("target.arch is unsupported");
119
+
120
+ assertPlainObject(value.object, "object");
121
+ assertExactKeys(value.object, ["r2Key", "filename", "sha256", "sizeBytes"], "object");
122
+ assertFilename(value.object.filename);
123
+ assertSha256(value.object.sha256, "object.sha256");
124
+ if (!Number.isSafeInteger(value.object.sizeBytes) || value.object.sizeBytes <= 0) fail("object.sizeBytes must be a positive safe integer");
125
+ if (value.distribution.delivery === "bundled") {
126
+ if (value.object.r2Key !== null || value.pointerKey !== null) fail("bundled artifacts must not claim R2 keys");
127
+ } else {
128
+ if (value.object.r2Key !== runtimeArtifactObjectKey(value)) fail("object.r2Key contradicts its immutable SHA-256 object key");
129
+ if (value.pointerKey !== runtimeArtifactManifestPointerKey(value)) fail("pointerKey must be the replace-only stable current manifest key");
130
+ }
131
+
132
+ assertPlainObject(value.versions, "versions");
133
+ assertExactKeys(value.versions, VERSION_FIELDS, "versions");
134
+ for (const field of VERSION_FIELDS) assertNonempty(value.versions[field], `versions.${field}`);
135
+
136
+ assertPlainObject(value.provenance, "provenance");
137
+ assertExactKeys(value.provenance, ["source", "sourceRevision", "licenseId", "noticeSha256"], "provenance");
138
+ assertHttpsUrl(value.provenance.source, "provenance.source");
139
+ assertNonempty(value.provenance.sourceRevision, "provenance.sourceRevision");
140
+ assertNonempty(value.provenance.licenseId, "provenance.licenseId");
141
+ assertSha256(value.provenance.noticeSha256, "provenance.noticeSha256");
142
+ if (normalizeLicense(value.versions.license) !== normalizeLicense(value.provenance.licenseId)) {
143
+ fail("license version contradicts provenance licenseId");
144
+ }
145
+
146
+ assertPlainObject(value.promotion, "promotion");
147
+ assertExactKeys(value.promotion, ["authorityAppKey", "promotionId", "promotedAt", "evidenceSha256"], "promotion");
148
+ assertAppKey(value.promotion.authorityAppKey, "promotion.authorityAppKey");
149
+ assertNonempty(value.promotion.promotionId, "promotion.promotionId");
150
+ const promotedAt = new Date(value.promotion.promotedAt);
151
+ if (Number.isNaN(promotedAt.getTime()) || promotedAt.toISOString() !== value.promotion.promotedAt) fail("promotion.promotedAt must be an exact ISO-8601 UTC timestamp");
152
+ assertSha256(value.promotion.evidenceSha256, "promotion.evidenceSha256");
153
+ return value;
154
+ }
155
+
156
+ export function canonicalRuntimeArtifactManifest(manifest) {
157
+ validateRuntimeArtifactManifest(manifest);
158
+ return JSON.stringify(sortDeep(manifest));
159
+ }
160
+
161
+ export function signRuntimeArtifactManifest(manifest, privateKey, keyId) {
162
+ assertNonempty(keyId, "signature.keyId");
163
+ const canonical = Buffer.from(canonicalRuntimeArtifactManifest(manifest), "utf8");
164
+ return {
165
+ manifest,
166
+ signature: {
167
+ algorithm: "Ed25519",
168
+ keyId,
169
+ value: edSign(null, canonical, privateKey).toString("base64url"),
170
+ },
171
+ };
172
+ }
173
+
174
+ export function verifyRuntimeArtifactEnvelope(value, publicKey) {
175
+ assertPlainObject(value, "envelope");
176
+ assertExactKeys(value, ["manifest", "signature"], "envelope");
177
+ validateRuntimeArtifactManifest(value.manifest);
178
+ assertPlainObject(value.signature, "signature");
179
+ assertExactKeys(value.signature, ["algorithm", "keyId", "value"], "signature");
180
+ if (value.signature.algorithm !== "Ed25519") fail("signature algorithm must be Ed25519");
181
+ assertNonempty(value.signature.keyId, "signature.keyId");
182
+ assertNonempty(value.signature.value, "signature.value");
183
+ let signature;
184
+ try {
185
+ signature = Buffer.from(value.signature.value, "base64url");
186
+ } catch {
187
+ fail("signature is not base64url");
188
+ }
189
+ if (signature.length !== 64 || !edVerify(null, Buffer.from(canonicalRuntimeArtifactManifest(value.manifest), "utf8"), publicKey, signature)) {
190
+ fail("signature verification failed");
191
+ }
192
+ return value.manifest;
193
+ }
194
+
195
+ function sortDeep(value) {
196
+ if (Array.isArray(value)) return value.map(sortDeep);
197
+ if (value && typeof value === "object") {
198
+ return Object.fromEntries(Object.keys(value).sort().map((key) => [key, sortDeep(value[key])]));
199
+ }
200
+ return value;
201
+ }
202
+
203
+ function assertExactKeys(value, expected, label) {
204
+ const expectedSet = new Set(expected);
205
+ for (const key of Object.keys(value)) if (!expectedSet.has(key)) fail(`${label} has unknown field ${key}`);
206
+ for (const key of expected) if (!Object.hasOwn(value, key)) fail(`${label}.${key} is required`);
207
+ }
208
+
209
+ function assertPlainObject(value, label) {
210
+ if (value === null || typeof value !== "object" || Array.isArray(value) || Object.getPrototypeOf(value) !== Object.prototype) fail(`${label} must be a plain object`);
211
+ }
212
+
213
+ function assertAppKey(value, label) {
214
+ if (!RIGHT_SUITE_APP_KEYS.includes(value)) fail(`${label} is not a Right Suite app`);
215
+ }
216
+
217
+ function assertFilename(value) {
218
+ assertNonempty(value, "object.filename");
219
+ if (value === "." || value === ".." || value.includes("/") || value.includes("\\") || value.includes("..")) fail("object.filename must be one safe basename");
220
+ }
221
+
222
+ function assertSha256(value, label) {
223
+ if (typeof value !== "string" || !SHA256.test(value)) fail(`${label} must be lowercase SHA-256`);
224
+ }
225
+
226
+ function assertNonempty(value, label) {
227
+ if (typeof value !== "string" || value.trim() !== value || value.length === 0) fail(`${label} must be a nonempty exact string`);
228
+ }
229
+
230
+ function assertHttpsUrl(value, label) {
231
+ assertNonempty(value, label);
232
+ let url;
233
+ try { url = new URL(value); } catch { fail(`${label} must be an HTTPS URL`); }
234
+ if (url.protocol !== "https:") fail(`${label} must be an HTTPS URL`);
235
+ }
236
+
237
+ function normalizeLicense(value) {
238
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "");
239
+ }
240
+
241
+ function cloneObject(value) {
242
+ return value && typeof value === "object" ? structuredClone(value) : value;
243
+ }
244
+
245
+ function fail(message) {
246
+ throw new Error(`invalid runtime artifact manifest: ${message}`);
247
+ }
@@ -0,0 +1,128 @@
1
+ import assert from "node:assert/strict";
2
+ import { generateKeyPairSync } from "node:crypto";
3
+ import test from "node:test";
4
+
5
+ import {
6
+ buildRuntimeArtifactManifest,
7
+ runtimeArtifactManifestPointerKey,
8
+ runtimeArtifactObjectKey,
9
+ signRuntimeArtifactManifest,
10
+ validateRuntimeArtifactManifest,
11
+ verifyRuntimeArtifactEnvelope,
12
+ } from "./runtime-artifact-manifest.mjs";
13
+
14
+ const digest = "a".repeat(64);
15
+ const evidenceDigest = "b".repeat(64);
16
+
17
+ function validManifest(overrides = {}) {
18
+ return buildRuntimeArtifactManifest({
19
+ artifactKind: "asr-model",
20
+ filename: "parakeet-tdt-v3.onnx",
21
+ sha256: digest,
22
+ sizeBytes: 42,
23
+ entitlement: { appKey: "scraperight", tier: "pro" },
24
+ distribution: { delivery: "private-r2", bucket: "rightapps-updates" },
25
+ target: { os: "windows", arch: "x86_64" },
26
+ versions: {
27
+ runtime: "onnxruntime-1.22.0",
28
+ model: "parakeet-tdt-0.6b-v3",
29
+ tokenizer: "sentencepiece-2026-07-14",
30
+ preprocessing: "rightkit-asr-0.1.0",
31
+ license: "cc-by-4.0",
32
+ provenance: "heardright-approved-2026-07-14",
33
+ },
34
+ provenance: {
35
+ source: "https://huggingface.co/nvidia/parakeet-tdt-0.6b-v3",
36
+ sourceRevision: "0123456789abcdef",
37
+ licenseId: "CC-BY-4.0",
38
+ noticeSha256: "c".repeat(64),
39
+ },
40
+ promotion: {
41
+ authorityAppKey: "heardright",
42
+ promotionId: "hr-asr-2026-07-14-001",
43
+ promotedAt: "2026-07-14T12:00:00.000Z",
44
+ evidenceSha256: evidenceDigest,
45
+ },
46
+ ...overrides,
47
+ });
48
+ }
49
+
50
+ test("builds exact stable-pointer and immutable content-addressed object keys", () => {
51
+ const manifest = validManifest();
52
+ assert.equal(
53
+ manifest.pointerKey,
54
+ "scraperight/runtime-artifacts/asr-model/windows/x86_64/current/manifest.json",
55
+ );
56
+ assert.equal(
57
+ manifest.object.r2Key,
58
+ `scraperight/runtime-artifacts/objects/sha256/${digest}/parakeet-tdt-v3.onnx`,
59
+ );
60
+ assert.equal(runtimeArtifactManifestPointerKey(manifest), manifest.pointerKey);
61
+ assert.equal(runtimeArtifactObjectKey(manifest), manifest.object.r2Key);
62
+ assert.equal(validateRuntimeArtifactManifest(manifest), manifest);
63
+ });
64
+
65
+ test("signs and verifies the canonical manifest with Ed25519", () => {
66
+ const { privateKey, publicKey } = generateKeyPairSync("ed25519");
67
+ const envelope = signRuntimeArtifactManifest(validManifest(), privateKey, "rightapps-runtime-2026-01");
68
+ assert.equal(envelope.signature.algorithm, "Ed25519");
69
+ assert.equal(verifyRuntimeArtifactEnvelope(envelope, publicKey).artifactKind, "asr-model");
70
+
71
+ const tampered = structuredClone(envelope);
72
+ tampered.manifest.object.sizeBytes += 1;
73
+ assert.throws(() => verifyRuntimeArtifactEnvelope(tampered, publicKey), /signature verification failed/);
74
+ });
75
+
76
+ test("fails closed on missing or contradictory digest, provenance, version, scope, and promotion metadata", () => {
77
+ const mutations = [
78
+ (m) => delete m.versions.tokenizer,
79
+ (m) => { m.versions.license = ""; },
80
+ (m) => delete m.provenance.sourceRevision,
81
+ (m) => { m.provenance.licenseId = "MIT"; },
82
+ (m) => { m.entitlement.tier = "free"; },
83
+ (m) => { m.object.sha256 = "d".repeat(64); },
84
+ (m) => { m.object.sizeBytes = 0; },
85
+ (m) => { m.pointerKey = "scraperight/runtime-artifacts/asr-model/windows/x86_64/v1/manifest.json"; },
86
+ (m) => { m.promotion.authorityAppKey = "unknown"; },
87
+ (m) => { m.promotion.evidenceSha256 = "not-a-digest"; },
88
+ ];
89
+ for (const mutate of mutations) {
90
+ const manifest = structuredClone(validManifest());
91
+ mutate(manifest);
92
+ assert.throws(() => validateRuntimeArtifactManifest(manifest));
93
+ }
94
+ });
95
+
96
+ test("rejects unknown fields and path traversal instead of guessing", () => {
97
+ const extra = structuredClone(validManifest());
98
+ extra.secretUrl = "https://example.invalid/token";
99
+ assert.throws(() => validateRuntimeArtifactManifest(extra), /unknown field/);
100
+ assert.throws(() => validManifest({ filename: "../model.onnx" }), /filename/);
101
+ });
102
+
103
+ test("requires an explicit compatible entitlement and delivery lane per artifact", () => {
104
+ const publicMedia = validManifest({
105
+ artifactKind: "media-runtime",
106
+ entitlement: { appKey: "scraperight", tier: "public" },
107
+ distribution: { delivery: "public-r2", bucket: "rightapps-downloads" },
108
+ });
109
+ assert.equal(publicMedia.distribution.delivery, "public-r2");
110
+
111
+ const bundledOrt = validManifest({
112
+ artifactKind: "ort-runtime",
113
+ entitlement: { appKey: "viewright", tier: "bundled" },
114
+ distribution: { delivery: "bundled", bucket: null },
115
+ });
116
+ assert.equal(bundledOrt.pointerKey, null);
117
+ assert.equal(bundledOrt.object.r2Key, null);
118
+
119
+ assert.throws(() => validManifest({
120
+ entitlement: { appKey: "scraperight", tier: "public" },
121
+ distribution: { delivery: "public-r2", bucket: "rightapps-downloads" },
122
+ }), /model.*private-r2.*pro/i);
123
+ assert.throws(() => validManifest({
124
+ artifactKind: "media-runtime",
125
+ entitlement: { appKey: "scraperight", tier: "public" },
126
+ distribution: { delivery: "private-r2", bucket: "rightapps-updates" },
127
+ }), /contradict/);
128
+ });
@@ -0,0 +1,131 @@
1
+ import { existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import path from "node:path";
4
+ import { spawnSync } from "node:child_process";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ export const DEFAULT_APPS = Object.freeze([
8
+ { key: "viewright", remote: "https://github.com/bogusyogi/viewright.git", appDir: "." },
9
+ { key: "scraperight", remote: "https://github.com/bogusyogi/scraperight.git", appDir: "." },
10
+ { key: "heardright", remote: "https://github.com/bogusyogi/heardright.git", appDir: "tauri-app-next" },
11
+ { key: "mailright", remote: "https://github.com/bogusyogi/mailright.git", appDir: "." },
12
+ { key: "coderight", remote: "https://github.com/bogusyogi/coderight.git", appDir: "apps/coderight-tauri" },
13
+ ]);
14
+
15
+ function run(command, args, cwd) {
16
+ const result = spawnSync(command, args, {
17
+ cwd,
18
+ encoding: "utf8",
19
+ windowsHide: true,
20
+ maxBuffer: 16 * 1024 * 1024,
21
+ env: { ...process.env, CI: "1", GIT_TERMINAL_PROMPT: "0" },
22
+ });
23
+ return {
24
+ command: [command, ...args].join(" "),
25
+ status: result.status,
26
+ stdout: String(result.stdout ?? "").trim().slice(-8000),
27
+ stderr: String(result.stderr ?? "").trim().slice(-8000),
28
+ error: result.error?.message,
29
+ };
30
+ }
31
+
32
+ function runPnpm(args, cwd, command) {
33
+ if (process.platform !== "win32" || command !== "pnpm") return run(command, args, cwd);
34
+ for (const entry of String(process.env.PATH ?? "").split(path.delimiter)) {
35
+ const cli = path.join(entry.replace(/^"|"$/g, ""), "node_modules", "pnpm", "bin", "pnpm.mjs");
36
+ if (existsSync(cli)) return run(process.execPath, [cli, ...args], cwd);
37
+ }
38
+ throw new Error("pnpm installation could not be resolved from PATH");
39
+ }
40
+
41
+ function requireSuccess(result, label) {
42
+ if (result.status !== 0) {
43
+ throw new Error(`${label} failed (${result.status ?? "spawn error"})\n${result.stderr || result.stdout || result.error || "no output"}`);
44
+ }
45
+ }
46
+
47
+ function assertGeneratedWorkRoot(workRoot, tempParent) {
48
+ const parent = path.resolve(tempParent);
49
+ const candidate = path.resolve(workRoot);
50
+ if (path.dirname(candidate) !== parent || !path.basename(candidate).startsWith("rightkit-standalone-")) {
51
+ throw new Error(`refusing cleanup outside generated standalone directory: ${candidate}`);
52
+ }
53
+ }
54
+
55
+ export async function verifyStandaloneClones({
56
+ apps = DEFAULT_APPS,
57
+ evidencePath = path.resolve("standalone-clone-evidence.json"),
58
+ tempParent = tmpdir(),
59
+ pnpmCommand = "pnpm",
60
+ } = {}) {
61
+ mkdirSync(tempParent, { recursive: true });
62
+ const workRoot = mkdtempSync(path.join(path.resolve(tempParent), "rightkit-standalone-"));
63
+ const evidence = { schemaVersion: 1, generatedAt: new Date().toISOString(), workRoot, apps: [] };
64
+
65
+ try {
66
+ for (const app of apps) {
67
+ const cloneRoot = path.join(workRoot, app.key);
68
+ const clone = run("git", ["clone", "--depth", "1", "--single-branch", app.remote, cloneRoot], workRoot);
69
+ requireSuccess(clone, `${app.key} clone`);
70
+ const appRoot = path.resolve(cloneRoot, app.appDir);
71
+ if (!appRoot.startsWith(`${path.resolve(cloneRoot)}${path.sep}`) && appRoot !== path.resolve(cloneRoot)) {
72
+ throw new Error(`${app.key} package root escapes its clone`);
73
+ }
74
+ if (!existsSync(path.join(appRoot, "package.json"))) throw new Error(`${app.key} package root does not exist: ${app.appDir}`);
75
+ const pkg = JSON.parse(readFileSync(path.join(appRoot, "package.json"), "utf8"));
76
+ const expectedPnpm = String(pkg.packageManager ?? "").match(/^pnpm@(\d+\.\d+\.\d+)$/)?.[1];
77
+ if (!expectedPnpm) throw new Error(`${app.key} must pin an exact pnpm packageManager`);
78
+ const pnpmVersion = runPnpm(["--version"], appRoot, pnpmCommand);
79
+ requireSuccess(pnpmVersion, `${app.key} pnpm version`);
80
+ if (pnpmVersion.stdout !== expectedPnpm) {
81
+ throw new Error(`${app.key} requires pnpm ${expectedPnpm}, found ${pnpmVersion.stdout}`);
82
+ }
83
+ const install = runPnpm(["install", "--frozen-lockfile"], appRoot, pnpmCommand);
84
+ requireSuccess(install, `${app.key} pnpm install`);
85
+ const doctor = runPnpm(["release:doctor"], appRoot, pnpmCommand);
86
+ requireSuccess(doctor, `${app.key} release:doctor`);
87
+ const revision = run("git", ["rev-parse", "HEAD"], cloneRoot);
88
+ requireSuccess(revision, `${app.key} revision`);
89
+ evidence.apps.push({
90
+ key: app.key,
91
+ remote: app.remote,
92
+ appDir: app.appDir,
93
+ revision: revision.stdout,
94
+ packageManager: pkg.packageManager,
95
+ clone,
96
+ install,
97
+ doctor,
98
+ });
99
+ mkdirSync(path.dirname(path.resolve(evidencePath)), { recursive: true });
100
+ writeFileSync(path.resolve(evidencePath), `${JSON.stringify(evidence, null, 2)}\n`, "utf8");
101
+ }
102
+ return evidence;
103
+ } finally {
104
+ assertGeneratedWorkRoot(workRoot, tempParent);
105
+ rmSync(workRoot, { recursive: true, force: true });
106
+ }
107
+ }
108
+
109
+ function parseArgs(argv) {
110
+ let evidencePath = path.resolve("standalone-clone-evidence.json");
111
+ let selectedKeys = [];
112
+ for (let index = 0; index < argv.length; index += 1) {
113
+ if (argv[index] === "--evidence") evidencePath = path.resolve(argv[++index]);
114
+ else if (argv[index] === "--app") selectedKeys.push(argv[++index]);
115
+ else throw new Error(`unknown argument: ${argv[index]}`);
116
+ }
117
+ const apps = selectedKeys.length ? DEFAULT_APPS.filter(({ key }) => selectedKeys.includes(key)) : DEFAULT_APPS;
118
+ if (apps.length !== (selectedKeys.length || DEFAULT_APPS.length)) throw new Error("unknown or duplicate --app value");
119
+ return { apps, evidencePath };
120
+ }
121
+
122
+ if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url))) {
123
+ try {
124
+ const evidence = await verifyStandaloneClones(parseArgs(process.argv.slice(2)));
125
+ for (const app of evidence.apps) process.stdout.write(`[standalone] ${app.key} ${app.revision.slice(0, 12)} install+doctor passed\n`);
126
+ process.stdout.write(`[standalone] evidence ${path.resolve(parseArgs(process.argv.slice(2)).evidencePath)}\n`);
127
+ } catch (error) {
128
+ process.stderr.write(`[standalone] ${error.message}\n`);
129
+ process.exitCode = 1;
130
+ }
131
+ }
@@ -0,0 +1,76 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtempSync, readFileSync, writeFileSync, mkdirSync, existsSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import path from "node:path";
5
+ import { spawnSync } from "node:child_process";
6
+ import test from "node:test";
7
+
8
+ import { verifyStandaloneClones } from "./standalone-clone-verify.mjs";
9
+
10
+ function run(command, args, cwd) {
11
+ const result = spawnSync(command, args, { cwd, encoding: "utf8", windowsHide: true });
12
+ assert.equal(result.status, 0, `${command} ${args.join(" ")}\n${result.stderr}`);
13
+ }
14
+
15
+ test("standalone verifier clones, installs, doctors from a nested app root, and removes only its own temp tree", async (t) => {
16
+ const fixtureRoot = mkdtempSync(path.join(tmpdir(), "rightkit-standalone-fixture-"));
17
+ t.after(() => rmSync(fixtureRoot, { recursive: true, force: true }));
18
+ const source = path.join(fixtureRoot, "source");
19
+ const outsideSentinel = path.join(fixtureRoot, "keep.txt");
20
+ mkdirSync(path.join(source, "apps", "desktop"), { recursive: true });
21
+ writeFileSync(outsideSentinel, "keep", "utf8");
22
+ writeFileSync(path.join(source, "apps", "desktop", "package.json"), JSON.stringify({
23
+ name: "standalone-fixture",
24
+ private: true,
25
+ packageManager: "pnpm@11.12.0",
26
+ scripts: { "release:doctor": "node doctor.mjs" },
27
+ }), "utf8");
28
+ writeFileSync(path.join(source, "apps", "desktop", "pnpm-lock.yaml"), "lockfileVersion: '9.0'\nsettings:\n autoInstallPeers: true\n excludeLinksFromLockfile: false\nimporters:\n .: {}\n", "utf8");
29
+ writeFileSync(path.join(source, "apps", "desktop", "doctor.mjs"), "process.stdout.write('fixture doctor passed\\n')\n", "utf8");
30
+ run("git", ["init", "-q"], source);
31
+ run("git", ["config", "user.email", "fixture@example.test"], source);
32
+ run("git", ["config", "user.name", "Fixture"], source);
33
+ run("git", ["add", "."], source);
34
+ run("git", ["commit", "-qm", "fixture"], source);
35
+
36
+ const evidencePath = path.join(fixtureRoot, "evidence.json");
37
+ const result = await verifyStandaloneClones({
38
+ apps: [{ key: "fixture", remote: source, appDir: "apps/desktop" }],
39
+ evidencePath,
40
+ tempParent: fixtureRoot,
41
+ });
42
+
43
+ assert.equal(result.apps[0].install.status, 0);
44
+ assert.equal(result.apps[0].doctor.status, 0);
45
+ assert.match(result.apps[0].doctor.stdout, /fixture doctor passed/);
46
+ assert.equal(existsSync(result.workRoot), false, "generated clone tree must be removed");
47
+ assert.equal(readFileSync(outsideSentinel, "utf8"), "keep", "cleanup must not escape the generated tree");
48
+ assert.deepEqual(JSON.parse(readFileSync(evidencePath, "utf8")).apps.map(({ key }) => key), ["fixture"]);
49
+ });
50
+
51
+ test("release package exposes the local standalone verification lane", () => {
52
+ const pkg = JSON.parse(readFileSync(new URL("./package.json", import.meta.url), "utf8"));
53
+ assert.equal(pkg.scripts["verify:standalone"], "node standalone-clone-verify.mjs");
54
+ });
55
+
56
+ test("standalone verifier rejects a package root that escapes its clone", async (t) => {
57
+ const fixtureRoot = mkdtempSync(path.join(tmpdir(), "rightkit-standalone-escape-"));
58
+ t.after(() => rmSync(fixtureRoot, { recursive: true, force: true }));
59
+ const source = path.join(fixtureRoot, "source");
60
+ mkdirSync(source, { recursive: true });
61
+ writeFileSync(path.join(source, "README.md"), "fixture", "utf8");
62
+ run("git", ["init", "-q"], source);
63
+ run("git", ["config", "user.email", "fixture@example.test"], source);
64
+ run("git", ["config", "user.name", "Fixture"], source);
65
+ run("git", ["add", "."], source);
66
+ run("git", ["commit", "-qm", "fixture"], source);
67
+
68
+ await assert.rejects(
69
+ verifyStandaloneClones({
70
+ apps: [{ key: "fixture", remote: source, appDir: ".." }],
71
+ evidencePath: path.join(fixtureRoot, "evidence.json"),
72
+ tempParent: fixtureRoot,
73
+ }),
74
+ /package root escapes its clone/,
75
+ );
76
+ });