docdex 0.1.11 → 0.2.2
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/CHANGELOG.md +1 -1
- package/README.md +103 -65
- package/bin/docdex.js +145 -5
- package/lib/daemon_version.js +80 -0
- package/lib/install.js +1940 -28
- package/lib/installer_logging.js +134 -0
- package/lib/platform.js +275 -20
- package/lib/platform_matrix.js +127 -0
- package/lib/postinstall_setup.js +885 -0
- package/lib/release_manifest.js +226 -0
- package/lib/release_signing.js +93 -0
- package/package.json +4 -2
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const EXIT_CODE_BY_CODE = Object.freeze({
|
|
4
|
+
DOCDEX_MANIFEST_MALFORMED: 10,
|
|
5
|
+
DOCDEX_TARGET_TRIPLE_INVALID: 11,
|
|
6
|
+
DOCDEX_ASSET_NO_MATCH: 12,
|
|
7
|
+
DOCDEX_ASSET_MULTI_MATCH: 13,
|
|
8
|
+
DOCDEX_ASSET_MALFORMED: 14
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
class ManifestResolutionError extends Error {
|
|
12
|
+
/**
|
|
13
|
+
* @param {string} code
|
|
14
|
+
* @param {string} message
|
|
15
|
+
* @param {object} [details]
|
|
16
|
+
*/
|
|
17
|
+
constructor(code, message, details) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.name = "ManifestResolutionError";
|
|
20
|
+
this.code = code;
|
|
21
|
+
this.exitCode = EXIT_CODE_BY_CODE[code] ?? 1;
|
|
22
|
+
this.details = {
|
|
23
|
+
targetTriple: null,
|
|
24
|
+
manifestVersion: null,
|
|
25
|
+
assetName: null,
|
|
26
|
+
...(details || {})
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isPlainObject(value) {
|
|
32
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getManifestVersion(manifest) {
|
|
36
|
+
if (!isPlainObject(manifest)) return null;
|
|
37
|
+
return manifest.manifestVersion ?? manifest.schemaVersion ?? manifest.version ?? null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function getSupportedTargetTriples(manifest) {
|
|
41
|
+
if (!isPlainObject(manifest)) return [];
|
|
42
|
+
|
|
43
|
+
if (isPlainObject(manifest.targets)) return Object.keys(manifest.targets).sort();
|
|
44
|
+
|
|
45
|
+
const assets = Array.isArray(manifest.assets) ? manifest.assets : [];
|
|
46
|
+
const triples = [];
|
|
47
|
+
for (const entry of assets) {
|
|
48
|
+
if (!isPlainObject(entry)) continue;
|
|
49
|
+
const triple =
|
|
50
|
+
entry.target_triple ??
|
|
51
|
+
entry.targetTriple ??
|
|
52
|
+
entry.target ??
|
|
53
|
+
entry.triple ??
|
|
54
|
+
entry.platform;
|
|
55
|
+
if (typeof triple === "string" && triple) triples.push(triple);
|
|
56
|
+
}
|
|
57
|
+
return [...new Set(triples)].sort();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function normalizeSha256(value) {
|
|
61
|
+
if (typeof value !== "string") return null;
|
|
62
|
+
const trimmed = value.trim().toLowerCase();
|
|
63
|
+
if (!/^[0-9a-f]{64}$/.test(trimmed)) return null;
|
|
64
|
+
return trimmed;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function extractAssetAndIntegrity(entry) {
|
|
68
|
+
// Asset identifier/name: tolerate a few common shapes.
|
|
69
|
+
// - entry.asset: string | { name, id }
|
|
70
|
+
// - entry.name / entry.asset_name / entry.assetName: string
|
|
71
|
+
// Integrity:
|
|
72
|
+
// - entry.integrity.sha256
|
|
73
|
+
// - entry.sha256
|
|
74
|
+
const asset = entry.asset;
|
|
75
|
+
|
|
76
|
+
let assetName = null;
|
|
77
|
+
let assetId = null;
|
|
78
|
+
|
|
79
|
+
if (typeof asset === "string") {
|
|
80
|
+
assetName = asset;
|
|
81
|
+
} else if (isPlainObject(asset)) {
|
|
82
|
+
if (typeof asset.name === "string") assetName = asset.name;
|
|
83
|
+
if (typeof asset.id === "string" || typeof asset.id === "number") assetId = asset.id;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!assetName) {
|
|
87
|
+
const candidate =
|
|
88
|
+
entry.asset_name ??
|
|
89
|
+
entry.assetName ??
|
|
90
|
+
entry.name ??
|
|
91
|
+
entry.filename ??
|
|
92
|
+
entry.file_name ??
|
|
93
|
+
entry.fileName;
|
|
94
|
+
if (typeof candidate === "string") assetName = candidate;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const sha256 =
|
|
98
|
+
normalizeSha256(entry.integrity?.sha256) ??
|
|
99
|
+
normalizeSha256(entry.sha256) ??
|
|
100
|
+
normalizeSha256(entry.checksum?.sha256);
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
asset: assetName ? { name: assetName, ...(assetId != null ? { id: assetId } : {}) } : null,
|
|
104
|
+
integrity: sha256 ? { sha256 } : null
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function entriesFromManifest(manifest) {
|
|
109
|
+
if (!isPlainObject(manifest)) {
|
|
110
|
+
throw new ManifestResolutionError(
|
|
111
|
+
"DOCDEX_MANIFEST_MALFORMED",
|
|
112
|
+
"Malformed manifest: expected a JSON object at top-level",
|
|
113
|
+
{ manifestVersion: null }
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (isPlainObject(manifest.targets)) {
|
|
118
|
+
const entries = [];
|
|
119
|
+
for (const [targetTriple, value] of Object.entries(manifest.targets)) {
|
|
120
|
+
if (!isPlainObject(value)) continue;
|
|
121
|
+
entries.push({ targetTriple, entry: value });
|
|
122
|
+
}
|
|
123
|
+
return entries;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (Array.isArray(manifest.assets)) {
|
|
127
|
+
const entries = [];
|
|
128
|
+
for (const value of manifest.assets) {
|
|
129
|
+
if (!isPlainObject(value)) continue;
|
|
130
|
+
const targetTriple =
|
|
131
|
+
value.target_triple ??
|
|
132
|
+
value.targetTriple ??
|
|
133
|
+
value.target ??
|
|
134
|
+
value.triple ??
|
|
135
|
+
value.platform;
|
|
136
|
+
if (typeof targetTriple !== "string" || !targetTriple) continue;
|
|
137
|
+
entries.push({ targetTriple, entry: value });
|
|
138
|
+
}
|
|
139
|
+
return entries;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
throw new ManifestResolutionError(
|
|
143
|
+
"DOCDEX_MANIFEST_MALFORMED",
|
|
144
|
+
"Malformed manifest: expected `targets` object or `assets` array",
|
|
145
|
+
{ manifestVersion: getManifestVersion(manifest) }
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Deterministically resolve exactly one asset entry (canonical asset identifier/name + integrity metadata)
|
|
151
|
+
* for a given Rust target triple.
|
|
152
|
+
*
|
|
153
|
+
* The manifest is assumed to be validated by the caller, but this function still provides deterministic,
|
|
154
|
+
* actionable errors when the expected structure or entries are missing.
|
|
155
|
+
*
|
|
156
|
+
* @param {object} manifest
|
|
157
|
+
* @param {string} targetTriple
|
|
158
|
+
* @returns {{targetTriple: string, asset: {name: string, id?: (string|number)}, integrity: {sha256: string}}}
|
|
159
|
+
*/
|
|
160
|
+
function resolveCanonicalAssetForTargetTriple(manifest, targetTriple) {
|
|
161
|
+
const manifestVersion = getManifestVersion(manifest);
|
|
162
|
+
|
|
163
|
+
if (typeof targetTriple !== "string" || !targetTriple.trim()) {
|
|
164
|
+
throw new ManifestResolutionError(
|
|
165
|
+
"DOCDEX_TARGET_TRIPLE_INVALID",
|
|
166
|
+
"Invalid target triple: expected a non-empty string",
|
|
167
|
+
{ targetTriple: null, manifestVersion }
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const needle = targetTriple.trim();
|
|
172
|
+
const entries = entriesFromManifest(manifest);
|
|
173
|
+
const matches = entries.filter((e) => e.targetTriple === needle);
|
|
174
|
+
|
|
175
|
+
if (matches.length === 0) {
|
|
176
|
+
const supported = getSupportedTargetTriples(manifest);
|
|
177
|
+
const supportedMsg = supported.length ? supported.join(", ") : "(none)";
|
|
178
|
+
throw new ManifestResolutionError(
|
|
179
|
+
"DOCDEX_ASSET_NO_MATCH",
|
|
180
|
+
`No asset found in manifest for target triple ${needle}. Supported: ${supportedMsg}`,
|
|
181
|
+
{ targetTriple: needle, manifestVersion, supported, assetName: null }
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (matches.length > 1) {
|
|
186
|
+
const simplified = matches
|
|
187
|
+
.map((m) => extractAssetAndIntegrity(m.entry).asset?.name || "(unknown)")
|
|
188
|
+
.sort();
|
|
189
|
+
throw new ManifestResolutionError(
|
|
190
|
+
"DOCDEX_ASSET_MULTI_MATCH",
|
|
191
|
+
`Multiple assets found in manifest for target triple ${needle}: ${simplified.join(", ")}`,
|
|
192
|
+
{ targetTriple: needle, manifestVersion, matches: simplified, assetName: null }
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const resolved = extractAssetAndIntegrity(matches[0].entry);
|
|
197
|
+
if (!resolved.asset?.name) {
|
|
198
|
+
throw new ManifestResolutionError(
|
|
199
|
+
"DOCDEX_ASSET_MALFORMED",
|
|
200
|
+
`Manifest entry for ${needle} is missing a canonical asset name/identifier`,
|
|
201
|
+
{ targetTriple: needle, manifestVersion, assetName: null }
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (!resolved.integrity?.sha256) {
|
|
206
|
+
throw new ManifestResolutionError(
|
|
207
|
+
"DOCDEX_ASSET_MALFORMED",
|
|
208
|
+
`Manifest entry for ${needle} is missing SHA-256 integrity metadata`,
|
|
209
|
+
{ targetTriple: needle, manifestVersion, assetName: resolved.asset.name }
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
targetTriple: needle,
|
|
215
|
+
manifestVersion,
|
|
216
|
+
asset: resolved.asset,
|
|
217
|
+
integrity: resolved.integrity
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
module.exports = {
|
|
222
|
+
ManifestResolutionError,
|
|
223
|
+
resolveCanonicalAssetForTargetTriple,
|
|
224
|
+
getSupportedTargetTriples,
|
|
225
|
+
getManifestVersion
|
|
226
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const crypto = require("node:crypto");
|
|
4
|
+
|
|
5
|
+
// Pinned verifier material for release integrity signatures.
|
|
6
|
+
//
|
|
7
|
+
// Notes:
|
|
8
|
+
// - This public key is used to verify detached signatures over integrity metadata files
|
|
9
|
+
// (e.g. `SHA256SUMS`, `docdex-release-manifest.json`) when present.
|
|
10
|
+
// - Forks can override via `DOCDEX_RELEASE_SIGNING_PUBLIC_KEY` (PEM string).
|
|
11
|
+
// - Official releases may rotate this key; rotation requires updating this value and
|
|
12
|
+
// re-signing release metadata with the corresponding private key.
|
|
13
|
+
const DEFAULT_RELEASE_SIGNING_PUBLIC_KEY_PEM = `-----BEGIN PUBLIC KEY-----
|
|
14
|
+
MCowBQYDK2VwAyEAuifOmifWm19cTMDxg6PVTBo3f997/P0qqTytnhUmjwE=
|
|
15
|
+
-----END PUBLIC KEY-----`;
|
|
16
|
+
|
|
17
|
+
const SIGNATURE_ALGORITHM = "ed25519";
|
|
18
|
+
const SIGNATURE_SUFFIX = ".sig";
|
|
19
|
+
|
|
20
|
+
function getSignaturePolicyFromEnv(envValue) {
|
|
21
|
+
const raw = String(envValue || "").trim().toLowerCase();
|
|
22
|
+
if (!raw) return "optional";
|
|
23
|
+
if (raw === "optional" || raw === "require" || raw === "required" || raw === "disabled" || raw === "off") return raw;
|
|
24
|
+
if (raw === "0" || raw === "false" || raw === "no") return "disabled";
|
|
25
|
+
if (raw === "1" || raw === "true" || raw === "yes") return "required";
|
|
26
|
+
return "invalid";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizePolicy(policy) {
|
|
30
|
+
if (policy === "require") return "required";
|
|
31
|
+
if (policy === "off") return "disabled";
|
|
32
|
+
return policy;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function signaturePolicy() {
|
|
36
|
+
return normalizePolicy(getSignaturePolicyFromEnv(process.env.DOCDEX_SIGNATURE_POLICY));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getReleaseSigningPublicKeyPem() {
|
|
40
|
+
const envKey = process.env.DOCDEX_RELEASE_SIGNING_PUBLIC_KEY;
|
|
41
|
+
if (envKey && String(envKey).trim()) return String(envKey).trim();
|
|
42
|
+
return DEFAULT_RELEASE_SIGNING_PUBLIC_KEY_PEM;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parseDetachedSignatureBase64(text) {
|
|
46
|
+
const value = String(text || "").trim();
|
|
47
|
+
if (!value) return null;
|
|
48
|
+
// Allow `ed25519:<base64>` for explicitness; otherwise treat as base64.
|
|
49
|
+
const parts = value.split(":");
|
|
50
|
+
const maybeB64 = parts.length === 2 ? parts[1].trim() : value;
|
|
51
|
+
if (!maybeB64) return null;
|
|
52
|
+
return maybeB64;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function verifyDetachedSignatureEd25519({ data, signatureText, publicKeyPem }) {
|
|
56
|
+
const sigB64 = parseDetachedSignatureBase64(signatureText);
|
|
57
|
+
if (!sigB64) return { ok: false, reason: "empty_signature" };
|
|
58
|
+
|
|
59
|
+
let signature;
|
|
60
|
+
try {
|
|
61
|
+
signature = Buffer.from(sigB64, "base64");
|
|
62
|
+
} catch {
|
|
63
|
+
return { ok: false, reason: "signature_not_base64" };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (signature.length !== 64) return { ok: false, reason: "signature_wrong_length" };
|
|
67
|
+
|
|
68
|
+
let keyObject;
|
|
69
|
+
try {
|
|
70
|
+
keyObject = crypto.createPublicKey(publicKeyPem);
|
|
71
|
+
} catch {
|
|
72
|
+
return { ok: false, reason: "public_key_invalid" };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const ok = crypto.verify(null, Buffer.isBuffer(data) ? data : Buffer.from(data), keyObject, signature);
|
|
77
|
+
return ok ? { ok: true } : { ok: false, reason: "signature_invalid" };
|
|
78
|
+
} catch {
|
|
79
|
+
return { ok: false, reason: "verify_failed" };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
module.exports = {
|
|
84
|
+
DEFAULT_RELEASE_SIGNING_PUBLIC_KEY_PEM,
|
|
85
|
+
SIGNATURE_ALGORITHM,
|
|
86
|
+
SIGNATURE_SUFFIX,
|
|
87
|
+
signaturePolicy,
|
|
88
|
+
getSignaturePolicyFromEnv,
|
|
89
|
+
getReleaseSigningPublicKeyPem,
|
|
90
|
+
parseDetachedSignatureBase64,
|
|
91
|
+
verifyDetachedSignatureEd25519
|
|
92
|
+
};
|
|
93
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "docdex",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Docdex CLI as an npm-installable binary wrapper.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"docdex": "bin/docdex.js",
|
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
"LICENSE"
|
|
15
15
|
],
|
|
16
16
|
"scripts": {
|
|
17
|
-
"postinstall": "node ./lib/install.js"
|
|
17
|
+
"postinstall": "node ./lib/install.js",
|
|
18
|
+
"test": "node --test",
|
|
19
|
+
"pack:verify": "node --test test/packaging_guardrails.test.js"
|
|
18
20
|
},
|
|
19
21
|
"engines": {
|
|
20
22
|
"node": ">=18"
|