@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.
- package/asr-artifact-adoption.mjs +68 -0
- package/asr-artifact-adoption.test.mjs +122 -0
- package/cargo-contract.mjs +26 -3
- package/cli/right-release.mjs +12 -0
- package/model-promote.mjs +262 -0
- package/model-promote.test.mjs +240 -0
- package/package.json +8 -6
- package/publish-swift.mjs +122 -0
- package/publish-swift.test.mjs +44 -0
- package/publish-update.mjs +7 -12
- package/qa-contract.mjs +61 -0
- package/qa-contract.test.mjs +47 -0
- package/release.mjs +30 -0
- package/release.test.mjs +15 -0
- package/right-suite-contract.test.mjs +17 -4
- package/rightapps-register.mjs +31 -0
- package/rightapps-register.test.mjs +28 -0
- package/rightkit-versions.json +11 -2
- package/runtime-artifact-manifest.mjs +247 -0
- package/runtime-artifact-manifest.test.mjs +128 -0
- package/standalone-clone-verify.mjs +131 -0
- package/standalone-clone-verify.test.mjs +76 -0
|
@@ -10,8 +10,13 @@ import {
|
|
|
10
10
|
assertPublishedRightKitCargoDependencies,
|
|
11
11
|
validateRightKitCargoContract,
|
|
12
12
|
} from "./cargo-contract.mjs";
|
|
13
|
+
import { assertAsrAdapterPair } from "./asr-artifact-adoption.mjs";
|
|
13
14
|
|
|
14
15
|
const workspace = path.resolve(new URL("../../../..", import.meta.url).pathname.replace(/^\/(\w:)/, "$1"));
|
|
16
|
+
// The Right Suite web layer's repo + filesystem path is `rightsites` post the 2026-07-15 naming lock;
|
|
17
|
+
// a not-yet-renamed checkout may still have `rightapps`. Resolve whichever exists so the contract
|
|
18
|
+
// passes on both. (The runtime service namespace stays `rightapps` — that is deliberately untouched.)
|
|
19
|
+
const siteRepoDir = existsSync(path.join(workspace, "rightsites")) ? "rightsites" : "rightapps";
|
|
15
20
|
const versionsPath = path.join(path.dirname(new URL(import.meta.url).pathname.replace(/^\/(\w:)/, "$1")), "rightkit-versions.json");
|
|
16
21
|
const versions = JSON.parse(readFileSync(versionsPath, "utf8"));
|
|
17
22
|
const isolatedCargoHome = mkdtempSync(path.join(tmpdir(), "rightkit-cargo-home-"));
|
|
@@ -21,7 +26,9 @@ const apps = [
|
|
|
21
26
|
{ key: "viewright", root: "viewright", tauri: "src-tauri/tauri.conf.json", releaseFiles: ["scripts/build-mac-notarized.sh"] },
|
|
22
27
|
{ key: "scraperight", root: "scraperight", tauri: "src-tauri/tauri.conf.json", releaseFiles: ["package.sh"] },
|
|
23
28
|
{ key: "heardright", root: "heardright/tauri-app-next", repoRoot: "heardright", tauri: "src-tauri/tauri.conf.json", releaseFiles: ["scripts/mac-dmg.mjs", "scripts/publish-release.mjs"] },
|
|
24
|
-
|
|
29
|
+
// mac-dmg.mjs is the release file, as in HeardRight/CodeRight. build-mac.sh only delegates to it,
|
|
30
|
+
// so asserting the mirror call against the wrapper failed a pipeline that does mirror.
|
|
31
|
+
{ key: "mailright", root: "mailright", tauri: "src-tauri/tauri.conf.json", releaseFiles: ["scripts/mac-dmg.mjs"] },
|
|
25
32
|
{ key: "coderight", root: "coderight/apps/coderight-tauri", tauri: "src-tauri/tauri.conf.json", releaseFiles: ["scripts/mac-dmg.mjs"] },
|
|
26
33
|
];
|
|
27
34
|
|
|
@@ -310,7 +317,7 @@ test("Cargo version contract rejects staged versions that mismatch canonical man
|
|
|
310
317
|
test("RightKit exposes one current version manifest", () => {
|
|
311
318
|
assert.equal(existsSync(versionsPath), true, "rightkit-versions.json must be the single current-version source");
|
|
312
319
|
assert.match(versions.packageManager, /^pnpm@\d+\.\d+\.\d+$/);
|
|
313
|
-
assert.equal(versions.npm["@rightkit/release"], "0.2.
|
|
320
|
+
assert.equal(versions.npm["@rightkit/release"], "0.2.22");
|
|
314
321
|
assert.equal(versions.npm["@rightkit/license"], "0.1.5");
|
|
315
322
|
assert.equal(versions.npm["@rightkit/logs"], "0.1.3");
|
|
316
323
|
assert.equal(versions.npm["@rightkit/tauri"], "0.1.0");
|
|
@@ -329,7 +336,7 @@ test("license v2 public vector is identical at every portable consumer boundary"
|
|
|
329
336
|
);
|
|
330
337
|
for (const relativePath of [
|
|
331
338
|
"tools/rightkit/packages/license/test-vectors/license-v2.json",
|
|
332
|
-
|
|
339
|
+
`${siteRepoDir}/packages/api/src/licensing/test-vectors/license-v2.json`,
|
|
333
340
|
"scraperight/tests/fixtures/license-v2.json",
|
|
334
341
|
]) {
|
|
335
342
|
assert.equal(
|
|
@@ -450,6 +457,12 @@ for (const app of apps) {
|
|
|
450
457
|
});
|
|
451
458
|
}
|
|
452
459
|
|
|
460
|
+
test("HeardRight and ScrapeRight expose one locked ASR promotion adapter contract", async () => {
|
|
461
|
+
const heard = (await import(`${pathToFileURL(path.join(workspace, "heardright/tauri-app-next/right-release.config.mjs"))}?asr=${Date.now()}`)).default;
|
|
462
|
+
const scrape = (await import(`${pathToFileURL(path.join(workspace, "scraperight/right-release.config.mjs"))}?asr=${Date.now()}`)).default;
|
|
463
|
+
assert.equal(assertAsrAdapterPair(heard.runtimeArtifacts?.asr, scrape.runtimeArtifacts?.asr), true);
|
|
464
|
+
});
|
|
465
|
+
|
|
453
466
|
function findFiles(root, filename) {
|
|
454
467
|
const found = [];
|
|
455
468
|
const visit = (dir) => {
|
|
@@ -488,7 +501,7 @@ test("Right Suite has no hosted workflow files", () => {
|
|
|
488
501
|
|
|
489
502
|
test("RightApps brand hosts expose app-keyed update manifest proxies", () => {
|
|
490
503
|
for (const app of apps) {
|
|
491
|
-
const siteRoot = path.join(workspace,
|
|
504
|
+
const siteRoot = path.join(workspace, siteRepoDir, app.key);
|
|
492
505
|
for (const route of ["patches", "latest"]) {
|
|
493
506
|
const routeFile = path.join(siteRoot, "src", "routes", "releases", `${route}.json`, "index.ts");
|
|
494
507
|
const source = readFileSync(routeFile, "utf8");
|
|
@@ -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
|
+
});
|
package/rightkit-versions.json
CHANGED
|
@@ -4,14 +4,23 @@
|
|
|
4
4
|
"npm": {
|
|
5
5
|
"@rightkit/license": "0.1.5",
|
|
6
6
|
"@rightkit/logs": "0.1.3",
|
|
7
|
-
"@rightkit/
|
|
7
|
+
"@rightkit/platform-ui": "0.1.0",
|
|
8
|
+
"@rightkit/qa": "0.1.0",
|
|
9
|
+
"@rightkit/release": "0.2.22",
|
|
8
10
|
"@rightkit/tauri": "0.1.0",
|
|
9
11
|
"@rightkit/updates": "0.2.3"
|
|
10
12
|
},
|
|
11
13
|
"cargo": {
|
|
12
14
|
"rightkit-license": "0.1.2",
|
|
13
15
|
"rightkit-logs": "0.1.0",
|
|
16
|
+
"rightkit-process": "0.1.0",
|
|
14
17
|
"rightkit-tauri": "0.1.0"
|
|
15
18
|
},
|
|
16
|
-
"stagedCargo": {}
|
|
19
|
+
"stagedCargo": {},
|
|
20
|
+
"swift": {
|
|
21
|
+
"rightkit-swift": {
|
|
22
|
+
"url": "https://github.com/bogusyogi/rightkit-swift.git",
|
|
23
|
+
"version": "0.1.0"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
17
26
|
}
|
|
@@ -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
|
+
}
|