@rightkit/release 0.2.22 → 0.2.23
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 +1 -1
- package/cli/right-release.mjs +0 -0
- package/legal-contract.mjs +165 -0
- package/legal-contract.test.mjs +136 -0
- package/package.json +7 -8
- package/release.mjs +9 -0
- package/right-suite-contract.test.mjs +1 -1
- package/standalone-clone-verify.mjs +7 -1
package/cargo-contract.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { tmpdir } from "node:os";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { spawnSync } from "node:child_process";
|
|
5
5
|
|
|
6
|
-
const SKIP_DIRECTORIES = new Set([".cache", ".git", ".claude", ".worktrees", "node_modules", "target", "vendor"]);
|
|
6
|
+
const SKIP_DIRECTORIES = new Set([".audit", ".cache", ".git", ".claude", ".worktrees", "node_modules", "target", "vendor"]);
|
|
7
7
|
const CRATES_IO_SOURCES = new Set([
|
|
8
8
|
"registry+https://github.com/rust-lang/crates.io-index",
|
|
9
9
|
"registry+https://index.crates.io/",
|
package/cli/right-release.mjs
CHANGED
|
File without changes
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
const REQUIRED_ROLES = new Set([
|
|
6
|
+
"license",
|
|
7
|
+
"eula",
|
|
8
|
+
"acceptable_use",
|
|
9
|
+
"product_schedule",
|
|
10
|
+
"privacy_notice",
|
|
11
|
+
"third_party_notices",
|
|
12
|
+
]);
|
|
13
|
+
const AGREEMENT_ROLES = new Set(["license", "eula", "acceptable_use", "product_schedule"]);
|
|
14
|
+
const SHA256 = /^[a-f0-9]{64}$/i;
|
|
15
|
+
const APP_KEY = /^[a-z][a-z0-9-]{1,63}$/;
|
|
16
|
+
const PLACEHOLDER = /\[(?:COUNSEL|TODO|TBD)(?::|\])|\b(?:TODO|TBD|PLACEHOLDER|NOT READY|RELEASE BLOCKER)\b|⚠\s*verify|^- \[ \]/im;
|
|
17
|
+
|
|
18
|
+
function sha256(bytes) {
|
|
19
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function fail(appName, message) {
|
|
23
|
+
throw new Error(`${appName ?? "app"} legal contract: ${message}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function within(parent, candidate) {
|
|
27
|
+
const relative = path.relative(parent, candidate);
|
|
28
|
+
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function resolveContained(parent, relative, appName, label) {
|
|
32
|
+
if (typeof relative !== "string" || !relative.trim() || path.isAbsolute(relative)) {
|
|
33
|
+
fail(appName, `${label} must be a non-empty relative path`);
|
|
34
|
+
}
|
|
35
|
+
const candidate = path.resolve(parent, relative);
|
|
36
|
+
if (!within(parent, candidate)) fail(appName, `${label} path escapes the legal snapshot (path traversal)`);
|
|
37
|
+
return candidate;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function readJson(file, appName, label) {
|
|
41
|
+
let value;
|
|
42
|
+
try {
|
|
43
|
+
value = JSON.parse(readFileSync(file, "utf8"));
|
|
44
|
+
} catch (error) {
|
|
45
|
+
fail(appName, `${label} is missing or invalid JSON: ${error.message}`);
|
|
46
|
+
}
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function assertLegalReleaseContract(root, legal, appName, platform) {
|
|
51
|
+
const appRoot = realpathSync(root);
|
|
52
|
+
if (!legal || typeof legal !== "object" || typeof legal.manifest !== "string") {
|
|
53
|
+
fail(appName, "release config must declare legal.manifest");
|
|
54
|
+
}
|
|
55
|
+
const manifestPath = path.resolve(appRoot, legal.manifest);
|
|
56
|
+
if (!within(appRoot, manifestPath) || !existsSync(manifestPath)) {
|
|
57
|
+
fail(appName, `legal.manifest must exist inside the app root: ${legal.manifest}`);
|
|
58
|
+
}
|
|
59
|
+
const manifestBytes = readFileSync(manifestPath);
|
|
60
|
+
const manifest = readJson(manifestPath, appName, "legal manifest");
|
|
61
|
+
const snapshotRoot = realpathSync(path.dirname(manifestPath));
|
|
62
|
+
|
|
63
|
+
if (manifest.schema !== 1 || manifest.suite !== "right-suite-desktop") {
|
|
64
|
+
fail(appName, "manifest must use Right Suite legal schema 1");
|
|
65
|
+
}
|
|
66
|
+
if (!APP_KEY.test(manifest.appKey ?? "") || (appName && manifest.appKey !== appName)) {
|
|
67
|
+
fail(appName, `manifest appKey must match release app (${manifest.appKey ?? "missing"})`);
|
|
68
|
+
}
|
|
69
|
+
if (manifest.freeWorkerMaximum !== 9 || manifest.paidWorkerMinimum !== 10) {
|
|
70
|
+
fail(appName, "manifest Worker threshold must be free through 9 and paid from 10");
|
|
71
|
+
}
|
|
72
|
+
if (typeof manifest.acceptanceVersion !== "string" || !manifest.acceptanceVersion.trim()) {
|
|
73
|
+
fail(appName, "manifest acceptanceVersion is required");
|
|
74
|
+
}
|
|
75
|
+
if (!Array.isArray(manifest.documents)) fail(appName, "manifest documents must be an array");
|
|
76
|
+
|
|
77
|
+
const seen = new Set();
|
|
78
|
+
const resolvedDocuments = [];
|
|
79
|
+
for (const document of manifest.documents) {
|
|
80
|
+
if (!REQUIRED_ROLES.has(document.role)) fail(appName, `unsupported document role: ${document.role ?? "missing"}`);
|
|
81
|
+
if (seen.has(document.role)) fail(appName, `duplicate document role: ${document.role}`);
|
|
82
|
+
seen.add(document.role);
|
|
83
|
+
if (!SHA256.test(document.sha256 ?? "")) fail(appName, `${document.role} sha256 must be 64 hexadecimal characters`);
|
|
84
|
+
if (AGREEMENT_ROLES.has(document.role) && (document.treatment !== "agree" || document.material !== true)) {
|
|
85
|
+
fail(appName, `${document.role} must be a material agreement`);
|
|
86
|
+
}
|
|
87
|
+
if (document.role === "privacy_notice" && document.treatment !== "acknowledge") {
|
|
88
|
+
fail(appName, "privacy_notice must be acknowledged, not treated as consent/agreement");
|
|
89
|
+
}
|
|
90
|
+
if (document.role === "third_party_notices" && document.treatment !== "notice") {
|
|
91
|
+
fail(appName, "third_party_notices must use notice treatment");
|
|
92
|
+
}
|
|
93
|
+
const file = resolveContained(snapshotRoot, document.path, appName, `${document.role} document`);
|
|
94
|
+
if (!existsSync(file)) fail(appName, `missing ${document.role} document: ${document.path}`);
|
|
95
|
+
const realFile = realpathSync(file);
|
|
96
|
+
if (!within(snapshotRoot, realFile) || !within(appRoot, realFile)) {
|
|
97
|
+
fail(appName, `${document.role} document resolves outside the legal snapshot`);
|
|
98
|
+
}
|
|
99
|
+
const bytes = readFileSync(realFile);
|
|
100
|
+
if (sha256(bytes) !== document.sha256.toLowerCase()) {
|
|
101
|
+
fail(appName, `${document.path} sha256 does not match legal-manifest.json`);
|
|
102
|
+
}
|
|
103
|
+
const text = bytes.toString("utf8");
|
|
104
|
+
if (PLACEHOLDER.test(text)) {
|
|
105
|
+
fail(appName, `${document.path} contains a placeholder or unchecked release blocker`);
|
|
106
|
+
}
|
|
107
|
+
if (document.role === "third_party_notices") {
|
|
108
|
+
if (!/build provenance\s*:/i.test(text) || !/inputs\s*:/i.test(text) || !/generated-by\s*=/i.test(text)) {
|
|
109
|
+
fail(appName, "third-party notices must contain Build provenance, inputs, and generated-by evidence");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
resolvedDocuments.push({ ...document, absolutePath: realFile });
|
|
113
|
+
}
|
|
114
|
+
for (const role of REQUIRED_ROLES) {
|
|
115
|
+
if (!seen.has(role)) fail(appName, `missing required document role: ${role}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const appendixIds = new Set();
|
|
119
|
+
const appendices = [];
|
|
120
|
+
if (manifest.appendices != null && !Array.isArray(manifest.appendices)) {
|
|
121
|
+
fail(appName, "manifest appendices must be an array");
|
|
122
|
+
}
|
|
123
|
+
for (const appendix of manifest.appendices ?? []) {
|
|
124
|
+
if (typeof appendix.id !== "string" || !appendix.id.trim()) fail(appName, "appendix id is required");
|
|
125
|
+
if (appendixIds.has(appendix.id)) fail(appName, `duplicate appendix id: ${appendix.id}`);
|
|
126
|
+
appendixIds.add(appendix.id);
|
|
127
|
+
if (typeof appendix.title !== "string" || !appendix.title.trim()) fail(appName, `${appendix.id} appendix title is required`);
|
|
128
|
+
if (!SHA256.test(appendix.sha256 ?? "")) fail(appName, `${appendix.id} appendix sha256 is invalid`);
|
|
129
|
+
const file = resolveContained(snapshotRoot, appendix.path, appName, `${appendix.id} appendix`);
|
|
130
|
+
if (!existsSync(file)) fail(appName, `${appendix.id} appendix is missing: ${appendix.path}`);
|
|
131
|
+
const realFile = realpathSync(file);
|
|
132
|
+
if (!within(snapshotRoot, realFile) || !within(appRoot, realFile)) {
|
|
133
|
+
fail(appName, `${appendix.id} appendix resolves outside the legal snapshot`);
|
|
134
|
+
}
|
|
135
|
+
const bytes = readFileSync(realFile);
|
|
136
|
+
if (sha256(bytes) !== appendix.sha256.toLowerCase()) fail(appName, `${appendix.id} appendix sha256 does not match`);
|
|
137
|
+
if (PLACEHOLDER.test(bytes.toString("utf8"))) fail(appName, `${appendix.id} appendix contains a release blocker`);
|
|
138
|
+
appendices.push({ ...appendix, absolutePath: realFile });
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (platform === "win") {
|
|
142
|
+
if (typeof legal.tauriConfig !== "string") fail(appName, "Windows legal gate requires legal.tauriConfig");
|
|
143
|
+
const tauriPath = path.resolve(appRoot, legal.tauriConfig);
|
|
144
|
+
if (!within(appRoot, tauriPath) || !existsSync(tauriPath)) fail(appName, `missing Tauri config: ${legal.tauriConfig}`);
|
|
145
|
+
const tauri = readJson(tauriPath, appName, "Tauri config");
|
|
146
|
+
const configured = tauri.bundle?.licenseFile;
|
|
147
|
+
if (typeof configured !== "string" || !configured.trim()) {
|
|
148
|
+
fail(appName, "Windows Tauri bundle.licenseFile must point to the snapshot EULA");
|
|
149
|
+
}
|
|
150
|
+
const configuredPath = path.resolve(path.dirname(tauriPath), configured);
|
|
151
|
+
const eula = resolvedDocuments.find((document) => document.role === "eula");
|
|
152
|
+
if (!eula || path.normalize(configuredPath).toLowerCase() !== path.normalize(eula.absolutePath).toLowerCase()) {
|
|
153
|
+
fail(appName, "Windows Tauri bundle.licenseFile must point to the exact EULA snapshot");
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
appKey: manifest.appKey,
|
|
159
|
+
acceptanceVersion: manifest.acceptanceVersion,
|
|
160
|
+
manifestPath,
|
|
161
|
+
manifestSha256: sha256(manifestBytes),
|
|
162
|
+
documents: resolvedDocuments,
|
|
163
|
+
appendices,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import test from "node:test";
|
|
7
|
+
import { assertLegalReleaseContract } from "./legal-contract.mjs";
|
|
8
|
+
|
|
9
|
+
function digest(text) {
|
|
10
|
+
return createHash("sha256").update(text).digest("hex");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function fixture() {
|
|
14
|
+
const root = mkdtempSync(path.join(os.tmpdir(), "right-legal-contract-"));
|
|
15
|
+
const legalDir = path.join(root, "legal");
|
|
16
|
+
const tauriDir = path.join(root, "src-tauri");
|
|
17
|
+
mkdirSync(legalDir, { recursive: true });
|
|
18
|
+
mkdirSync(tauriDir, { recursive: true });
|
|
19
|
+
const definitions = [
|
|
20
|
+
["license", "LICENSE.md", "Ownership notice\n"],
|
|
21
|
+
["eula", "EULA.md", "End user license agreement\n"],
|
|
22
|
+
["acceptable_use", "ACCEPTABLE_USE.md", "Acceptable use\n"],
|
|
23
|
+
["product_schedule", "PRODUCT_SCHEDULE.md", "Product schedule\n"],
|
|
24
|
+
["privacy_notice", "PRIVACY.md", "Privacy notice\n"],
|
|
25
|
+
["third_party_notices", "THIRD_PARTY_NOTICES.md", "Build provenance: fixture 1.0 windows-x86_64; inputs: pnpm-lock=abc Cargo.lock=def; generated-by=fixture\n"],
|
|
26
|
+
];
|
|
27
|
+
for (const [, file, content] of definitions) writeFileSync(path.join(legalDir, file), content);
|
|
28
|
+
const manifest = {
|
|
29
|
+
schema: 1,
|
|
30
|
+
suite: "right-suite-desktop",
|
|
31
|
+
appKey: "fixture",
|
|
32
|
+
productName: "Fixture",
|
|
33
|
+
licensor: "Damned Ventures LLC",
|
|
34
|
+
effectiveDate: "2026-07-16",
|
|
35
|
+
acceptanceVersion: "fixture-2026-07-16-v1",
|
|
36
|
+
freeWorkerMaximum: 9,
|
|
37
|
+
paidWorkerMinimum: 10,
|
|
38
|
+
documents: definitions.map(([role, file, content]) => ({
|
|
39
|
+
id: `fixture-${role}`,
|
|
40
|
+
role,
|
|
41
|
+
title: role,
|
|
42
|
+
version: "1.0",
|
|
43
|
+
sha256: digest(content),
|
|
44
|
+
path: file,
|
|
45
|
+
publicUrl: `https://example.test/legal/${role}`,
|
|
46
|
+
treatment: ["privacy_notice"].includes(role) ? "acknowledge" : role === "third_party_notices" ? "notice" : "agree",
|
|
47
|
+
material: !["privacy_notice", "third_party_notices"].includes(role),
|
|
48
|
+
})),
|
|
49
|
+
};
|
|
50
|
+
writeFileSync(path.join(legalDir, "legal-manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
51
|
+
writeFileSync(
|
|
52
|
+
path.join(tauriDir, "tauri.conf.json"),
|
|
53
|
+
`${JSON.stringify({ bundle: { licenseFile: "../legal/EULA.md" } }, null, 2)}\n`,
|
|
54
|
+
);
|
|
55
|
+
return {
|
|
56
|
+
root,
|
|
57
|
+
legal: { manifest: "legal/legal-manifest.json", tauriConfig: "src-tauri/tauri.conf.json" },
|
|
58
|
+
manifest,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
test("accepts a complete, hash-bound legal snapshot", () => {
|
|
63
|
+
const { root, legal } = fixture();
|
|
64
|
+
const result = assertLegalReleaseContract(root, legal, "fixture", "win");
|
|
65
|
+
assert.equal(result.appKey, "fixture");
|
|
66
|
+
assert.equal(result.documents.length, 6);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("rejects a missing legal config", () => {
|
|
70
|
+
const { root } = fixture();
|
|
71
|
+
assert.throws(() => assertLegalReleaseContract(root, null, "fixture", "win"), /must declare legal\.manifest/i);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("rejects changed bytes and unsafe manifest paths", () => {
|
|
75
|
+
const changed = fixture();
|
|
76
|
+
writeFileSync(path.join(changed.root, "legal", "EULA.md"), "changed without a new manifest\n");
|
|
77
|
+
assert.throws(() => assertLegalReleaseContract(changed.root, changed.legal, "fixture", "win"), /EULA\.md.*sha-?256/i);
|
|
78
|
+
|
|
79
|
+
const traversal = fixture();
|
|
80
|
+
traversal.manifest.documents[0].path = "../outside.md";
|
|
81
|
+
writeFileSync(path.join(traversal.root, "legal", "legal-manifest.json"), `${JSON.stringify(traversal.manifest)}\n`);
|
|
82
|
+
assert.throws(() => assertLegalReleaseContract(traversal.root, traversal.legal, "fixture", "win"), /path.*snapshot|traversal/i);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("rejects placeholders and unresolved checklist markers", () => {
|
|
86
|
+
const { root, legal, manifest } = fixture();
|
|
87
|
+
const eula = "Terms [COUNSEL: decide later]\n- [ ] fill this in\n";
|
|
88
|
+
writeFileSync(path.join(root, "legal", "EULA.md"), eula);
|
|
89
|
+
manifest.documents.find((document) => document.role === "eula").sha256 = digest(eula);
|
|
90
|
+
writeFileSync(path.join(root, "legal", "legal-manifest.json"), `${JSON.stringify(manifest)}\n`);
|
|
91
|
+
assert.throws(() => assertLegalReleaseContract(root, legal, "fixture", "win"), /placeholder|unchecked|release blocker/i);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("rejects an explicitly unresolved release blocker even without a checkbox", () => {
|
|
95
|
+
const { root, legal, manifest } = fixture();
|
|
96
|
+
const notice = "Build provenance: fixture; inputs: lock=abc; generated-by=fixture\nRELEASE BLOCKER: exact model notice is unresolved.\n";
|
|
97
|
+
writeFileSync(path.join(root, "legal", "THIRD_PARTY_NOTICES.md"), notice);
|
|
98
|
+
manifest.documents.find((document) => document.role === "third_party_notices").sha256 = digest(notice);
|
|
99
|
+
writeFileSync(path.join(root, "legal", "legal-manifest.json"), `${JSON.stringify(manifest)}\n`);
|
|
100
|
+
assert.throws(() => assertLegalReleaseContract(root, legal, "fixture", "win"), /release blocker/i);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("requires Windows installer clickthrough to use the exact EULA snapshot", () => {
|
|
104
|
+
const { root, legal } = fixture();
|
|
105
|
+
writeFileSync(path.join(root, "src-tauri", "tauri.conf.json"), JSON.stringify({ bundle: { licenseFile: "../legal/LICENSE.md" } }));
|
|
106
|
+
assert.throws(() => assertLegalReleaseContract(root, legal, "fixture", "win"), /licenseFile.*EULA/i);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("requires third-party build provenance", () => {
|
|
110
|
+
const { root, legal, manifest } = fixture();
|
|
111
|
+
const notice = "Third-party software is included.\n";
|
|
112
|
+
writeFileSync(path.join(root, "legal", "THIRD_PARTY_NOTICES.md"), notice);
|
|
113
|
+
manifest.documents.find((document) => document.role === "third_party_notices").sha256 = digest(notice);
|
|
114
|
+
writeFileSync(path.join(root, "legal", "legal-manifest.json"), `${JSON.stringify(manifest)}\n`);
|
|
115
|
+
assert.throws(() => assertLegalReleaseContract(root, legal, "fixture", "win"), /third-party.*provenance/i);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("manifest hash evidence is the hash of the manifest bytes", () => {
|
|
119
|
+
const { root, legal } = fixture();
|
|
120
|
+
const result = assertLegalReleaseContract(root, legal, "fixture", "mac");
|
|
121
|
+
const bytes = readFileSync(path.join(root, legal.manifest));
|
|
122
|
+
assert.equal(result.manifestSha256, createHash("sha256").update(bytes).digest("hex"));
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("hash-binds declared third-party appendices and rejects missing files", () => {
|
|
126
|
+
const { root, legal, manifest } = fixture();
|
|
127
|
+
const appendix = "full dependency license text\n";
|
|
128
|
+
writeFileSync(path.join(root, "legal", "RUST_LICENSES.txt"), appendix);
|
|
129
|
+
manifest.appendices = [{ id: "rust-licenses", title: "Rust licenses", path: "RUST_LICENSES.txt", sha256: digest(appendix) }];
|
|
130
|
+
writeFileSync(path.join(root, "legal", "legal-manifest.json"), `${JSON.stringify(manifest)}\n`);
|
|
131
|
+
assert.equal(assertLegalReleaseContract(root, legal, "fixture", "mac").appendices.length, 1);
|
|
132
|
+
|
|
133
|
+
manifest.appendices[0].path = "missing.txt";
|
|
134
|
+
writeFileSync(path.join(root, "legal", "legal-manifest.json"), `${JSON.stringify(manifest)}\n`);
|
|
135
|
+
assert.throws(() => assertLegalReleaseContract(root, legal, "fixture", "mac"), /appendix.*missing/i);
|
|
136
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rightkit/release",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.23",
|
|
4
4
|
"description": "Portable Right Suite release CLI/SDK: signed installers, updater artifacts, patch/update routing, hardening, R2 publish, and RightApps registration.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -14,11 +14,6 @@
|
|
|
14
14
|
"*.py"
|
|
15
15
|
],
|
|
16
16
|
"sideEffects": false,
|
|
17
|
-
"scripts": {
|
|
18
|
-
"test": "node --test *.test.mjs",
|
|
19
|
-
"doctor:all": "node --test right-suite-contract.test.mjs",
|
|
20
|
-
"verify:standalone": "node standalone-clone-verify.mjs"
|
|
21
|
-
},
|
|
22
17
|
"publishConfig": {
|
|
23
18
|
"registry": "https://registry.npmjs.org/",
|
|
24
19
|
"access": "public"
|
|
@@ -28,5 +23,9 @@
|
|
|
28
23
|
"url": "git+https://github.com/adrdsouza/claude.git",
|
|
29
24
|
"directory": "tools/rightkit/packages/release"
|
|
30
25
|
},
|
|
31
|
-
"
|
|
32
|
-
|
|
26
|
+
"scripts": {
|
|
27
|
+
"test": "node --test *.test.mjs",
|
|
28
|
+
"doctor:all": "node --test right-suite-contract.test.mjs",
|
|
29
|
+
"verify:standalone": "node standalone-clone-verify.mjs"
|
|
30
|
+
}
|
|
31
|
+
}
|
package/release.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
6
6
|
import { spawn, spawnSync } from "node:child_process";
|
|
7
7
|
import { validateRightKitCargoContract } from "./cargo-contract.mjs";
|
|
8
8
|
import { assertQaBackdoorContract } from "./qa-contract.mjs";
|
|
9
|
+
import { assertLegalReleaseContract } from "./legal-contract.mjs";
|
|
9
10
|
|
|
10
11
|
const TOOL_ROOT = path.dirname(fileURLToPath(import.meta.url));
|
|
11
12
|
const HARDENING_SCAN = path.resolve(TOOL_ROOT, "hardeningscan.mjs");
|
|
@@ -67,6 +68,9 @@ await validateRightKitPackageContract(root, config.app);
|
|
|
67
68
|
validateRightKitCargoContract(root, RIGHTKIT_VERSIONS.cargo, config.app ?? path.basename(root));
|
|
68
69
|
const target = config.targets?.[opts.platform];
|
|
69
70
|
if (!target) fail(`${config.app ?? "app"} has no ${opts.platform} release target`);
|
|
71
|
+
const legalContract = config.legal
|
|
72
|
+
? assertLegalReleaseContract(root, config.legal, config.app ?? path.basename(root), opts.platform)
|
|
73
|
+
: null;
|
|
70
74
|
if (target.signed !== true) {
|
|
71
75
|
fail(`${config.app ?? "app"} ${opts.platform} must declare signed: true; unsigned release targets are forbidden`);
|
|
72
76
|
}
|
|
@@ -83,6 +87,11 @@ if (opts.doctor) {
|
|
|
83
87
|
console.log(`packageManager: ${config.packageManager}`);
|
|
84
88
|
console.log(`workdir: ${workdir}`);
|
|
85
89
|
console.log(`hardeningscan: ${HARDENING_SCAN}`);
|
|
90
|
+
if (legalContract) {
|
|
91
|
+
console.log(`legal: ${legalContract.manifestPath}`);
|
|
92
|
+
console.log(`legalAcceptance: ${legalContract.acceptanceVersion}`);
|
|
93
|
+
console.log(`legalManifestSha256: ${legalContract.manifestSha256}`);
|
|
94
|
+
}
|
|
86
95
|
if (target.sign?.files?.length) console.log(`sign: ${target.sign.files.join(", ")}`);
|
|
87
96
|
if (target.publishBlocked) console.log(`publishBlocked: ${target.publishBlocked}`);
|
|
88
97
|
process.exit(0);
|
|
@@ -467,7 +467,7 @@ function findFiles(root, filename) {
|
|
|
467
467
|
const found = [];
|
|
468
468
|
const visit = (dir) => {
|
|
469
469
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
470
|
-
if (entry.isDirectory() && [".cache", ".git", ".claude", ".worktrees", "node_modules", "target", "vendor"].includes(entry.name)) continue;
|
|
470
|
+
if (entry.isDirectory() && [".audit", ".cache", ".git", ".claude", ".worktrees", "node_modules", "target", "vendor"].includes(entry.name)) continue;
|
|
471
471
|
const full = path.join(dir, entry.name);
|
|
472
472
|
if (entry.isDirectory()) visit(full);
|
|
473
473
|
else if (entry.isFile() && entry.name === filename) found.push(full);
|
|
@@ -32,8 +32,14 @@ function run(command, args, cwd) {
|
|
|
32
32
|
function runPnpm(args, cwd, command) {
|
|
33
33
|
if (process.platform !== "win32" || command !== "pnpm") return run(command, args, cwd);
|
|
34
34
|
for (const entry of String(process.env.PATH ?? "").split(path.delimiter)) {
|
|
35
|
-
const
|
|
35
|
+
const directory = entry.replace(/^"|"$/g, "");
|
|
36
|
+
const cli = path.join(directory, "node_modules", "pnpm", "bin", "pnpm.mjs");
|
|
36
37
|
if (existsSync(cli)) return run(process.execPath, [cli, ...args], cwd);
|
|
38
|
+
const cmdShim = path.join(directory, "pnpm.cmd");
|
|
39
|
+
if (existsSync(cmdShim)) {
|
|
40
|
+
const target = readFileSync(cmdShim, "utf8").match(/@node\s+"([^"]+pnpm\.(?:c|m)?js)"/i)?.[1];
|
|
41
|
+
if (target && existsSync(target)) return run(process.execPath, [target, ...args], cwd);
|
|
42
|
+
}
|
|
37
43
|
}
|
|
38
44
|
throw new Error("pnpm installation could not be resolved from PATH");
|
|
39
45
|
}
|