@rightkit/release 0.2.19 → 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.
- package/cargo-contract.mjs +159 -0
- package/cli/right-release.mjs +9 -0
- package/model-promote.mjs +262 -0
- package/model-promote.test.mjs +240 -0
- package/package.json +3 -2
- package/publish-update.mjs +7 -12
- package/release.mjs +2 -0
- package/release.test.mjs +64 -2
- package/right-suite-contract.test.mjs +315 -22
- package/rightapps-register.mjs +31 -0
- package/rightapps-register.test.mjs +28 -0
- package/rightkit-versions.json +7 -4
- 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
|
@@ -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
|
+
});
|