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 CHANGED
@@ -1,13 +1,15 @@
1
1
  {
2
2
  "name": "wuphf",
3
- "version": "0.155.1",
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
+ };
@@ -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
- downloadBinary().catch((err) => {
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") ||