wuphf 0.155.1 → 0.157.0
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/package.json +3 -1
- package/scripts/cloudflared.json +26 -0
- package/scripts/download-cloudflared.js +233 -0
- package/scripts/postinstall.js +29 -1
package/package.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wuphf",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.157.0",
|
|
4
4
|
"description": "Slack for AI employees with a shared brain. A collaborative office where AI employees run your work 24x7.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"wuphf": "bin/wuphf.js"
|
|
7
7
|
},
|
|
8
8
|
"files": [
|
|
9
9
|
"bin/wuphf.js",
|
|
10
|
+
"scripts/cloudflared.json",
|
|
10
11
|
"scripts/download-binary.js",
|
|
12
|
+
"scripts/download-cloudflared.js",
|
|
11
13
|
"scripts/postinstall.js",
|
|
12
14
|
"scripts/version-check.js",
|
|
13
15
|
"README.md"
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "Pinned cloudflared release that wuphf bundles for the public-tunnel feature. To bump: pick a release tag from https://github.com/cloudflare/cloudflared/releases, copy the SHA256 values from that release's notes, and update both fields in lockstep. Never bump version without bumping sha256 — the install will refuse to run an unverified binary.",
|
|
3
|
+
"version": "2026.3.0",
|
|
4
|
+
"platforms": {
|
|
5
|
+
"darwin-amd64": {
|
|
6
|
+
"asset": "cloudflared-darwin-amd64.tgz",
|
|
7
|
+
"sha256": "b91dbec79a3e3809d5508b96d8b0bdfbf3ad7d51f858200228fa3e57100580d9"
|
|
8
|
+
},
|
|
9
|
+
"darwin-arm64": {
|
|
10
|
+
"asset": "cloudflared-darwin-arm64.tgz",
|
|
11
|
+
"sha256": "633cee0fd41fd2020e17498beecc54811bf4fc99f891c080dc9343eb0f449c60"
|
|
12
|
+
},
|
|
13
|
+
"linux-amd64": {
|
|
14
|
+
"asset": "cloudflared-linux-amd64",
|
|
15
|
+
"sha256": "4a9e50e6d6d798e90fcd01933151a90bf7edd99a0a55c28ad18f2e16263a5c30"
|
|
16
|
+
},
|
|
17
|
+
"linux-arm64": {
|
|
18
|
+
"asset": "cloudflared-linux-arm64",
|
|
19
|
+
"sha256": "0755ba4cbab59980e6148367fcf53a8f3ec85a97deefd63c2420cf7850769bee"
|
|
20
|
+
},
|
|
21
|
+
"windows-amd64": {
|
|
22
|
+
"asset": "cloudflared-windows-amd64.exe",
|
|
23
|
+
"sha256": "59b12880b24af581cf5b1013db601c7d843b9b097e9c78aa5957c7f39f741885"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Downloads the pinned cloudflared release into npm/bin/ so the public-tunnel
|
|
4
|
+
// feature works without a separate `brew install cloudflared` step. Mirrors
|
|
5
|
+
// download-binary.js in shape (version pin + SHA256 verification + atomic
|
|
6
|
+
// place into bin/) but targets a different upstream and uses a JSON manifest
|
|
7
|
+
// instead of a goreleaser checksums.txt because cloudflared's GitHub releases
|
|
8
|
+
// publish hashes inline in the release notes rather than as a sibling file.
|
|
9
|
+
//
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Integrity contract
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Both the version tag AND the per-platform SHA256 live in cloudflared.json
|
|
14
|
+
// in this directory. The download flow is:
|
|
15
|
+
//
|
|
16
|
+
// 1. Map (process.platform, process.arch) -> manifest entry. If the
|
|
17
|
+
// current platform isn't listed, exit 0 silently — wuphf core install
|
|
18
|
+
// should still succeed; tunnels will surface a clear error at runtime.
|
|
19
|
+
// 2. Fetch the asset from
|
|
20
|
+
// https://github.com/cloudflare/cloudflared/releases/download/<version>/<asset>
|
|
21
|
+
// 3. SHA256 the local copy and compare against the manifest hash. Mismatch
|
|
22
|
+
// is FATAL and scrubs the file — same posture as download-binary.js.
|
|
23
|
+
// 4. For .tgz assets, extract the inner `cloudflared` binary; for raw
|
|
24
|
+
// (linux + windows) assets, just rename. Place at npm/bin/cloudflared
|
|
25
|
+
// (or .exe on Windows).
|
|
26
|
+
//
|
|
27
|
+
// Cloudflare's release pipeline publishes the binary and the release-notes
|
|
28
|
+
// hashes from the same atomic process, so a tampered asset would have to
|
|
29
|
+
// come with a tampered manifest commit in OUR repo to survive — that is a
|
|
30
|
+
// weaker guarantee than goreleaser's signed checksums.txt but matches the
|
|
31
|
+
// security model of every npm package that bundles a third-party binary.
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
const fs = require("node:fs");
|
|
35
|
+
const fsp = require("node:fs/promises");
|
|
36
|
+
const path = require("node:path");
|
|
37
|
+
const os = require("node:os");
|
|
38
|
+
const crypto = require("node:crypto");
|
|
39
|
+
const { execFileSync } = require("node:child_process");
|
|
40
|
+
|
|
41
|
+
const MANIFEST_PATH = path.join(__dirname, "cloudflared.json");
|
|
42
|
+
const RELEASE_BASE_URL =
|
|
43
|
+
"https://github.com/cloudflare/cloudflared/releases/download";
|
|
44
|
+
|
|
45
|
+
function loadManifest() {
|
|
46
|
+
const text = fs.readFileSync(MANIFEST_PATH, "utf8");
|
|
47
|
+
return JSON.parse(text);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Translate Node's process.platform / process.arch into the cloudflared
|
|
51
|
+
// manifest key. Returns null when the arch/platform is unrecognised (e.g.
|
|
52
|
+
// Linux 386). For known-but-unpublished combinations like Windows ARM64 it
|
|
53
|
+
// returns the key ("windows-arm64") so downloadCloudflared() can fall through
|
|
54
|
+
// the "no manifest entry" branch — keeping the error surface in the Go
|
|
55
|
+
// controller where cloudflared is actually required.
|
|
56
|
+
function detectManifestKey() {
|
|
57
|
+
const osMap = { darwin: "darwin", linux: "linux", win32: "windows" };
|
|
58
|
+
const archMap = { x64: "amd64", arm64: "arm64" };
|
|
59
|
+
const goOs = osMap[process.platform];
|
|
60
|
+
const goArch = archMap[process.arch];
|
|
61
|
+
if (!goOs || !goArch) return null;
|
|
62
|
+
return `${goOs}-${goArch}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Target filename inside npm/bin/. Lower-case "cloudflared" matches the
|
|
66
|
+
// upstream binary's name; the .exe suffix is mandatory on Windows so
|
|
67
|
+
// CreateProcess will launch it.
|
|
68
|
+
function targetBinaryFilename() {
|
|
69
|
+
return process.platform === "win32" ? "cloudflared.exe" : "cloudflared";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function targetBinaryPath() {
|
|
73
|
+
return path.join(__dirname, "..", "bin", targetBinaryFilename());
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// fetchToFileTimeoutMs caps how long a single download attempt can hang
|
|
77
|
+
// before the postinstall step gives up. Without this, a slow or unresponsive
|
|
78
|
+
// GitHub release CDN would block `npm install` indefinitely with no recovery
|
|
79
|
+
// path. 120s is generous — Cloudflare's release tarball is ~30MB, so even a
|
|
80
|
+
// 2 Mbps connection finishes well inside the budget.
|
|
81
|
+
const fetchToFileTimeoutMs = 120_000;
|
|
82
|
+
|
|
83
|
+
async function fetchToFile(url, dest) {
|
|
84
|
+
// The timeout must stay armed across BOTH the fetch handshake AND the
|
|
85
|
+
// body read. fetch() resolves once response headers arrive, so a peer
|
|
86
|
+
// that sends headers and then stalls the body would still hang the
|
|
87
|
+
// postinstall indefinitely if we cleared the timer right after fetch
|
|
88
|
+
// returned. We clear it only after the file has been fully written.
|
|
89
|
+
const controller = new AbortController();
|
|
90
|
+
const timer = setTimeout(() => controller.abort(), fetchToFileTimeoutMs);
|
|
91
|
+
try {
|
|
92
|
+
const res = await fetch(url, {
|
|
93
|
+
redirect: "follow",
|
|
94
|
+
signal: controller.signal,
|
|
95
|
+
});
|
|
96
|
+
if (!res.ok) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
`Download failed: ${res.status} ${res.statusText} (${url})`,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
102
|
+
await fsp.writeFile(dest, buf);
|
|
103
|
+
} finally {
|
|
104
|
+
clearTimeout(timer);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function sha256OfFile(filePath) {
|
|
109
|
+
const hash = crypto.createHash("sha256");
|
|
110
|
+
const stream = fs.createReadStream(filePath);
|
|
111
|
+
for await (const chunk of stream) {
|
|
112
|
+
hash.update(chunk);
|
|
113
|
+
}
|
|
114
|
+
return hash.digest("hex");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Extract the `cloudflared` binary from a goreleaser-style .tgz into tmpDir.
|
|
118
|
+
// Cloudflared's macOS archives contain a single top-level `cloudflared`
|
|
119
|
+
// file, so a bare `tar -xzf` is sufficient.
|
|
120
|
+
function extractTgz(archivePath, tmpDir, silent) {
|
|
121
|
+
const stdio = silent ? "ignore" : "inherit";
|
|
122
|
+
execFileSync("tar", ["-xzf", archivePath, "-C", tmpDir], { stdio });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function downloadCloudflared({ silent = false } = {}) {
|
|
126
|
+
const manifestKey = detectManifestKey();
|
|
127
|
+
if (!manifestKey) {
|
|
128
|
+
if (!silent) {
|
|
129
|
+
process.stderr.write(
|
|
130
|
+
`wuphf: cloudflared not bundled for ${process.platform}-${process.arch}; ` +
|
|
131
|
+
`the Public Tunnel feature will report it missing at runtime.\n`,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
const manifest = loadManifest();
|
|
137
|
+
const entry = manifest.platforms[manifestKey];
|
|
138
|
+
if (!entry) {
|
|
139
|
+
if (!silent) {
|
|
140
|
+
process.stderr.write(
|
|
141
|
+
`wuphf: no cloudflared asset pinned for ${manifestKey}; ` +
|
|
142
|
+
`the Public Tunnel feature will report it missing at runtime.\n`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const { asset, sha256: expectedHash } = entry;
|
|
149
|
+
const url = `${RELEASE_BASE_URL}/${manifest.version}/${asset}`;
|
|
150
|
+
const target = targetBinaryPath();
|
|
151
|
+
const binDir = path.dirname(target);
|
|
152
|
+
await fsp.mkdir(binDir, { recursive: true });
|
|
153
|
+
|
|
154
|
+
const tmpDir = await fsp.mkdtemp(path.join(os.tmpdir(), "wuphf-cf-"));
|
|
155
|
+
const downloadPath = path.join(tmpDir, asset);
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
if (!silent) {
|
|
159
|
+
process.stderr.write(
|
|
160
|
+
`wuphf: downloading cloudflared ${manifest.version} (${asset})\n`,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
await fetchToFile(url, downloadPath);
|
|
164
|
+
|
|
165
|
+
const actualHash = await sha256OfFile(downloadPath);
|
|
166
|
+
if (actualHash.toLowerCase() !== expectedHash.toLowerCase()) {
|
|
167
|
+
await fsp.rm(downloadPath, { force: true });
|
|
168
|
+
throw new Error(
|
|
169
|
+
`SHA256 mismatch for ${asset}.\n` +
|
|
170
|
+
` expected: ${expectedHash}\n` +
|
|
171
|
+
` actual: ${actualHash}\n` +
|
|
172
|
+
`Refusing to install cloudflared. This may indicate a tampered ` +
|
|
173
|
+
`release asset or a corrupted download.`,
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Atomic placement: stage the binary at "<target>.tmp" then rename over
|
|
178
|
+
// the final path. If the install is interrupted (Ctrl+C, OOM-kill,
|
|
179
|
+
// power loss) midway through copy/chmod/codesign, `findCloudflared`
|
|
180
|
+
// would otherwise pick up a half-written file on next launch and fail
|
|
181
|
+
// with a confusing exec-format error. `fs.rename` is atomic on POSIX
|
|
182
|
+
// and stagingPath is sibling to target, so EXDEV cannot apply here.
|
|
183
|
+
const stagingPath = `${target}.tmp`;
|
|
184
|
+
if (asset.endsWith(".tgz")) {
|
|
185
|
+
extractTgz(downloadPath, tmpDir, silent);
|
|
186
|
+
const extracted = path.join(tmpDir, "cloudflared");
|
|
187
|
+
await fsp.copyFile(extracted, stagingPath);
|
|
188
|
+
} else {
|
|
189
|
+
// linux + windows assets are already raw binaries.
|
|
190
|
+
await fsp.copyFile(downloadPath, stagingPath);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (process.platform !== "win32") {
|
|
194
|
+
await fsp.chmod(stagingPath, 0o755);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// macOS 15+ invalidates the upstream ad-hoc signature after copy+chmod
|
|
198
|
+
// and the kernel SIGKILLs an unsigned exec. Re-sign locally so the
|
|
199
|
+
// first `Start tunnel` click does not fail with code-signing errors.
|
|
200
|
+
if (process.platform === "darwin") {
|
|
201
|
+
try {
|
|
202
|
+
execFileSync("codesign", ["--force", "--sign", "-", stagingPath], {
|
|
203
|
+
stdio: "ignore",
|
|
204
|
+
});
|
|
205
|
+
} catch {
|
|
206
|
+
// codesign is best-effort.
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Atomic move into the final path. After this returns, target either
|
|
211
|
+
// is the fully-prepared binary or is the previous version; never a
|
|
212
|
+
// half-written file.
|
|
213
|
+
await fsp.rename(stagingPath, target);
|
|
214
|
+
|
|
215
|
+
if (!silent) {
|
|
216
|
+
process.stderr.write(
|
|
217
|
+
`wuphf: cloudflared ${manifest.version} installed at ${target}\n`,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
return target;
|
|
221
|
+
} finally {
|
|
222
|
+
await fsp.rm(tmpDir, { recursive: true, force: true });
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
module.exports = {
|
|
227
|
+
downloadCloudflared,
|
|
228
|
+
// Exported for tests:
|
|
229
|
+
detectManifestKey,
|
|
230
|
+
loadManifest,
|
|
231
|
+
targetBinaryFilename,
|
|
232
|
+
sha256OfFile,
|
|
233
|
+
};
|
package/scripts/postinstall.js
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
// fatal and cannot be soft-failed — that path exists to catch tampering.
|
|
21
21
|
|
|
22
22
|
const { downloadBinary } = require("./download-binary");
|
|
23
|
+
const { downloadCloudflared } = require("./download-cloudflared");
|
|
23
24
|
|
|
24
25
|
if (process.env.WUPHF_SKIP_POSTINSTALL === "1") {
|
|
25
26
|
process.stderr.write(
|
|
@@ -28,7 +29,34 @@ if (process.env.WUPHF_SKIP_POSTINSTALL === "1") {
|
|
|
28
29
|
process.exit(0);
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
|
|
32
|
+
// Cloudflared is BEST-EFFORT: a failure here must not block the wuphf
|
|
33
|
+
// install, because tunnels are an optional feature and a corp proxy that
|
|
34
|
+
// blocks github.com release assets shouldn't make `npm install wuphf` fail
|
|
35
|
+
// outright. The runtime path in cmd/wuphf/tunnel.go already returns a clear
|
|
36
|
+
// "cloudflared is not installed" message when the user clicks Start tunnel,
|
|
37
|
+
// so a soft failure here just defers the install hint to that moment.
|
|
38
|
+
//
|
|
39
|
+
// Skip via WUPHF_SKIP_CLOUDFLARED=1 for offline builds and air-gapped CI
|
|
40
|
+
// images that prefer to ship without the bundled tunnel binary.
|
|
41
|
+
async function tryDownloadCloudflared() {
|
|
42
|
+
if (process.env.WUPHF_SKIP_CLOUDFLARED === "1") {
|
|
43
|
+
process.stderr.write(
|
|
44
|
+
"wuphf: cloudflared bundling skipped via WUPHF_SKIP_CLOUDFLARED=1\n",
|
|
45
|
+
);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
await downloadCloudflared();
|
|
50
|
+
} catch (err) {
|
|
51
|
+
const message = err && err.message ? err.message : String(err);
|
|
52
|
+
process.stderr.write(
|
|
53
|
+
`wuphf: cloudflared bundle failed (${message}).\n` +
|
|
54
|
+
`wuphf: continuing — the Public Tunnel feature will surface a missing-binary error at runtime.\n`,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
downloadBinary().then(tryDownloadCloudflared).catch((err) => {
|
|
32
60
|
const message = err && err.message ? err.message : String(err);
|
|
33
61
|
const isIntegrityFailure =
|
|
34
62
|
message.includes("SHA256 mismatch") ||
|