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.
@@ -0,0 +1,134 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const DEFAULT_COMPONENT = "docdex-installer";
5
+
6
+ function parseBooleanEnv(value, defaultValue) {
7
+ if (value == null) return defaultValue;
8
+ const normalized = String(value).trim().toLowerCase();
9
+ if (!normalized) return defaultValue;
10
+ if (["1", "true", "yes", "y", "on"].includes(normalized)) return true;
11
+ if (["0", "false", "no", "n", "off"].includes(normalized)) return false;
12
+ return defaultValue;
13
+ }
14
+
15
+ function redactAuthHeader(value) {
16
+ if (typeof value !== "string") return value;
17
+ return value.replace(/\b(Bearer)\s+([^\s]+)/gi, "$1 [REDACTED]");
18
+ }
19
+
20
+ function shouldRedactKey(key) {
21
+ return /token|authorization|password|secret|signature|sig|credential|private[_-]?key|api[_-]?key/i.test(String(key));
22
+ }
23
+
24
+ function shouldRedactUrlParam(key) {
25
+ const k = String(key).toLowerCase();
26
+ if (k.startsWith("x-amz-")) return true;
27
+ return /token|access_token|auth|signature|sig|key|credential|session/i.test(k);
28
+ }
29
+
30
+ function redactUrl(url) {
31
+ if (typeof url !== "string") return url;
32
+ const value = url.trim();
33
+ if (!value) return value;
34
+
35
+ try {
36
+ const parsed = new URL(value);
37
+ parsed.username = "";
38
+ parsed.password = "";
39
+ for (const [k] of parsed.searchParams.entries()) {
40
+ if (shouldRedactUrlParam(k)) parsed.searchParams.set(k, "REDACTED");
41
+ }
42
+ return parsed.toString();
43
+ } catch {
44
+ // Best-effort redaction for non-URL strings.
45
+ return value
46
+ .replace(/\/\/([^/@:\s]+):([^/@\s]+)@/g, "//REDACTED:REDACTED@")
47
+ .replace(/([?&])(token|access_token|auth|signature|sig|key|credential)=([^&]+)/gi, "$1$2=REDACTED");
48
+ }
49
+ }
50
+
51
+ function redactValue(value, depth = 0) {
52
+ if (depth > 6) return "[REDACTED_DEPTH]";
53
+ if (value == null) return value;
54
+
55
+ if (typeof value === "string") {
56
+ const withRedactedUrls = value.replace(/https?:\/\/[^\s"'()<>]+/gi, (match) => redactUrl(match));
57
+ const maybeUrl = /^https?:\/\//i.test(withRedactedUrls) ? redactUrl(withRedactedUrls) : withRedactedUrls;
58
+ return redactAuthHeader(maybeUrl);
59
+ }
60
+
61
+ if (Array.isArray(value)) return value.map((v) => redactValue(v, depth + 1));
62
+
63
+ if (typeof value === "object") {
64
+ const out = {};
65
+ for (const [k, v] of Object.entries(value)) {
66
+ if (shouldRedactKey(k)) {
67
+ out[k] = "[REDACTED]";
68
+ continue;
69
+ }
70
+ if (/url$/i.test(k) || /_url$/i.test(k) || /url_/i.test(k) || /Url$/.test(k)) {
71
+ out[k] = redactUrl(typeof v === "string" ? v : String(v ?? ""));
72
+ continue;
73
+ }
74
+ out[k] = redactValue(v, depth + 1);
75
+ }
76
+ return out;
77
+ }
78
+
79
+ return value;
80
+ }
81
+
82
+ function safeJsonStringify(value) {
83
+ const seen = new WeakSet();
84
+ return JSON.stringify(value, (key, val) => {
85
+ if (val && typeof val === "object") {
86
+ if (seen.has(val)) return "[Circular]";
87
+ seen.add(val);
88
+ }
89
+ return val;
90
+ });
91
+ }
92
+
93
+ function createInstallerStructuredLogger({
94
+ component = DEFAULT_COMPONENT,
95
+ baseFields = {},
96
+ enabled = parseBooleanEnv(process.env.DOCDEX_INSTALLER_STRUCTURED_LOG, false),
97
+ sink = process.stderr
98
+ } = {}) {
99
+ function emit({ level = "info", event, message, fields, error } = {}) {
100
+ if (!enabled) return;
101
+ const payload = {
102
+ ts: new Date().toISOString(),
103
+ level,
104
+ component,
105
+ event: typeof event === "string" ? event : "installer.event",
106
+ message: typeof message === "string" ? message : null,
107
+ ...redactValue(baseFields),
108
+ ...redactValue(fields || {})
109
+ };
110
+
111
+ if (error) {
112
+ payload.error = redactValue({
113
+ name: error?.name || null,
114
+ code: typeof error?.code === "string" ? error.code : null,
115
+ message: error?.message || String(error)
116
+ });
117
+ }
118
+
119
+ try {
120
+ sink.write(`${safeJsonStringify(payload)}\n`);
121
+ } catch {
122
+ // Best-effort: never fail install due to logging.
123
+ }
124
+ }
125
+
126
+ return { emit, enabled };
127
+ }
128
+
129
+ module.exports = {
130
+ createInstallerStructuredLogger,
131
+ parseBooleanEnv,
132
+ redactUrl,
133
+ redactValue
134
+ };
package/lib/platform.js CHANGED
@@ -1,48 +1,303 @@
1
1
  "use strict";
2
2
 
3
+ const fs = require("node:fs");
4
+
5
+ const {
6
+ PLATFORM_ENTRY_BY_KEY,
7
+ PUBLISHED_PLATFORM_KEYS,
8
+ PUBLISHED_TARGET_TRIPLES,
9
+ assetNameForPlatformKey
10
+ } = require("./platform_matrix");
11
+
12
+ class UnsupportedPlatformError extends Error {
13
+ /**
14
+ * @param {{
15
+ * platform: string,
16
+ * arch: string,
17
+ * libc?: (null|"gnu"|"musl"),
18
+ * candidatePlatformKey?: (string|null),
19
+ * candidateTargetTriple?: (string|null),
20
+ * reason?: (string|null),
21
+ * supportedPlatformKeys: string[],
22
+ * supportedTargetTriples: string[]
23
+ * }} options
24
+ */
25
+ constructor({
26
+ platform,
27
+ arch,
28
+ libc = null,
29
+ candidatePlatformKey = null,
30
+ candidateTargetTriple = null,
31
+ reason = null,
32
+ supportedPlatformKeys,
33
+ supportedTargetTriples
34
+ }) {
35
+ super(`Unsupported platform: ${platform}/${arch}`);
36
+ this.name = "UnsupportedPlatformError";
37
+ this.code = "DOCDEX_UNSUPPORTED_PLATFORM";
38
+ this.exitCode = 3;
39
+ this.details = {
40
+ targetTriple: null,
41
+ manifestVersion: null,
42
+ assetName: null,
43
+ platform,
44
+ arch,
45
+ libc,
46
+ candidatePlatformKey,
47
+ candidateTargetTriple,
48
+ reason,
49
+ supportedPlatformKeys: supportedPlatformKeys || [],
50
+ supportedTargetTriples: supportedTargetTriples || []
51
+ };
52
+ }
53
+ }
54
+
55
+ const SUPPORTED_PLATFORM_KEYS = PUBLISHED_PLATFORM_KEYS;
56
+ const SUPPORTED_TARGET_TRIPLES = PUBLISHED_TARGET_TRIPLES;
57
+
58
+ function normalizeLibc(value) {
59
+ if (value == null) return null;
60
+ const libc = String(value).toLowerCase().trim();
61
+ if (!libc) return null;
62
+ if (libc === "glibc") return "gnu";
63
+ if (libc === "musl" || libc === "gnu") return libc;
64
+ return null;
65
+ }
66
+
67
+ function readFileSliceSync(fsModule, filePath, offset, length) {
68
+ const fd = fsModule.openSync(filePath, "r");
69
+ try {
70
+ const buf = Buffer.alloc(length);
71
+ const bytesRead = fsModule.readSync(fd, buf, 0, length, offset);
72
+ return bytesRead === length ? buf : buf.subarray(0, bytesRead);
73
+ } finally {
74
+ fsModule.closeSync(fd);
75
+ }
76
+ }
77
+
78
+ function detectLibcFromElfInterpreter(execPath, fsModule) {
79
+ if (!execPath || typeof execPath !== "string") return null;
80
+ try {
81
+ const header = readFileSliceSync(fsModule, execPath, 0, 64);
82
+ if (header.length < 64) return null;
83
+ if (header[0] !== 0x7f || header[1] !== 0x45 || header[2] !== 0x4c || header[3] !== 0x46) return null;
84
+
85
+ const eiClass = header[4]; // 1=32-bit, 2=64-bit
86
+ const eiData = header[5]; // 1=little-endian
87
+ if (eiData !== 1) return null;
88
+
89
+ const PT_INTERP = 3;
90
+
91
+ let phoff;
92
+ let phentsize;
93
+ let phnum;
94
+
95
+ if (eiClass === 2) {
96
+ phoff = Number(header.readBigUInt64LE(32));
97
+ phentsize = header.readUInt16LE(54);
98
+ phnum = header.readUInt16LE(56);
99
+ } else if (eiClass === 1) {
100
+ phoff = header.readUInt32LE(28);
101
+ phentsize = header.readUInt16LE(42);
102
+ phnum = header.readUInt16LE(44);
103
+ } else {
104
+ return null;
105
+ }
106
+
107
+ if (!phoff || !phentsize || !phnum) return null;
108
+ if (phnum > 64) return null;
109
+ if (phentsize < 32 || phentsize > 128) return null;
110
+
111
+ const tableSize = phentsize * phnum;
112
+ const phTable = readFileSliceSync(fsModule, execPath, phoff, tableSize);
113
+ if (phTable.length < tableSize) return null;
114
+
115
+ for (let i = 0; i < phnum; i++) {
116
+ const base = i * phentsize;
117
+ const pType = phTable.readUInt32LE(base);
118
+ if (pType !== PT_INTERP) continue;
119
+
120
+ let pOffset;
121
+ let pFileSz;
122
+
123
+ if (eiClass === 2) {
124
+ pOffset = Number(phTable.readBigUInt64LE(base + 8));
125
+ pFileSz = Number(phTable.readBigUInt64LE(base + 32));
126
+ } else {
127
+ pOffset = phTable.readUInt32LE(base + 4);
128
+ pFileSz = phTable.readUInt32LE(base + 16);
129
+ }
130
+
131
+ if (!pOffset || !pFileSz || pFileSz > 4096) return null;
132
+ const interpBytes = readFileSliceSync(fsModule, execPath, pOffset, pFileSz);
133
+ const interp = interpBytes.toString("utf8").split("\0")[0];
134
+ if (!interp) return null;
135
+ if (interp.includes("ld-musl")) return "musl";
136
+ if (interp.includes("ld-linux")) return "gnu";
137
+ return null;
138
+ }
139
+ } catch {
140
+ return null;
141
+ }
142
+
143
+ return null;
144
+ }
145
+
3
146
  function detectLibc() {
4
- const override = process.env.DOCDEX_LIBC;
5
- if (override) {
6
- const libc = override.toLowerCase();
7
- if (libc === "musl" || libc === "gnu") {
8
- return libc;
147
+ return detectLibcFromRuntime();
148
+ }
149
+
150
+ function detectLibcFromRuntime(options) {
151
+ const env = options?.env ?? process.env;
152
+ const fsModule = options?.fs ?? fs;
153
+
154
+ const overrideRaw = env?.DOCDEX_LIBC;
155
+ if (overrideRaw != null && String(overrideRaw).trim() !== "") {
156
+ const override = normalizeLibc(overrideRaw);
157
+ if (!override) {
158
+ throw new Error(`Invalid DOCDEX_LIBC=${overrideRaw}; expected "gnu", "musl", or "glibc"`);
9
159
  }
160
+ return override;
10
161
  }
11
162
 
12
- const report = typeof process.report?.getReport === "function" ? process.report.getReport() : null;
163
+ const report =
164
+ options?.report ??
165
+ (typeof process.report?.getReport === "function" ? process.report.getReport() : null);
166
+
13
167
  const glibcVersion = report?.header?.glibcVersionRuntime;
14
- return glibcVersion ? "gnu" : "musl";
168
+ if (typeof glibcVersion === "string" && glibcVersion.trim()) return "gnu";
169
+
170
+ // Some Node builds include a `musl` field in the report header. Treat it as a positive signal.
171
+ if (report?.header?.musl != null) return "musl";
172
+
173
+ // Alpine is musl-based by default; use it as a strong hint.
174
+ try {
175
+ if (typeof fsModule.existsSync === "function" && fsModule.existsSync("/etc/alpine-release")) return "musl";
176
+ } catch {}
177
+
178
+ const execPath = options?.execPath ?? process.execPath;
179
+ const fromElf = detectLibcFromElfInterpreter(execPath, fsModule);
180
+ if (fromElf) return fromElf;
181
+
182
+ // Deterministic fallback: musl binaries are typically more portable on glibc than the reverse.
183
+ return "musl";
15
184
  }
16
185
 
17
- function detectPlatformKey() {
18
- const platform = process.platform;
19
- const arch = process.arch;
186
+ function detectPlatformKey(options) {
187
+ const platform = options?.platform ?? process.platform;
188
+ const arch = options?.arch ?? process.arch;
189
+ let libc = null;
190
+ let candidatePlatformKey = null;
191
+ let candidateTargetTriple = null;
20
192
 
21
193
  if (platform === "darwin") {
22
- if (arch === "arm64") return "darwin-arm64";
23
- if (arch === "x64") return "darwin-x64";
194
+ if (arch === "arm64") candidatePlatformKey = "darwin-arm64";
195
+ if (arch === "x64") candidatePlatformKey = "darwin-x64";
24
196
  }
25
197
 
26
198
  if (platform === "linux") {
27
- const libc = detectLibc();
28
- if (arch === "arm64") return `linux-arm64-${libc}`;
29
- if (arch === "x64") return `linux-x64-${libc}`;
199
+ libc = normalizeLibc(options?.libc) ?? detectLibcFromRuntime(options);
200
+ if (arch === "arm64") candidatePlatformKey = `linux-arm64-${libc}`;
201
+ if (arch === "x64") candidatePlatformKey = `linux-x64-${libc}`;
30
202
  }
31
203
 
32
204
  if (platform === "win32") {
33
- if (arch === "x64") return "win32-x64";
34
- if (arch === "arm64") return "win32-arm64";
205
+ if (arch === "x64") candidatePlatformKey = "win32-x64";
206
+ if (arch === "arm64") candidatePlatformKey = "win32-arm64";
35
207
  }
36
208
 
37
- throw new Error(`Unsupported platform: ${platform}/${arch}`);
209
+ if (candidatePlatformKey) {
210
+ const entry = PLATFORM_ENTRY_BY_KEY[candidatePlatformKey];
211
+ candidateTargetTriple = entry?.targetTriple ?? null;
212
+ if (entry?.published) return candidatePlatformKey;
213
+ if (entry && entry.published === false) {
214
+ throw new UnsupportedPlatformError({
215
+ platform,
216
+ arch,
217
+ libc,
218
+ candidatePlatformKey,
219
+ candidateTargetTriple,
220
+ reason: "target_not_published",
221
+ supportedPlatformKeys: SUPPORTED_PLATFORM_KEYS,
222
+ supportedTargetTriples: SUPPORTED_TARGET_TRIPLES
223
+ });
224
+ }
225
+ }
226
+
227
+ throw new UnsupportedPlatformError({
228
+ platform,
229
+ arch,
230
+ libc,
231
+ candidatePlatformKey,
232
+ candidateTargetTriple,
233
+ reason: "unknown_or_unsupported_runtime",
234
+ supportedPlatformKeys: SUPPORTED_PLATFORM_KEYS,
235
+ supportedTargetTriples: SUPPORTED_TARGET_TRIPLES
236
+ });
38
237
  }
39
238
 
40
239
  function artifactName(platformKey) {
41
- return `docdexd-${platformKey}.tar.gz`;
240
+ return assetNameForPlatformKey(platformKey);
241
+ }
242
+
243
+ /**
244
+ * Human-readable release asset naming pattern for `docdexd` archives.
245
+ * @param {string|null} [platformKey]
246
+ * @param {{includeExample?: boolean, exampleAssetName?: string}} [options]
247
+ */
248
+ function assetPatternForPlatformKey(platformKey, options) {
249
+ const includeExample = options?.includeExample !== false;
250
+ const exampleAssetName =
251
+ typeof options?.exampleAssetName === "string" && options.exampleAssetName.trim()
252
+ ? options.exampleAssetName.trim()
253
+ : null;
254
+ const base = "docdexd-<platformKey>.tar.gz";
255
+ if (!platformKey || typeof platformKey !== "string") return base;
256
+ if (!includeExample) return base;
257
+ return `${base} (e.g. ${exampleAssetName || assetNameForPlatformKey(platformKey)})`;
258
+ }
259
+
260
+ function targetTripleForPlatformKey(platformKey) {
261
+ const triple = PLATFORM_ENTRY_BY_KEY[platformKey]?.targetTriple;
262
+ if (triple) return triple;
263
+ throw new Error(
264
+ `Unsupported platform key: ${platformKey}. Supported: ${Object.keys(PLATFORM_ENTRY_BY_KEY).sort().join(", ")}`
265
+ );
266
+ }
267
+
268
+ function detectTargetTriple(options) {
269
+ return targetTripleForPlatformKey(detectPlatformKey(options));
270
+ }
271
+
272
+ /**
273
+ * Resolve the full platform support policy (runtime → supported? → target triple + asset naming).
274
+ * Consumers should use this as the single source of truth for distinguishing unsupported platforms
275
+ * from missing release artifacts (supported-but-missing).
276
+ *
277
+ * @param {Parameters<typeof detectPlatformKey>[0]} [options]
278
+ */
279
+ function resolvePlatformPolicy(options) {
280
+ const platform = options?.platform ?? process.platform;
281
+ const arch = options?.arch ?? process.arch;
282
+ const platformKey = detectPlatformKey(options);
283
+ const targetTriple = targetTripleForPlatformKey(platformKey);
284
+ return {
285
+ detected: { platform, arch },
286
+ platformKey,
287
+ targetTriple,
288
+ expectedAssetName: artifactName(platformKey),
289
+ expectedAssetPattern: assetPatternForPlatformKey(platformKey)
290
+ };
42
291
  }
43
292
 
44
293
  module.exports = {
45
294
  detectLibc,
295
+ detectLibcFromRuntime,
46
296
  detectPlatformKey,
47
- artifactName
297
+ UnsupportedPlatformError,
298
+ artifactName,
299
+ assetPatternForPlatformKey,
300
+ targetTripleForPlatformKey,
301
+ detectTargetTriple,
302
+ resolvePlatformPolicy
48
303
  };
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Single source of truth for:
5
+ * detected runtime (platform/arch/libc) -> platformKey -> Rust target triple -> release asset naming.
6
+ * Contract: docs/contracts/installer_platform_mapping_v1.md
7
+ *
8
+ * Notes:
9
+ * - `platformKey` is the suffix used in release assets: `docdexd-<platformKey>.tar.gz`.
10
+ * - Some mappings are defined but marked `published: false` when the release workflow does not
11
+ * currently publish a matching binary artifact; these MUST be treated as unsupported at install/runtime.
12
+ */
13
+
14
+ /** @type {{platform: string, arch: string, libc: (null|"gnu"|"musl"), platformKey: string, targetTriple: string, published: boolean}[]} */
15
+ const PLATFORM_MATRIX = Object.freeze([
16
+ {
17
+ platform: "darwin",
18
+ arch: "arm64",
19
+ libc: null,
20
+ platformKey: "darwin-arm64",
21
+ targetTriple: "aarch64-apple-darwin",
22
+ published: true
23
+ },
24
+ {
25
+ platform: "darwin",
26
+ arch: "x64",
27
+ libc: null,
28
+ platformKey: "darwin-x64",
29
+ targetTriple: "x86_64-apple-darwin",
30
+ published: true
31
+ },
32
+ {
33
+ platform: "linux",
34
+ arch: "x64",
35
+ libc: "gnu",
36
+ platformKey: "linux-x64-gnu",
37
+ targetTriple: "x86_64-unknown-linux-gnu",
38
+ published: true
39
+ },
40
+ {
41
+ platform: "linux",
42
+ arch: "x64",
43
+ libc: "musl",
44
+ platformKey: "linux-x64-musl",
45
+ targetTriple: "x86_64-unknown-linux-musl",
46
+ published: true
47
+ },
48
+ {
49
+ platform: "linux",
50
+ arch: "arm64",
51
+ libc: "gnu",
52
+ platformKey: "linux-arm64-gnu",
53
+ targetTriple: "aarch64-unknown-linux-gnu",
54
+ published: true
55
+ },
56
+ {
57
+ platform: "linux",
58
+ arch: "arm64",
59
+ libc: "musl",
60
+ platformKey: "linux-arm64-musl",
61
+ targetTriple: "aarch64-unknown-linux-musl",
62
+ published: false
63
+ },
64
+ {
65
+ platform: "win32",
66
+ arch: "x64",
67
+ libc: null,
68
+ platformKey: "win32-x64",
69
+ targetTriple: "x86_64-pc-windows-msvc",
70
+ published: true
71
+ },
72
+ {
73
+ platform: "win32",
74
+ arch: "arm64",
75
+ libc: null,
76
+ platformKey: "win32-arm64",
77
+ targetTriple: "aarch64-pc-windows-msvc",
78
+ published: false
79
+ }
80
+ ]);
81
+
82
+ const PLATFORM_ENTRY_BY_KEY = Object.freeze(
83
+ PLATFORM_MATRIX.reduce((acc, entry) => {
84
+ acc[entry.platformKey] = entry;
85
+ return acc;
86
+ }, {})
87
+ );
88
+
89
+ const PUBLISHED_PLATFORM_KEYS = Object.freeze(
90
+ PLATFORM_MATRIX.filter((e) => e.published)
91
+ .map((e) => e.platformKey)
92
+ .slice()
93
+ .sort()
94
+ );
95
+
96
+ const PUBLISHED_TARGET_TRIPLES = Object.freeze(
97
+ [...new Set(PLATFORM_MATRIX.filter((e) => e.published).map((e) => e.targetTriple))].sort()
98
+ );
99
+
100
+ function archiveBaseForPlatformKey(platformKey) {
101
+ return `docdexd-${platformKey}`;
102
+ }
103
+
104
+ function assetNameForPlatformKey(platformKey) {
105
+ return `${archiveBaseForPlatformKey(platformKey)}.tar.gz`;
106
+ }
107
+
108
+ /**
109
+ * Default target list for release-manifest generation (published targets only).
110
+ * Shape matches scripts/generate_release_manifest.cjs `targets` input.
111
+ */
112
+ const PUBLISHED_RELEASE_TARGETS = Object.freeze(
113
+ PLATFORM_MATRIX.filter((e) => e.published)
114
+ .map((e) => ({ targetTriple: e.targetTriple, archiveBase: archiveBaseForPlatformKey(e.platformKey) }))
115
+ .slice()
116
+ .sort((a, b) => a.targetTriple.localeCompare(b.targetTriple))
117
+ );
118
+
119
+ module.exports = {
120
+ PLATFORM_MATRIX,
121
+ PLATFORM_ENTRY_BY_KEY,
122
+ PUBLISHED_PLATFORM_KEYS,
123
+ PUBLISHED_TARGET_TRIPLES,
124
+ PUBLISHED_RELEASE_TARGETS,
125
+ archiveBaseForPlatformKey,
126
+ assetNameForPlatformKey
127
+ };