@rightkit/release 0.2.20 → 0.2.22

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,68 @@
1
+ import { isDeepStrictEqual } from "node:util";
2
+
3
+ import { verifyRuntimeArtifactEnvelope } from "./runtime-artifact-manifest.mjs";
4
+
5
+ const ASR_AUTHORITY = "heardright";
6
+ const ASR_CONSUMER = "scraperight";
7
+ const ASR_KIND = "asr-model";
8
+
9
+ export function assertAsrArtifactAdoption({ authorityEnvelope, consumerEnvelope, publicKey, trustedKeyId }) {
10
+ const authority = verifyTrusted(authorityEnvelope, publicKey, trustedKeyId);
11
+ const consumer = verifyTrusted(consumerEnvelope, publicKey, trustedKeyId);
12
+
13
+ assertManifestRole(authority, ASR_AUTHORITY, "authority");
14
+ assertManifestRole(consumer, ASR_CONSUMER, "consumer");
15
+ for (const manifest of [authority, consumer]) {
16
+ if (manifest.artifactKind !== ASR_KIND) throw new Error("ASR adoption manifest must be an asr-model");
17
+ if (manifest.promotion.authorityAppKey !== ASR_AUTHORITY) throw new Error("ASR promotion authority must be heardright");
18
+ }
19
+
20
+ assertSame(authority.target, consumer.target, "target");
21
+ assertSame(authority.object.filename, consumer.object.filename, "object.filename");
22
+ assertSame(authority.object.sha256, consumer.object.sha256, "object.sha256");
23
+ assertSame(authority.object.sizeBytes, consumer.object.sizeBytes, "object.sizeBytes");
24
+ for (const field of ["runtime", "model", "tokenizer", "preprocessing", "license", "provenance"]) {
25
+ assertSame(authority.versions[field], consumer.versions[field], `versions.${field}`);
26
+ }
27
+ assertSame(authority.provenance, consumer.provenance, "provenance");
28
+ assertSame(authority.promotion, consumer.promotion, "promotion");
29
+ return {
30
+ sha256: authority.object.sha256,
31
+ promotionId: authority.promotion.promotionId,
32
+ target: authority.target,
33
+ };
34
+ }
35
+
36
+ export function assertAsrAdapterPair(authority, consumer) {
37
+ assertAdapter(authority, ASR_AUTHORITY, "authority");
38
+ assertAdapter(consumer, ASR_CONSUMER, "consumer");
39
+ for (const [field, expected] of [
40
+ ["authorityAppKey", ASR_AUTHORITY],
41
+ ["artifactKind", ASR_KIND],
42
+ ["delivery", "private-r2"],
43
+ ["entitlementTier", "pro"],
44
+ ]) {
45
+ assertSame(authority[field], expected, `authority adapter ${field}`);
46
+ assertSame(consumer[field], expected, `consumer adapter ${field}`);
47
+ }
48
+ return true;
49
+ }
50
+
51
+ function verifyTrusted(envelope, publicKey, trustedKeyId) {
52
+ if (envelope?.signature?.keyId !== trustedKeyId) throw new Error("untrusted runtime artifact key id");
53
+ return verifyRuntimeArtifactEnvelope(envelope, publicKey);
54
+ }
55
+
56
+ function assertManifestRole(manifest, appKey, role) {
57
+ if (manifest.entitlement.appKey !== appKey) throw new Error(`${role} manifest must belong to ${appKey}`);
58
+ }
59
+
60
+ function assertAdapter(value, appKey, role) {
61
+ if (!value || value.schema !== 1 || value.appKey !== appKey || value.role !== role) {
62
+ throw new Error(`invalid ${role} ASR adapter`);
63
+ }
64
+ }
65
+
66
+ function assertSame(left, right, field) {
67
+ if (!isDeepStrictEqual(left, right)) throw new Error(`ASR drift: ${field}`);
68
+ }
@@ -0,0 +1,122 @@
1
+ import assert from "node:assert/strict";
2
+ import { generateKeyPairSync } from "node:crypto";
3
+ import test from "node:test";
4
+
5
+ import { assertAsrAdapterPair, assertAsrArtifactAdoption } from "./asr-artifact-adoption.mjs";
6
+ import { buildRuntimeArtifactManifest, signRuntimeArtifactManifest } from "./runtime-artifact-manifest.mjs";
7
+
8
+ const digest = "a".repeat(64);
9
+ const notice = "b".repeat(64);
10
+ const evidence = "c".repeat(64);
11
+ const { privateKey, publicKey } = generateKeyPairSync("ed25519");
12
+
13
+ function envelope(appKey, overrides = {}) {
14
+ const manifest = buildRuntimeArtifactManifest({
15
+ artifactKind: "asr-model",
16
+ entitlement: { appKey, tier: "pro" },
17
+ distribution: { delivery: "private-r2", bucket: "rightapps-updates" },
18
+ target: { os: "windows", arch: "x86_64" },
19
+ filename: "parakeet-unified-en-0.6b.tar.zst",
20
+ sha256: digest,
21
+ sizeBytes: 1234,
22
+ versions: {
23
+ runtime: "rightkit-asr@0.1.0",
24
+ model: "parakeet-unified-en-0.6b",
25
+ tokenizer: "sha256:tokenizer-v1",
26
+ preprocessing: "heardright-frontend-v1",
27
+ license: "CC-BY-4.0",
28
+ provenance: "nvidia/parakeet-tdt-0.6b-v3",
29
+ },
30
+ provenance: {
31
+ source: "https://huggingface.co/nvidia/parakeet-tdt-0.6b-v3",
32
+ sourceRevision: "revision-1",
33
+ licenseId: "CC-BY-4.0",
34
+ noticeSha256: notice,
35
+ },
36
+ promotion: {
37
+ authorityAppKey: "heardright",
38
+ promotionId: "hr-asr-2026-07-14",
39
+ promotedAt: "2026-07-14T00:00:00.000Z",
40
+ evidenceSha256: evidence,
41
+ },
42
+ ...overrides,
43
+ });
44
+ return signRuntimeArtifactManifest(manifest, privateKey, "rightkit-runtime-artifacts-2026-07");
45
+ }
46
+
47
+ test("accepts app-scoped pointers with one exact HeardRight-certified ASR identity", () => {
48
+ const result = assertAsrArtifactAdoption({
49
+ authorityEnvelope: envelope("heardright"),
50
+ consumerEnvelope: envelope("scraperight"),
51
+ publicKey,
52
+ trustedKeyId: "rightkit-runtime-artifacts-2026-07",
53
+ });
54
+
55
+ assert.equal(result.sha256, digest);
56
+ assert.equal(result.promotionId, "hr-asr-2026-07-14");
57
+ });
58
+
59
+ test("rejects a validly signed ScrapeRight manifest with ASR drift", () => {
60
+ const drifted = envelope("scraperight", {
61
+ versions: {
62
+ runtime: "rightkit-asr@0.1.1",
63
+ model: "parakeet-unified-en-0.6b",
64
+ tokenizer: "sha256:tokenizer-v1",
65
+ preprocessing: "heardright-frontend-v1",
66
+ license: "CC-BY-4.0",
67
+ provenance: "nvidia/parakeet-tdt-0.6b-v3",
68
+ },
69
+ });
70
+
71
+ assert.throws(() => assertAsrArtifactAdoption({
72
+ authorityEnvelope: envelope("heardright"),
73
+ consumerEnvelope: drifted,
74
+ publicKey,
75
+ trustedKeyId: "rightkit-runtime-artifacts-2026-07",
76
+ }), /ASR drift: versions\.runtime/);
77
+ });
78
+
79
+ test("rejects wrong roles, authority, target, signature key, and artifact kind", () => {
80
+ const base = {
81
+ authorityEnvelope: envelope("heardright"),
82
+ consumerEnvelope: envelope("scraperight"),
83
+ publicKey,
84
+ trustedKeyId: "rightkit-runtime-artifacts-2026-07",
85
+ };
86
+ assert.throws(() => assertAsrArtifactAdoption({ ...base, authorityEnvelope: envelope("scraperight") }), /authority manifest must belong to heardright/);
87
+ assert.throws(() => assertAsrArtifactAdoption({ ...base, consumerEnvelope: envelope("heardright") }), /consumer manifest must belong to scraperight/);
88
+ assert.throws(() => assertAsrArtifactAdoption({ ...base, trustedKeyId: "rotated-without-app-update" }), /untrusted runtime artifact key id/);
89
+ assert.throws(() => assertAsrArtifactAdoption({
90
+ ...base,
91
+ consumerEnvelope: envelope("scraperight", { target: { os: "darwin", arch: "aarch64" } }),
92
+ }), /ASR drift: target/);
93
+ assert.throws(() => assertAsrArtifactAdoption({
94
+ ...base,
95
+ consumerEnvelope: envelope("scraperight", { artifactKind: "ocr-model" }),
96
+ }), /must be an asr-model/);
97
+ });
98
+
99
+ test("rejects a consumer signed by a different key", () => {
100
+ const other = generateKeyPairSync("ed25519");
101
+ const consumer = signRuntimeArtifactManifest(
102
+ envelope("scraperight").manifest,
103
+ other.privateKey,
104
+ "rightkit-runtime-artifacts-2026-07",
105
+ );
106
+ assert.throws(() => assertAsrArtifactAdoption({
107
+ authorityEnvelope: envelope("heardright"),
108
+ consumerEnvelope: consumer,
109
+ publicKey,
110
+ trustedKeyId: "rightkit-runtime-artifacts-2026-07",
111
+ }), /signature verification failed/);
112
+ });
113
+
114
+ test("locks the thin app adapters to HeardRight authority and private Pro delivery", () => {
115
+ const authority = {
116
+ schema: 1, appKey: "heardright", role: "authority", authorityAppKey: "heardright",
117
+ artifactKind: "asr-model", delivery: "private-r2", entitlementTier: "pro",
118
+ };
119
+ const consumer = { ...authority, appKey: "scraperight", role: "consumer" };
120
+ assert.equal(assertAsrAdapterPair(authority, consumer), true);
121
+ assert.throws(() => assertAsrAdapterPair(authority, { ...consumer, authorityAppKey: "scraperight" }), /ASR drift/);
122
+ });
@@ -101,11 +101,34 @@ function assertNoRightKitOverrides(parsed, label, source) {
101
101
  }
