@rightkit/release 0.2.70 → 0.2.72
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/build-release.mjs +19 -14
- package/cache-command.mjs +7 -4
- package/cli/right-release.mjs +0 -0
- package/direct-bootstrap.mjs +482 -0
- package/github-release.mjs +160 -2
- package/hardening-evidence.mjs +54 -0
- package/hardeningscan.mjs +78 -55
- package/native-cargo-layout.mjs +239 -0
- package/native-release-finalization.mjs +259 -0
- package/package.json +9 -11
- package/preflight.mjs +6 -3
- package/registry-parity.mjs +2 -2
- package/release-invocation.mjs +10 -2
- package/release-state.mjs +5 -2
- package/release.mjs +235 -16
- package/rightkit-versions.json +8 -3
- package/sign-release-manifest.mjs +223 -0
- package/supply-chain-evidence.mjs +175 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
const DEFAULT_MANIFEST = path.join("src-tauri", "Cargo.toml");
|
|
5
|
+
const LOCK_SOURCES = new Set(["manifest", "workspace", "root"]);
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Resolve every path which is derived from a native Cargo manifest in one
|
|
9
|
+
* place. RightRelease historically assumed src-tauri; retaining that
|
|
10
|
+
* default keeps existing applications source-compatible while allowing a
|
|
11
|
+
* workspace package to opt into its real manifest.
|
|
12
|
+
*/
|
|
13
|
+
export function resolveNativeCargoLayout({
|
|
14
|
+
appRoot,
|
|
15
|
+
config,
|
|
16
|
+
nativeAssembly = config?.nativeAssembly,
|
|
17
|
+
exists = existsSync,
|
|
18
|
+
read = (file) => readFileSync(file, "utf8"),
|
|
19
|
+
requireManifest = false,
|
|
20
|
+
requireLock = false,
|
|
21
|
+
} = {}) {
|
|
22
|
+
if (!appRoot) throw new TypeError("native Cargo layout requires appRoot");
|
|
23
|
+
const resolvedAppRoot = path.resolve(appRoot);
|
|
24
|
+
const declaredManifest = nativeAssembly?.cargoManifest ?? nativeAssembly?.manifest;
|
|
25
|
+
const manifestPath = resolveDeclaredPath(resolvedAppRoot, declaredManifest || DEFAULT_MANIFEST);
|
|
26
|
+
if (requireManifest && !exists(manifestPath)) throw new Error(`native Cargo manifest missing: ${manifestPath}`);
|
|
27
|
+
|
|
28
|
+
const manifestText = readIfPresent(manifestPath, exists, read);
|
|
29
|
+
const manifestDir = path.dirname(manifestPath);
|
|
30
|
+
const parsed = parseCargoManifest(manifestText);
|
|
31
|
+
const explicitWorkspace = nativeAssembly?.workspaceRoot ?? nativeAssembly?.cargoWorkspaceRoot;
|
|
32
|
+
const workspaceRoot = explicitWorkspace
|
|
33
|
+
? normalizeWorkspaceRoot(resolveDeclaredPath(resolvedAppRoot, explicitWorkspace), exists)
|
|
34
|
+
: discoverWorkspaceRoot({ appRoot: resolvedAppRoot, manifestPath, manifestDir, manifestText, exists, read });
|
|
35
|
+
|
|
36
|
+
const lock = resolveCargoLock({
|
|
37
|
+
appRoot: resolvedAppRoot,
|
|
38
|
+
manifestPath,
|
|
39
|
+
manifestDir,
|
|
40
|
+
workspaceRoot,
|
|
41
|
+
nativeAssembly,
|
|
42
|
+
exists,
|
|
43
|
+
});
|
|
44
|
+
if (requireLock && !lock.path) {
|
|
45
|
+
throw new Error(`native Cargo.lock missing (checked ${lock.candidates.join(", ")})`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const targetDeclaration = nativeAssembly?.targetDir
|
|
49
|
+
?? nativeAssembly?.targetRoot
|
|
50
|
+
?? nativeAssembly?.cargoTargetDir;
|
|
51
|
+
const targetLink = resolveDeclaredPath(resolvedAppRoot, targetDeclaration || path.join(relativeFromRoot(resolvedAppRoot, workspaceRoot), "target"));
|
|
52
|
+
const targetPrefix = relativePrefix(resolvedAppRoot, targetLink);
|
|
53
|
+
const manifestPrefix = relativePrefix(resolvedAppRoot, manifestDir);
|
|
54
|
+
const workspacePrefix = relativePrefix(resolvedAppRoot, workspaceRoot);
|
|
55
|
+
const targetPrefixes = [...new Set([
|
|
56
|
+
targetPrefix,
|
|
57
|
+
manifestPrefix ? `${manifestPrefix}target/` : "",
|
|
58
|
+
workspacePrefix ? `${workspacePrefix}target/` : "",
|
|
59
|
+
].filter(Boolean))].sort((left, right) => right.length - left.length);
|
|
60
|
+
const layout = {
|
|
61
|
+
appRoot: resolvedAppRoot,
|
|
62
|
+
manifestPath,
|
|
63
|
+
cargoManifest: manifestPath,
|
|
64
|
+
manifestDir,
|
|
65
|
+
manifestPrefix,
|
|
66
|
+
workspaceRoot,
|
|
67
|
+
cargoWorkspaceRoot: workspaceRoot,
|
|
68
|
+
workspacePrefix,
|
|
69
|
+
lockPath: lock.path,
|
|
70
|
+
cargoLock: lock.path,
|
|
71
|
+
lockSource: lock.source,
|
|
72
|
+
lockCandidates: lock.candidates,
|
|
73
|
+
targetRoot: targetLink,
|
|
74
|
+
targetDir: targetLink,
|
|
75
|
+
targetLink,
|
|
76
|
+
targetPrefix,
|
|
77
|
+
targetPrefixes,
|
|
78
|
+
targetRootPrefix: targetPrefix,
|
|
79
|
+
native: Boolean(nativeAssembly),
|
|
80
|
+
fallback: !declaredManifest,
|
|
81
|
+
manifest: parsed,
|
|
82
|
+
};
|
|
83
|
+
layout.resolveArtifactPath = (file) => resolveNativeArtifactPath(layout, file);
|
|
84
|
+
return layout;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export const resolveCargoLayout = resolveNativeCargoLayout;
|
|
88
|
+
export const resolveNativeCargoManifest = resolveNativeCargoLayout;
|
|
89
|
+
export const resolveCargoManifest = resolveNativeCargoLayout;
|
|
90
|
+
|
|
91
|
+
export function resolveNativeArtifactPath(layout, file) {
|
|
92
|
+
if (!layout?.appRoot) throw new TypeError("native artifact resolution requires a Cargo layout");
|
|
93
|
+
if (!file) throw new TypeError("native artifact path is required");
|
|
94
|
+
const value = String(file).replaceAll("\\", "/");
|
|
95
|
+
const targetPrefixes = layout.targetPrefixes ?? [layout.targetPrefix];
|
|
96
|
+
for (const prefixValue of targetPrefixes) {
|
|
97
|
+
const targetPrefix = stripTrailingSlash(prefixValue || "");
|
|
98
|
+
if (targetPrefix && (value === targetPrefix || value.startsWith(`${targetPrefix}/`))) {
|
|
99
|
+
return path.join(layout.targetRoot, value.slice(targetPrefix.length).replace(/^\/+/, ""));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return path.resolve(layout.appRoot, file);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function parseCargoManifest(text = "") {
|
|
106
|
+
const cleaned = String(text)
|
|
107
|
+
.replace(/\/\/.*$/gm, "")
|
|
108
|
+
.replace(/#.*$/gm, "");
|
|
109
|
+
const workspaceSection = section(cleaned, "workspace");
|
|
110
|
+
const packageSection = section(cleaned, "package");
|
|
111
|
+
return {
|
|
112
|
+
hasWorkspace: workspaceSection != null,
|
|
113
|
+
workspacePath: stringValue(workspaceSection?.workspace),
|
|
114
|
+
packageWorkspace: stringValue(packageSection?.workspace),
|
|
115
|
+
packageName: stringValue(packageSection?.name),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function findCargoLock({ manifestPath, appRoot, workspaceRoot, nativeAssembly, exists = existsSync } = {}) {
|
|
120
|
+
const resolvedAppRoot = path.resolve(appRoot || path.dirname(manifestPath || DEFAULT_MANIFEST));
|
|
121
|
+
const resolvedManifest = path.resolve(manifestPath || path.join(resolvedAppRoot, DEFAULT_MANIFEST));
|
|
122
|
+
const manifestDir = path.dirname(resolvedManifest);
|
|
123
|
+
return resolveCargoLock({
|
|
124
|
+
appRoot: resolvedAppRoot,
|
|
125
|
+
manifestPath: resolvedManifest,
|
|
126
|
+
manifestDir,
|
|
127
|
+
workspaceRoot: path.resolve(workspaceRoot || manifestDir),
|
|
128
|
+
nativeAssembly,
|
|
129
|
+
exists,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export const resolveCargoLockPath = findCargoLock;
|
|
134
|
+
|
|
135
|
+
function resolveCargoLock({ appRoot, manifestPath, manifestDir, workspaceRoot, nativeAssembly, exists }) {
|
|
136
|
+
const declaredPath = nativeAssembly?.cargoLockPath
|
|
137
|
+
?? nativeAssembly?.cargoLockFile
|
|
138
|
+
?? nativeAssembly?.lockfilePath
|
|
139
|
+
?? nativeAssembly?.lockPath;
|
|
140
|
+
const declared = nativeAssembly?.cargoLock ?? nativeAssembly?.lockfile ?? nativeAssembly?.lockSource;
|
|
141
|
+
const sourceDeclaration = nativeAssembly?.cargoLockSource ?? nativeAssembly?.lockSource;
|
|
142
|
+
const source = normalizeLockSource(sourceDeclaration ?? (typeof declared === "string" && LOCK_SOURCES.has(declared) ? declared : undefined));
|
|
143
|
+
if (declaredPath || (declared && !LOCK_SOURCES.has(String(declared)))) {
|
|
144
|
+
const explicit = declaredPath || declared;
|
|
145
|
+
const explicitPath = resolveDeclaredPath(appRoot, explicit);
|
|
146
|
+
return { path: exists(explicitPath) ? explicitPath : undefined, source: "explicit", candidates: [explicitPath] };
|
|
147
|
+
}
|
|
148
|
+
const roots = {
|
|
149
|
+
manifest: manifestDir,
|
|
150
|
+
workspace: workspaceRoot,
|
|
151
|
+
root: appRoot,
|
|
152
|
+
};
|
|
153
|
+
const orderedSources = source ? [source] : [workspaceRoot !== manifestDir ? "workspace" : "manifest", "manifest", "root"];
|
|
154
|
+
const candidates = [];
|
|
155
|
+
for (const candidateSource of orderedSources) {
|
|
156
|
+
const candidate = path.join(roots[candidateSource], "Cargo.lock");
|
|
157
|
+
if (!candidates.includes(candidate)) candidates.push(candidate);
|
|
158
|
+
}
|
|
159
|
+
const found = candidates.find((candidate) => exists(candidate));
|
|
160
|
+
return { path: found, source: found ? orderedSources[candidates.indexOf(found)] : undefined, candidates };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function discoverWorkspaceRoot({ appRoot, manifestPath, manifestDir, manifestText, exists, read }) {
|
|
164
|
+
const parsed = parseCargoManifest(manifestText);
|
|
165
|
+
const declaredWorkspace = parsed.workspacePath ?? parsed.packageWorkspace;
|
|
166
|
+
if (declaredWorkspace) {
|
|
167
|
+
return normalizeWorkspaceRoot(resolveDeclaredPath(manifestDir, declaredWorkspace), exists);
|
|
168
|
+
}
|
|
169
|
+
if (parsed.hasWorkspace) return manifestDir;
|
|
170
|
+
let current = manifestDir;
|
|
171
|
+
for (;;) {
|
|
172
|
+
const candidate = path.join(current, "Cargo.toml");
|
|
173
|
+
if (candidate !== manifestPath && exists(candidate) && parseCargoManifest(readIfPresent(candidate, exists, read)).hasWorkspace) return current;
|
|
174
|
+
if (current === appRoot) break;
|
|
175
|
+
const parent = path.dirname(current);
|
|
176
|
+
if (parent === current || !isWithin(appRoot, parent)) break;
|
|
177
|
+
current = parent;
|
|
178
|
+
}
|
|
179
|
+
return manifestDir;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function normalizeWorkspaceRoot(candidate, exists) {
|
|
183
|
+
const resolved = path.resolve(candidate);
|
|
184
|
+
if (path.basename(resolved).toLowerCase() === "cargo.toml") return path.dirname(resolved);
|
|
185
|
+
if (exists(resolved) && !path.extname(resolved)) return resolved;
|
|
186
|
+
if (exists(path.join(resolved, "Cargo.toml"))) return resolved;
|
|
187
|
+
return resolved;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function section(text, name) {
|
|
191
|
+
const expression = new RegExp(`(?:^|\\n)\\s*\\[${escapeRegExp(name)}\\]\\s*\\n([\\s\\S]*?)(?=\\n\\s*\\[|$)`, "m");
|
|
192
|
+
const match = text.match(expression);
|
|
193
|
+
if (!match) return undefined;
|
|
194
|
+
const values = {};
|
|
195
|
+
for (const line of match[1].split("\n")) {
|
|
196
|
+
const assignment = line.match(/^\s*([A-Za-z0-9_-]+)\s*=\s*(.*?)\s*$/);
|
|
197
|
+
if (assignment) values[assignment[1]] = assignment[2];
|
|
198
|
+
}
|
|
199
|
+
return values;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function stringValue(value) {
|
|
203
|
+
const match = String(value ?? "").match(/^['\"]([^'\"]+)['\"]$/);
|
|
204
|
+
return match?.[1];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function normalizeLockSource(value) {
|
|
208
|
+
if (value == null) return undefined;
|
|
209
|
+
const normalized = String(value).toLowerCase();
|
|
210
|
+
return LOCK_SOURCES.has(normalized) ? normalized : undefined;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function resolveDeclaredPath(root, value) {
|
|
214
|
+
if (path.isAbsolute(String(value))) return path.normalize(String(value));
|
|
215
|
+
return path.resolve(root, String(value));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function relativeFromRoot(root, candidate) {
|
|
219
|
+
const relative = path.relative(root, candidate);
|
|
220
|
+
return relative || ".";
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function relativePrefix(root, candidate) {
|
|
224
|
+
const relative = path.relative(root, candidate).replaceAll("\\", "/");
|
|
225
|
+
return relative && relative !== "." ? `${relative.replace(/\/+$/, "")}/` : "";
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function stripTrailingSlash(value) { return String(value).replace(/\/+$/, ""); }
|
|
229
|
+
|
|
230
|
+
function readIfPresent(file, exists, read) {
|
|
231
|
+
try { return exists(file) ? read(file) : ""; } catch { return ""; }
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function isWithin(root, candidate) {
|
|
235
|
+
const relative = path.relative(root, candidate);
|
|
236
|
+
return relative === "" || (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function escapeRegExp(value) { return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); }
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
export const NATIVE_FINALIZATION_SCHEMA = 1;
|
|
6
|
+
export const NATIVE_PROVENANCE_SCHEME = "rightkit-release";
|
|
7
|
+
|
|
8
|
+
const PLACEHOLDER = /(?:placeholder|fabricat|pending|unknown|example|dummy|todo|changeme)/i;
|
|
9
|
+
const STATUS = new Set(["complete", "completed", "pass", "passed", "verified"]);
|
|
10
|
+
|
|
11
|
+
export function sha256Bytes(value) {
|
|
12
|
+
return createHash("sha256").update(value).digest("hex");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function sha256File(file) {
|
|
16
|
+
return sha256Bytes(readFileSync(file));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function normalizeDigest(value, label = "digest") {
|
|
20
|
+
const digest = String(value ?? "").toLowerCase().replace(/^sha256:/, "");
|
|
21
|
+
if (!/^[0-9a-f]{64}$/.test(digest)) throw new Error(`${label} must be a SHA-256 hex digest`);
|
|
22
|
+
return digest;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function canonicalSignedArtifacts(artifacts = []) {
|
|
26
|
+
if (!Array.isArray(artifacts)) throw new TypeError("signedArtifacts must be an array");
|
|
27
|
+
return artifacts.map((artifact) => {
|
|
28
|
+
if (typeof artifact === "string") return { path: artifact };
|
|
29
|
+
if (!artifact || typeof artifact !== "object") throw new TypeError("signed artifact must be a path or object");
|
|
30
|
+
const value = { ...artifact };
|
|
31
|
+
if (!value.path && value.file) value.path = value.file;
|
|
32
|
+
if (!value.path) throw new Error("signed artifact path is required");
|
|
33
|
+
if (value.sha256 != null) value.sha256 = normalizeDigest(value.sha256, `signed artifact ${value.path}`);
|
|
34
|
+
if (value.sizeBytes != null && (!Number.isSafeInteger(value.sizeBytes) || value.sizeBytes < 0)) {
|
|
35
|
+
throw new Error(`signed artifact size must be a non-negative integer: ${value.path}`);
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
path: String(value.path).replaceAll("\\", "/"),
|
|
39
|
+
...(value.sha256 ? { sha256: value.sha256 } : {}),
|
|
40
|
+
...(value.sizeBytes != null ? { sizeBytes: value.sizeBytes } : {}),
|
|
41
|
+
};
|
|
42
|
+
}).sort((left, right) => left.path.localeCompare(right.path));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function canonicalPackageIdentity(identity = {}) {
|
|
46
|
+
if (typeof identity === "string") identity = { name: identity };
|
|
47
|
+
if (!identity || typeof identity !== "object" || Array.isArray(identity)) throw new TypeError("package identity must be an object");
|
|
48
|
+
const result = {};
|
|
49
|
+
for (const key of ["name", "path", "kind", "version", "target", "platform", "architecture"]) {
|
|
50
|
+
if (identity[key] != null) result[key] = String(identity[key]).replaceAll("\\", "/");
|
|
51
|
+
}
|
|
52
|
+
if (Array.isArray(identity.artifacts)) {
|
|
53
|
+
result.artifacts = identity.artifacts.map((value) => String(value).replaceAll("\\", "/")).sort();
|
|
54
|
+
}
|
|
55
|
+
if (!Object.keys(result).length) throw new Error("package identity must include a stable name, path, kind, or artifact list");
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Mint identity from signed files and package identity. Archive bytes are
|
|
61
|
+
* included when available; before packaging, identity remains bound to the
|
|
62
|
+
* signed artifact set plus stable package identity and is completed later by
|
|
63
|
+
* createNativeFinalizationReceipt.
|
|
64
|
+
*/
|
|
65
|
+
export function mintNativeProvenance({
|
|
66
|
+
app,
|
|
67
|
+
version,
|
|
68
|
+
platform,
|
|
69
|
+
architecture,
|
|
70
|
+
targetTriple,
|
|
71
|
+
signedArtifacts = [],
|
|
72
|
+
packageIdentity,
|
|
73
|
+
archiveSha256,
|
|
74
|
+
} = {}) {
|
|
75
|
+
const identity = canonicalPackageIdentity(packageIdentity);
|
|
76
|
+
const signed = canonicalSignedArtifacts(signedArtifacts);
|
|
77
|
+
if (!app || !version || !platform || !architecture) throw new Error("native provenance identity is incomplete");
|
|
78
|
+
const archive = archiveSha256 == null ? undefined : normalizeDigest(archiveSha256, "archiveSha256");
|
|
79
|
+
const payload = {
|
|
80
|
+
app: String(app),
|
|
81
|
+
version: String(version),
|
|
82
|
+
platform: String(platform),
|
|
83
|
+
architecture: String(architecture),
|
|
84
|
+
...(targetTriple ? { targetTriple: String(targetTriple) } : {}),
|
|
85
|
+
signedArtifacts: signed,
|
|
86
|
+
packageIdentity: identity,
|
|
87
|
+
...(archive ? { archiveSha256: archive } : {}),
|
|
88
|
+
};
|
|
89
|
+
const digest = sha256Bytes(JSON.stringify(payload));
|
|
90
|
+
const pathParts = [app, version, platform, architecture].map((value) => encodeURIComponent(String(value)));
|
|
91
|
+
return `${NATIVE_PROVENANCE_SCHEME}://${pathParts.join("/")}/${digest}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function validateNativeProvenance(value, { expectedScheme = NATIVE_PROVENANCE_SCHEME } = {}) {
|
|
95
|
+
const provenance = String(value ?? "");
|
|
96
|
+
if (!provenance.startsWith(`${expectedScheme}://`)) {
|
|
97
|
+
throw new Error(`native finalizer provenance must use ${expectedScheme}://`);
|
|
98
|
+
}
|
|
99
|
+
if (PLACEHOLDER.test(provenance)) throw new Error("native finalizer returned placeholder provenance");
|
|
100
|
+
const digest = provenance.slice(provenance.lastIndexOf("/") + 1);
|
|
101
|
+
normalizeDigest(digest, "native provenance identity");
|
|
102
|
+
return provenance;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function finalizationArchive(file, { root } = {}) {
|
|
106
|
+
const resolved = root && !path.isAbsolute(String(file)) ? path.resolve(root, file) : path.resolve(String(file));
|
|
107
|
+
if (!existsSync(resolved)) throw new Error(`native finalization archive missing: ${resolved}`);
|
|
108
|
+
const stat = statSync(resolved);
|
|
109
|
+
if (!stat.isFile()) throw new Error(`native finalization archive is not a file: ${resolved}`);
|
|
110
|
+
return {
|
|
111
|
+
path: root ? path.relative(root, resolved).replaceAll("\\", "/") : resolved.replaceAll("\\", "/"),
|
|
112
|
+
sha256: sha256File(resolved),
|
|
113
|
+
sizeBytes: stat.size,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function createNativeFinalizationReceipt({
|
|
118
|
+
app,
|
|
119
|
+
version,
|
|
120
|
+
platform,
|
|
121
|
+
architecture,
|
|
122
|
+
targetTriple,
|
|
123
|
+
signedArtifacts = [],
|
|
124
|
+
packageIdentity,
|
|
125
|
+
archive,
|
|
126
|
+
archivePath,
|
|
127
|
+
archiveSha256,
|
|
128
|
+
finalizerOutput,
|
|
129
|
+
root,
|
|
130
|
+
now = new Date().toISOString(),
|
|
131
|
+
} = {}) {
|
|
132
|
+
const identity = canonicalPackageIdentity(packageIdentity);
|
|
133
|
+
const signed = canonicalSignedArtifacts(signedArtifacts);
|
|
134
|
+
let resolvedArchive = archive;
|
|
135
|
+
if (!resolvedArchive && archivePath) resolvedArchive = finalizationArchive(archivePath, { root });
|
|
136
|
+
if (resolvedArchive) {
|
|
137
|
+
const archiveFile = resolvedArchive.path || resolvedArchive.file;
|
|
138
|
+
const digest = resolvedArchive.sha256 ?? archiveSha256;
|
|
139
|
+
if (!digest) throw new Error("native finalization archive digest is required");
|
|
140
|
+
if (archiveFile && root) {
|
|
141
|
+
const observed = finalizationArchive(archiveFile, { root });
|
|
142
|
+
if (normalizeDigest(digest, "archiveSha256") !== observed.sha256) throw new Error("native finalization archive digest does not match archive bytes");
|
|
143
|
+
resolvedArchive = observed;
|
|
144
|
+
}
|
|
145
|
+
resolvedArchive = {
|
|
146
|
+
...resolvedArchive,
|
|
147
|
+
...(archiveFile ? { path: String(archiveFile).replaceAll("\\", "/") } : {}),
|
|
148
|
+
sha256: normalizeDigest(digest, "archiveSha256"),
|
|
149
|
+
...(resolvedArchive.sizeBytes != null ? { sizeBytes: resolvedArchive.sizeBytes } : {}),
|
|
150
|
+
};
|
|
151
|
+
} else if (archiveSha256) {
|
|
152
|
+
resolvedArchive = { sha256: normalizeDigest(archiveSha256, "archiveSha256") };
|
|
153
|
+
} else {
|
|
154
|
+
throw new Error("native finalization receipt requires final archive digest");
|
|
155
|
+
}
|
|
156
|
+
const provenance = mintNativeProvenance({
|
|
157
|
+
app,
|
|
158
|
+
version,
|
|
159
|
+
platform,
|
|
160
|
+
architecture,
|
|
161
|
+
targetTriple,
|
|
162
|
+
signedArtifacts: signed,
|
|
163
|
+
packageIdentity: identity,
|
|
164
|
+
archiveSha256: resolvedArchive.sha256,
|
|
165
|
+
});
|
|
166
|
+
const output = finalizerOutput && typeof finalizerOutput === "object" ? finalizerOutput : {};
|
|
167
|
+
if (output.provenance != null) validateNativeProvenance(output.provenance);
|
|
168
|
+
if (output.signedArtifacts != null || output.signed != null) {
|
|
169
|
+
const observed = canonicalSignedArtifacts(output.signedArtifacts ?? output.signed);
|
|
170
|
+
if (JSON.stringify(observed) !== JSON.stringify(signed)) throw new Error("native finalizer signed artifact identity mismatch");
|
|
171
|
+
}
|
|
172
|
+
if (output.packageIdentity != null || output.package != null) {
|
|
173
|
+
const observed = canonicalPackageIdentity(output.packageIdentity ?? output.package);
|
|
174
|
+
if (JSON.stringify(observed) !== JSON.stringify(identity)) throw new Error("native finalizer package identity mismatch");
|
|
175
|
+
}
|
|
176
|
+
const outputArchiveDigest = output.archiveSha256 ?? output.archive?.sha256 ?? output.archive?.digest;
|
|
177
|
+
if (outputArchiveDigest != null && normalizeDigest(outputArchiveDigest, "finalizer archiveSha256") !== resolvedArchive.sha256) {
|
|
178
|
+
throw new Error("native finalizer archive digest does not match final archive");
|
|
179
|
+
}
|
|
180
|
+
const receipt = {
|
|
181
|
+
schema: NATIVE_FINALIZATION_SCHEMA,
|
|
182
|
+
kind: "rightkit-native-finalization",
|
|
183
|
+
status: "verified",
|
|
184
|
+
app: String(app),
|
|
185
|
+
version: String(version),
|
|
186
|
+
platform: String(platform),
|
|
187
|
+
architecture: String(architecture),
|
|
188
|
+
...(targetTriple ? { targetTriple: String(targetTriple) } : {}),
|
|
189
|
+
provenance,
|
|
190
|
+
signedArtifacts: signed,
|
|
191
|
+
packageIdentity: identity,
|
|
192
|
+
archive: resolvedArchive,
|
|
193
|
+
archiveSha256: resolvedArchive.sha256,
|
|
194
|
+
finalizedAt: now,
|
|
195
|
+
};
|
|
196
|
+
validateNativeFinalizationOutput(receipt, { requireArchive: true });
|
|
197
|
+
return receipt;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function validateNativeFinalizationOutput(output, {
|
|
201
|
+
expectedProvenance,
|
|
202
|
+
expectedArchiveSha256,
|
|
203
|
+
expectedSignedArtifacts,
|
|
204
|
+
expectedPackageIdentity,
|
|
205
|
+
requireArchive = false,
|
|
206
|
+
} = {}) {
|
|
207
|
+
if (!output || typeof output !== "object" || Array.isArray(output)) throw new Error("native finalizer output must be an object");
|
|
208
|
+
if (output.schema != null && output.schema !== NATIVE_FINALIZATION_SCHEMA) throw new Error(`unsupported native finalizer schema: ${output.schema}`);
|
|
209
|
+
const status = String(output.status ?? "").toLowerCase();
|
|
210
|
+
if (!STATUS.has(status)) throw new Error("native finalizer output must have complete, pass, or verified status");
|
|
211
|
+
const provenance = validateNativeProvenance(output.provenance);
|
|
212
|
+
if (expectedProvenance && provenance !== expectedProvenance) throw new Error("native finalizer provenance identity mismatch");
|
|
213
|
+
const signed = canonicalSignedArtifacts(output.signedArtifacts ?? output.signed ?? []);
|
|
214
|
+
if (expectedSignedArtifacts) {
|
|
215
|
+
const expected = canonicalSignedArtifacts(expectedSignedArtifacts);
|
|
216
|
+
if (JSON.stringify(signed) !== JSON.stringify(expected)) throw new Error("native finalizer signed artifact identity mismatch");
|
|
217
|
+
}
|
|
218
|
+
if (expectedPackageIdentity) {
|
|
219
|
+
const actual = canonicalPackageIdentity(output.packageIdentity ?? output.package ?? {});
|
|
220
|
+
const expected = canonicalPackageIdentity(expectedPackageIdentity);
|
|
221
|
+
if (JSON.stringify(actual) !== JSON.stringify(expected)) throw new Error("native finalizer package identity mismatch");
|
|
222
|
+
}
|
|
223
|
+
const archiveValue = output.archiveSha256 ?? output.archive?.sha256 ?? output.archive?.digest;
|
|
224
|
+
if (requireArchive && !archiveValue) throw new Error("native finalizer output must bind final archive digest");
|
|
225
|
+
const archiveSha256 = archiveValue ? normalizeDigest(archiveValue, "archiveSha256") : undefined;
|
|
226
|
+
if (expectedArchiveSha256 && archiveSha256 !== normalizeDigest(expectedArchiveSha256, "expected archiveSha256")) {
|
|
227
|
+
throw new Error("native finalizer archive digest mismatch");
|
|
228
|
+
}
|
|
229
|
+
if (output.archive?.path != null && PLACEHOLDER.test(String(output.archive.path))) throw new Error("native finalizer archive path is a placeholder");
|
|
230
|
+
return { ...output, provenance, signedArtifacts: signed, ...(archiveSha256 ? { archiveSha256 } : {}) };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export const validateNativeFinalization = validateNativeFinalizationOutput;
|
|
234
|
+
export const mintRightKitProvenance = mintNativeProvenance;
|
|
235
|
+
|
|
236
|
+
export function readNativeFinalizationOutput(file) {
|
|
237
|
+
if (!file) throw new TypeError("native finalizer output path is required");
|
|
238
|
+
if (!existsSync(file)) throw new Error(`native finalizer output missing: ${file}`);
|
|
239
|
+
let output;
|
|
240
|
+
try { output = JSON.parse(readFileSync(file, "utf8")); } catch (error) { throw new Error(`native finalizer output is not valid JSON: ${file}: ${error.message}`); }
|
|
241
|
+
return validateNativeFinalizationOutput(output);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export const finalizeNativeRelease = createNativeFinalizationReceipt;
|
|
245
|
+
|
|
246
|
+
export function resolveNativeFinalizerConfig(nativeAssembly) {
|
|
247
|
+
const finalizer = nativeAssembly?.finalizer;
|
|
248
|
+
if (!finalizer) return undefined;
|
|
249
|
+
if (typeof finalizer === "string") return { cmd: finalizer, args: [] };
|
|
250
|
+
if (!finalizer || typeof finalizer !== "object" || Array.isArray(finalizer)) throw new Error("nativeAssembly.finalizer must be a command object");
|
|
251
|
+
const command = finalizer.command ?? finalizer;
|
|
252
|
+
if (!command?.cmd || typeof command.cmd !== "string" || !Array.isArray(command.args ?? [])) throw new Error("nativeAssembly.finalizer must declare cmd and args");
|
|
253
|
+
return {
|
|
254
|
+
...finalizer,
|
|
255
|
+
cmd: command.cmd,
|
|
256
|
+
args: [...(command.args ?? [])].map(String),
|
|
257
|
+
output: finalizer.output ?? finalizer.receipt ?? finalizer.receiptPath ?? finalizer.outputFile,
|
|
258
|
+
};
|
|
259
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rightkit/release",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "Portable Right Suite release CLI/SDK: native
|
|
3
|
+
"version": "0.2.72",
|
|
4
|
+
"description": "Portable Right Suite release CLI/SDK: signed native archives, direct bootstrap transactions, immutable GitHub Release upload, R2 bootstrap publication, and add-on adoption.",
|
|
5
5
|
"license": "MIT OR Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
@@ -17,13 +17,6 @@
|
|
|
17
17
|
"!standalone-clone-evidence.json"
|
|
18
18
|
],
|
|
19
19
|
"sideEffects": false,
|
|
20
|
-
"scripts": {
|
|
21
|
-
"test": "node --test --test-concurrency=1 --test-force-exit *.test.mjs",
|
|
22
|
-
"test:registry-parity": "node registry-parity.mjs --allow-unpublished",
|
|
23
|
-
"prepublishOnly": "pnpm test && pnpm test:registry-parity",
|
|
24
|
-
"doctor:all": "node --test right-suite-contract.test.mjs",
|
|
25
|
-
"verify:standalone": "node standalone-clone-verify.mjs"
|
|
26
|
-
},
|
|
27
20
|
"publishConfig": {
|
|
28
21
|
"registry": "https://registry.npmjs.org/",
|
|
29
22
|
"access": "public"
|
|
@@ -33,5 +26,10 @@
|
|
|
33
26
|
"url": "git+https://github.com/bogusyogi/claude.git",
|
|
34
27
|
"directory": "tools/rightkit/packages/release"
|
|
35
28
|
},
|
|
36
|
-
"
|
|
37
|
-
|
|
29
|
+
"scripts": {
|
|
30
|
+
"test": "node --test --test-concurrency=1 --test-force-exit *.test.mjs",
|
|
31
|
+
"test:registry-parity": "node registry-parity.mjs --allow-unpublished",
|
|
32
|
+
"doctor:all": "node --test right-suite-contract.test.mjs",
|
|
33
|
+
"verify:standalone": "node standalone-clone-verify.mjs"
|
|
34
|
+
}
|
|
35
|
+
}
|
package/preflight.mjs
CHANGED
|
@@ -170,6 +170,8 @@ export function collectPreflight({
|
|
|
170
170
|
version,
|
|
171
171
|
configPath,
|
|
172
172
|
cargoLockPaths = [],
|
|
173
|
+
nativeLayout,
|
|
174
|
+
targetLink,
|
|
173
175
|
minFreeGb = 25,
|
|
174
176
|
env = process.env,
|
|
175
177
|
} = {}) {
|
|
@@ -177,7 +179,8 @@ export function collectPreflight({
|
|
|
177
179
|
const windows = platform === "win" || platform === "win32";
|
|
178
180
|
|
|
179
181
|
if (appRoot) {
|
|
180
|
-
const target = path.join(appRoot, "src-tauri", "target");
|
|
182
|
+
const target = targetLink ?? nativeLayout?.targetLink ?? path.join(appRoot, "src-tauri", "target");
|
|
183
|
+
const targetLabel = nativeLayout?.targetPrefix || "src-tauri/target/";
|
|
181
184
|
let entry = null;
|
|
182
185
|
try {
|
|
183
186
|
entry = lstatSync(target);
|
|
@@ -186,7 +189,7 @@ export function collectPreflight({
|
|
|
186
189
|
}
|
|
187
190
|
checks.push(
|
|
188
191
|
!entry || entry.isSymbolicLink()
|
|
189
|
-
? ok("target-bridge", entry ?
|
|
192
|
+
? ok("target-bridge", entry ? `${targetLabel} is a symbolic link` : `${targetLabel} is ready for the shared cache bridge`)
|
|
190
193
|
// A real directory here is not corruption on a broker-managed host: the
|
|
191
194
|
// build broker owns CARGO_TARGET_DIR and this is its own output (or a
|
|
192
195
|
// leftover from before it took ownership), not Cache V2's bridge target.
|
|
@@ -210,7 +213,7 @@ export function collectPreflight({
|
|
|
210
213
|
? fail(
|
|
211
214
|
"version",
|
|
212
215
|
`${app} ${version} is already sealed at ${sealedDir}; right-release will refuse to rebuild it`,
|
|
213
|
-
`bump to ${sealed.suggestion} in package.json
|
|
216
|
+
`bump to ${sealed.suggestion} in package.json and ${nativeLayout?.manifestPrefix ?? "src-tauri/"}Cargo.toml, then re-run the legal notices generator`,
|
|
214
217
|
)
|
|
215
218
|
: ok("version", `${version} is free to build`),
|
|
216
219
|
);
|
package/registry-parity.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { execFileSync } from 'node:child_process';
|
|
|
4
4
|
import { mkdtemp, mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises';
|
|
5
5
|
import { tmpdir } from 'node:os';
|
|
6
6
|
import { basename, join } from 'node:path';
|
|
7
|
-
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
8
8
|
|
|
9
9
|
const packageRoot = fileURLToPath(new URL('.', import.meta.url));
|
|
10
10
|
|
|
@@ -72,7 +72,7 @@ export async function verifyRegistryParity({ allowUnpublished = false, fetchImpl
|
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
if (import.meta.url ===
|
|
75
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
76
76
|
verifyRegistryParity({ allowUnpublished: process.argv.includes('--allow-unpublished') })
|
|
77
77
|
.then((result) => process.stdout.write(`${JSON.stringify(result)}\n`))
|
|
78
78
|
.catch((error) => { process.stderr.write(`${error.message}\n`); process.exitCode = 1; });
|
package/release-invocation.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { resolveNativeCargoLayout } from "./native-cargo-layout.mjs";
|
|
4
5
|
|
|
5
6
|
function git(cwd, args) {
|
|
6
7
|
const result = spawnSync("git", args, { cwd, encoding: "utf8", windowsHide: true });
|
|
@@ -53,12 +54,18 @@ export function resolveConfiguredBuildInputs(config, target, label = "release co
|
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
export function resolveReleaseBuildInputs({ repoRoot, appRoot, configPath, config, target, env = process.env }) {
|
|
57
|
+
const nativeLayout = resolveNativeCargoLayout({
|
|
58
|
+
appRoot,
|
|
59
|
+
config,
|
|
60
|
+
requireManifest: true,
|
|
61
|
+
requireLock: true,
|
|
62
|
+
});
|
|
56
63
|
const requiredInputs = [
|
|
57
64
|
configPath,
|
|
58
65
|
path.join(appRoot, "package.json"),
|
|
59
66
|
path.join(appRoot, "pnpm-lock.yaml"),
|
|
60
|
-
|
|
61
|
-
|
|
67
|
+
nativeLayout.manifestPath,
|
|
68
|
+
nativeLayout.lockPath,
|
|
62
69
|
...(target?.preflight?.files ?? []).map((file) => path.resolve(appRoot, expandEnv(file, env))),
|
|
63
70
|
].filter((file, index, files) => files.indexOf(file) === index);
|
|
64
71
|
for (const file of requiredInputs) {
|
|
@@ -77,6 +84,7 @@ export function resolveReleaseBuildInputs({ repoRoot, appRoot, configPath, confi
|
|
|
77
84
|
|
|
78
85
|
return {
|
|
79
86
|
requiredInputs,
|
|
87
|
+
nativeLayout,
|
|
80
88
|
buildInputs: {
|
|
81
89
|
include: configured.include.map(qualify),
|
|
82
90
|
exclude: (configured.exclude ?? []).map(qualify),
|
package/release-state.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { spawnSync } from "node:child_process";
|
|
|
3
3
|
import { existsSync, readFileSync, readdirSync, watch } from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { readCachePolicy, resolveCacheLayout } from "./cache-policy.mjs";
|
|
6
|
+
import { resolveNativeCargoLayout } from "./native-cargo-layout.mjs";
|
|
6
7
|
|
|
7
8
|
export function cacheFingerprint({ cargoLockSha256, rustc, target, architecture, features = [] }) {
|
|
8
9
|
const payload = JSON.stringify({
|
|
@@ -15,14 +16,16 @@ export function cacheFingerprint({ cargoLockSha256, rustc, target, architecture,
|
|
|
15
16
|
return createHash("sha256").update(payload).digest("hex").slice(0, 16);
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
export function resolveReleaseLayout({ repoRoot, configPath }) {
|
|
19
|
+
export function resolveReleaseLayout({ repoRoot, configPath, config, nativeLayout }) {
|
|
19
20
|
const resolvedRepoRoot = path.resolve(repoRoot);
|
|
20
21
|
const appRoot = path.dirname(path.resolve(configPath));
|
|
22
|
+
const resolvedNativeLayout = nativeLayout ?? resolveNativeCargoLayout({ appRoot, config });
|
|
21
23
|
return {
|
|
22
24
|
repoRoot: resolvedRepoRoot,
|
|
23
25
|
appRoot,
|
|
24
26
|
vaultRoot: path.join(resolvedRepoRoot, ".right-release"),
|
|
25
|
-
targetLink:
|
|
27
|
+
targetLink: resolvedNativeLayout.targetLink,
|
|
28
|
+
nativeLayout: resolvedNativeLayout,
|
|
26
29
|
};
|
|
27
30
|
}
|
|
28
31
|
|