graphify-sf 0.4.1 → 0.4.3
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/README.md +11 -5
- package/checksums.json +7 -0
- package/lib/download.js +121 -39
- package/package.json +8 -7
package/README.md
CHANGED
|
@@ -3,7 +3,12 @@
|
|
|
3
3
|
Install and run [graphify-sf](https://github.com/raykuonz/graphify-sf) — the Salesforce SFDX
|
|
4
4
|
knowledge-graph tool — from npm, without needing Python.
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
The pre-built binary for your OS/arch arrives with the install, inside a platform package
|
|
7
|
+
(`@graphify-sf/cli-<os>-<arch>`, an optional dependency); no download is needed. If that package
|
|
8
|
+
is missing (`--omit=optional`, or a registry mirror without the `@graphify-sf` scope), the binary
|
|
9
|
+
is downloaded from the GitHub Release instead. That download is checked against the sha256 shipped
|
|
10
|
+
in this package (`checksums.json`) and is refused if it doesn't match. Packages are published
|
|
11
|
+
from CI with npm provenance.
|
|
7
12
|
|
|
8
13
|
## Install
|
|
9
14
|
|
|
@@ -14,8 +19,8 @@ pnpm add graphify-sf
|
|
|
14
19
|
```
|
|
15
20
|
|
|
16
21
|
> **pnpm note:** pnpm sets `ignore-scripts=true` by default, so the `postinstall` step may not
|
|
17
|
-
> run automatically.
|
|
18
|
-
>
|
|
22
|
+
> run automatically. That only matters on the fallback path above: the binary is then downloaded
|
|
23
|
+
> (and verified) lazily on first use, so you may see a one-time download message.
|
|
19
24
|
|
|
20
25
|
## CLI usage
|
|
21
26
|
|
|
@@ -37,8 +42,9 @@ console.log("Graph written to:", graphJsonPath);
|
|
|
37
42
|
`runGraphify` returns a Promise that resolves with `{ code, graphJsonPath }` on success and
|
|
38
43
|
rejects on non-zero exit.
|
|
39
44
|
|
|
40
|
-
Additional exports: `ensureBinary()` (
|
|
41
|
-
|
|
45
|
+
Additional exports: `ensureBinary()` (resolves the binary, downloading and verifying it if
|
|
46
|
+
needed, and returns its path), `binaryPath()` (returns the fallback download path without
|
|
47
|
+
triggering a download).
|
|
42
48
|
|
|
43
49
|
## What's bundled
|
|
44
50
|
|
package/checksums.json
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"graphify-sf-darwin-arm64": "03096680fad912233f923d2eb25680e58cc483c0209e05dbb1878fc768912357",
|
|
3
|
+
"graphify-sf-darwin-x64": "f9f0a90b078b0450a1518596b90f2f37661b441d5db076637522d490e008ddb7",
|
|
4
|
+
"graphify-sf-linux-arm64": "8124d48ab7348086ab5c8e36a09fda7db5e0f82d6194fc2b27a58b9d509bcd32",
|
|
5
|
+
"graphify-sf-linux-x64": "9f708521f324f9018fae163d43704bf9f08412a649aa4f51e30fdbca492cc04e",
|
|
6
|
+
"graphify-sf-win32-x64.exe": "26645b75318621eaf5ef2620c0701c0ed250b1cd170d9e1ea13023909f40163a"
|
|
7
|
+
}
|
package/lib/download.js
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
+
const crypto = require("crypto");
|
|
3
4
|
const https = require("https");
|
|
4
5
|
const fs = require("fs");
|
|
5
6
|
const path = require("path");
|
|
7
|
+
const { Transform } = require("stream");
|
|
8
|
+
const { pipeline } = require("stream/promises");
|
|
6
9
|
|
|
7
10
|
const PKG = require("../package.json");
|
|
8
11
|
const VERSION = PKG.version;
|
|
9
12
|
|
|
13
|
+
// Release binaries are ~16-30 MB; anything far larger is not ours.
|
|
14
|
+
const MAX_DOWNLOAD_BYTES = 256 * 1024 * 1024;
|
|
15
|
+
// Abort a connection that stalls for this long (no bytes in either direction).
|
|
16
|
+
const DOWNLOAD_IDLE_TIMEOUT_MS = 60 * 1000;
|
|
17
|
+
|
|
10
18
|
const SUPPORTED = {
|
|
11
19
|
linux: ["x64", "arm64"],
|
|
12
20
|
darwin: ["x64", "arm64"],
|
|
@@ -50,12 +58,55 @@ function downloadUrl(asset) {
|
|
|
50
58
|
return `https://github.com/raykuonz/graphify-sf/releases/download/v${VERSION}/${asset}`;
|
|
51
59
|
}
|
|
52
60
|
|
|
61
|
+
function checksumsPath() {
|
|
62
|
+
return path.join(__dirname, "..", "checksums.json");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// sha256 the release workflow recorded for `asset`. checksums.json is written
|
|
66
|
+
// at publish time and ships inside the npm tarball, which lockfiles pin by
|
|
67
|
+
// integrity; GitHub release assets can be replaced under an existing tag, so a
|
|
68
|
+
// downloaded binary is trusted only if it matches. Throws when there is no
|
|
69
|
+
// usable entry (e.g. a source checkout), so callers fail closed.
|
|
70
|
+
function expectedSha256(asset, file) {
|
|
71
|
+
let table = null;
|
|
72
|
+
try {
|
|
73
|
+
table = JSON.parse(fs.readFileSync(file || checksumsPath(), "utf8"));
|
|
74
|
+
} catch {
|
|
75
|
+
/* missing or unreadable: handled below */
|
|
76
|
+
}
|
|
77
|
+
const digest = table && Object.hasOwn(table, asset) ? table[asset] : undefined;
|
|
78
|
+
if (typeof digest !== "string" || !/^[0-9a-f]{64}$/.test(digest)) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
`graphify-sf: no published sha256 for ${asset} (checksums.json missing or incomplete); ` +
|
|
81
|
+
`refusing to download an unverifiable binary. ` +
|
|
82
|
+
`Remediation: install via pip/pipx instead: pipx install graphify-sf, ` +
|
|
83
|
+
`or point GRAPHIFY_BIN at an existing binary`
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
return digest;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function sha256File(file) {
|
|
90
|
+
return crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// True only if `dest` exists and matches the published sha256 for this platform.
|
|
94
|
+
function verifiedCachedBinary(dest, file) {
|
|
95
|
+
if (!fs.existsSync(dest)) return false;
|
|
96
|
+
try {
|
|
97
|
+
return sha256File(dest) === expectedSha256(assetName(), file);
|
|
98
|
+
} catch {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
53
103
|
function fetchFollow(url, redirects) {
|
|
54
104
|
if (redirects === undefined) redirects = 5;
|
|
55
105
|
return new Promise((resolve, reject) => {
|
|
56
|
-
https
|
|
106
|
+
const req = https
|
|
57
107
|
.get(url, { headers: { "User-Agent": "graphify-sf-npm-installer" } }, (res) => {
|
|
58
108
|
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
109
|
+
res.resume(); // discard the redirect body so the socket is released
|
|
59
110
|
if (redirects === 0) {
|
|
60
111
|
reject(new Error(`graphify-sf: too many redirects downloading ${url}`));
|
|
61
112
|
return;
|
|
@@ -64,6 +115,7 @@ function fetchFollow(url, redirects) {
|
|
|
64
115
|
return;
|
|
65
116
|
}
|
|
66
117
|
if (res.statusCode !== 200) {
|
|
118
|
+
res.resume();
|
|
67
119
|
reject(
|
|
68
120
|
new Error(
|
|
69
121
|
`graphify-sf: download failed with HTTP ${res.statusCode} from ${url}. ` +
|
|
@@ -75,49 +127,72 @@ function fetchFollow(url, redirects) {
|
|
|
75
127
|
resolve(res);
|
|
76
128
|
})
|
|
77
129
|
.on("error", reject);
|
|
130
|
+
req.setTimeout(DOWNLOAD_IDLE_TIMEOUT_MS, () =>
|
|
131
|
+
req.destroy(new Error(`graphify-sf: download stalled for ${DOWNLOAD_IDLE_TIMEOUT_MS / 1000}s at ${url}`))
|
|
132
|
+
);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Pass-through that errors once more than `max` bytes have flowed.
|
|
137
|
+
function byteLimit(max, url) {
|
|
138
|
+
let seen = 0;
|
|
139
|
+
return new Transform({
|
|
140
|
+
transform(chunk, _encoding, callback) {
|
|
141
|
+
seen += chunk.length;
|
|
142
|
+
if (seen > max) {
|
|
143
|
+
callback(new Error(`graphify-sf: download from ${url} exceeded ${max} bytes; refusing it`));
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
callback(null, chunk);
|
|
147
|
+
},
|
|
78
148
|
});
|
|
79
149
|
}
|
|
80
150
|
|
|
81
|
-
|
|
151
|
+
// Download this platform's binary from GitHub Releases, verifying its sha256
|
|
152
|
+
// against checksums.json before it is renamed into place. The hash is taken
|
|
153
|
+
// from the bytes on disk, in a temp file private to this call, so concurrent
|
|
154
|
+
// first runs cannot interleave writes into a file another one verified.
|
|
155
|
+
// `opts` (dest, checksumsPath, fetch, maxBytes) exists for tests; production
|
|
156
|
+
// callers pass nothing. Progress goes to stderr so it never mixes into the
|
|
157
|
+
// CLI's stdout (e.g. `graphify-sf ... | jq`, or runGraphify with piped stdio).
|
|
158
|
+
async function download(opts) {
|
|
159
|
+
const o = opts || {};
|
|
82
160
|
const asset = assetName();
|
|
161
|
+
const expected = expectedSha256(asset, o.checksumsPath); // fails before any network request
|
|
83
162
|
const url = downloadUrl(asset);
|
|
84
|
-
const dest = binaryPath();
|
|
85
|
-
|
|
86
|
-
fs.mkdirSync(binDir(), { recursive: true });
|
|
163
|
+
const dest = o.dest || binaryPath();
|
|
164
|
+
const fetch = o.fetch || fetchFollow;
|
|
87
165
|
|
|
88
|
-
|
|
166
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
89
167
|
|
|
90
|
-
|
|
168
|
+
process.stderr.write(`graphify-sf: downloading ${asset} from ${url}\n`);
|
|
91
169
|
|
|
92
|
-
await
|
|
93
|
-
const tmp = dest + ".tmp";
|
|
94
|
-
const out = fs.createWriteStream(tmp);
|
|
95
|
-
res.pipe(out);
|
|
96
|
-
out.on("finish", () => {
|
|
97
|
-
out.close(() => {
|
|
98
|
-
try {
|
|
99
|
-
fs.renameSync(tmp, dest);
|
|
100
|
-
resolve();
|
|
101
|
-
} catch (e) {
|
|
102
|
-
reject(e);
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
out.on("error", (e) => {
|
|
107
|
-
fs.unlink(tmp, () => {});
|
|
108
|
-
reject(e);
|
|
109
|
-
});
|
|
110
|
-
res.on("error", (e) => {
|
|
111
|
-
fs.unlink(tmp, () => {});
|
|
112
|
-
reject(e);
|
|
113
|
-
});
|
|
114
|
-
});
|
|
170
|
+
const res = await fetch(url);
|
|
115
171
|
|
|
116
|
-
|
|
117
|
-
|
|
172
|
+
const tmp = `${dest}.${process.pid}.${crypto.randomBytes(6).toString("hex")}.tmp`;
|
|
173
|
+
try {
|
|
174
|
+
// pipeline destroys every stream on error or premature close; "wx" refuses
|
|
175
|
+
// to reuse an existing path.
|
|
176
|
+
await pipeline(res, byteLimit(o.maxBytes || MAX_DOWNLOAD_BYTES, url), fs.createWriteStream(tmp, { flags: "wx" }));
|
|
177
|
+
const actual = sha256File(tmp);
|
|
178
|
+
if (actual !== expected) {
|
|
179
|
+
throw new Error(
|
|
180
|
+
`graphify-sf: sha256 mismatch for ${asset} from ${url} ` +
|
|
181
|
+
`(expected ${expected}, got ${actual}); refusing to install it. ` +
|
|
182
|
+
`Remediation: install via pip/pipx instead: pipx install graphify-sf`
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
// Executable before it becomes visible at `dest`.
|
|
186
|
+
if (process.platform !== "win32") {
|
|
187
|
+
fs.chmodSync(tmp, 0o755);
|
|
188
|
+
}
|
|
189
|
+
fs.renameSync(tmp, dest);
|
|
190
|
+
} catch (e) {
|
|
191
|
+
fs.rmSync(tmp, { force: true });
|
|
192
|
+
throw e;
|
|
118
193
|
}
|
|
119
194
|
|
|
120
|
-
process.
|
|
195
|
+
process.stderr.write(`graphify-sf: installed to ${dest}\n`);
|
|
121
196
|
}
|
|
122
197
|
|
|
123
198
|
// Resolve a user-supplied binary path from the GRAPHIFY_BIN environment
|
|
@@ -189,7 +264,9 @@ function subpackageBinary() {
|
|
|
189
264
|
return null;
|
|
190
265
|
}
|
|
191
266
|
|
|
192
|
-
async function ensureBinary() {
|
|
267
|
+
async function ensureBinary(opts) {
|
|
268
|
+
const o = opts || {};
|
|
269
|
+
|
|
193
270
|
// 1. Explicit override always wins.
|
|
194
271
|
const fromEnv = envBinary();
|
|
195
272
|
if (fromEnv) {
|
|
@@ -203,14 +280,17 @@ async function ensureBinary() {
|
|
|
203
280
|
return fromSubpkg;
|
|
204
281
|
}
|
|
205
282
|
|
|
206
|
-
// 3.
|
|
207
|
-
|
|
208
|
-
|
|
283
|
+
// 3. A binary an earlier run downloaded into bin/, but only while it still
|
|
284
|
+
// matches the published sha256. A stale or tampered file is left for
|
|
285
|
+
// download() to replace atomically (rename); it is never returned, and
|
|
286
|
+
// deleting it here could race another process's freshly verified rename.
|
|
287
|
+
const dest = o.dest || binaryPath();
|
|
288
|
+
if (verifiedCachedBinary(dest, o.checksumsPath)) {
|
|
209
289
|
return dest;
|
|
210
290
|
}
|
|
211
291
|
|
|
212
|
-
// 4. Last resort: lazily download from GitHub Releases
|
|
213
|
-
await download();
|
|
292
|
+
// 4. Last resort: lazily download (and verify) from GitHub Releases.
|
|
293
|
+
await download(o);
|
|
214
294
|
return dest;
|
|
215
295
|
}
|
|
216
296
|
|
|
@@ -221,6 +301,8 @@ module.exports = {
|
|
|
221
301
|
download,
|
|
222
302
|
ensureBinary,
|
|
223
303
|
envBinary,
|
|
304
|
+
expectedSha256,
|
|
224
305
|
subpackageName,
|
|
225
306
|
subpackageBinary,
|
|
307
|
+
verifiedCachedBinary,
|
|
226
308
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "graphify-sf",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"description": "Turn any Salesforce SFDX project into a queryable knowledge graph — no Python required.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"graphify-sf": "bin/graphify-sf.js"
|
|
@@ -11,17 +11,18 @@
|
|
|
11
11
|
"test": "node --test"
|
|
12
12
|
},
|
|
13
13
|
"optionalDependencies": {
|
|
14
|
-
"@graphify-sf/cli-linux-x64": "0.4.
|
|
15
|
-
"@graphify-sf/cli-linux-arm64": "0.4.
|
|
16
|
-
"@graphify-sf/cli-darwin-x64": "0.4.
|
|
17
|
-
"@graphify-sf/cli-darwin-arm64": "0.4.
|
|
18
|
-
"@graphify-sf/cli-win32-x64": "0.4.
|
|
14
|
+
"@graphify-sf/cli-linux-x64": "0.4.3",
|
|
15
|
+
"@graphify-sf/cli-linux-arm64": "0.4.3",
|
|
16
|
+
"@graphify-sf/cli-darwin-x64": "0.4.3",
|
|
17
|
+
"@graphify-sf/cli-darwin-arm64": "0.4.3",
|
|
18
|
+
"@graphify-sf/cli-win32-x64": "0.4.3"
|
|
19
19
|
},
|
|
20
20
|
"files": [
|
|
21
|
-
"bin/",
|
|
21
|
+
"bin/graphify-sf.js",
|
|
22
22
|
"lib/",
|
|
23
23
|
"index.js",
|
|
24
24
|
"install.js",
|
|
25
|
+
"checksums.json",
|
|
25
26
|
"README.md"
|
|
26
27
|
],
|
|
27
28
|
"engines": {
|