102
102
  }
103
103
 
104
+ // `tomllib` is Python 3.11+. Windows pins `py -3.11`, but a bare `python3` on macOS is the system
105
+ // 3.9, which has no tomllib — so every app's release:doctor died on a stock Mac, and reported the
106
+ // manifest as invalid TOML when the real problem was the interpreter. Probe for one that can.
107
+ let tomlRunner;
108
+ function resolveTomlRunner() {
109
+ if (tomlRunner) return tomlRunner;
110
+ const probe = "import sys,tomllib";
111
+ const candidates =
112
+ process.platform === "win32"
113
+ ? [["py", ["-3.11"]], ["py", ["-3"]], ["python", []]]
114
+ : [["python3.13", []], ["python3.12", []], ["python3.11", []], ["python3", []]];
115
+ for (const [command, prefix] of candidates) {
116
+ const check = spawnSync(command, [...prefix, "-c", probe], { encoding: "utf8", windowsHide: true });
117
+ if (check.status === 0) {
118
+ tomlRunner = { command, prefix };
119
+ return tomlRunner;
120
+ }
121
+ }
122
+ throw new Error(
123
+ "no Python with tomllib found (needs 3.11+). Install one, or put it on PATH: the release " +
124
+ "contract reads Cargo manifests through it.",
125
+ );
126
+ }
127
+
104
128
  function readToml(file, label) {
105
129
  const script = "import json,pathlib,sys,tomllib; json.dump(tomllib.loads(pathlib.Path(sys.argv[1]).read_text(encoding='utf-8')),sys.stdout,default=str)";
106
- const command = process.platform === "win32" ? "py" : "python3";
107
- const args = process.platform === "win32" ? ["-3.11", "-c", script, file] : ["-c", script, file];
108
- const result = spawnSync(command, args, { encoding: "utf8", windowsHide: true });
130
+ const { command, prefix } = resolveTomlRunner();
131
+ const result = spawnSync(command, [...prefix, "-c", script, file], { encoding: "utf8", windowsHide: true });
109
132
  if (result.status !== 0) throw new Error(`${label} is not valid TOML: ${String(result.stderr ?? "").trim()}`);
110
133
  return JSON.parse(result.stdout);
111
134
  }
@@ -44,9 +44,18 @@ if (first === "--version" || first === "-v") {
44
44
  const rest = args.slice(1);
45
45
  if (rest[0] === "cargo") {
46
46
  run("publish-cargo.mjs", rest.slice(1));
47
+ } else if (rest[0] === "swift") {
48
+ run("publish-swift.mjs", rest.slice(1));
47
49
  } else {
48
50
  run("release.mjs", rest.includes("--upload") || rest.includes("--publish") ? rest : [...rest, "--upload"]);
49
51
  }
52
+ } else if (first === "model") {
53
+ const rest = args.slice(1);
54
+ if (rest[0] !== "promote") {
55
+ console.error("right-release model: expected promote");
56
+ process.exit(2);
57
+ }
58
+ run("model-promote.mjs", rest.slice(1));
50
59
  } else if (first === "lsclean") {
51
60
  runBinary("bash", [path.join(packageRoot, "lsclean.sh"), ...args.slice(1)]);
52
61
  } else if (first === "generate-dmg-background") {
@@ -113,6 +122,9 @@ Commands:
113
122
  release [--platform mac|win] --tier patch|update Build/package through the signed release lane
114
123
  publish [--platform mac|win] --tier patch|update Release plus R2 upload + RightApps registration
115
124
  publish cargo --crate <name> [--dry-run] Test, inspect, scan, and publish one crates.io package
125
+ publish swift [--scope <s>] [--url <u>] [--dry-run] Test, archive, scan, and publish RightKitSwift to a Swift registry
126
+ model promote --authority heardright --config <file> [--dry-run]
127
+ Sign and promote one runtime/model artifact
116
128
  doctor [--platform mac|win] Inspect one app's release config
117
129
  doctor --all Verify all Right Suite app release contracts
118
130
  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
+ }