diff --git a/CHANGELOG.md b/CHANGELOG.md index a796725..adf4f0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,43 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.2.1] - 2026-11-01 + +A patch release that repairs the **npm install path**, which has been +broken for every user since v1.1.0. + +`nodeup-npm/scripts/install.js` validates the redirect GitHub serves +for release-asset downloads against a host allowlist. GitHub now +serves release assets from `release-assets.githubusercontent.com`, but +the allowlist only listed `objects.githubusercontent.com` — so the +wrapper refused the redirect and `npm install -g nodeupx` aborted +before it could fetch the binary: + +``` +nodeup: refusing to follow redirect from +https://github.com/dipto0321/nodeup/releases/download/v1.2.0/checksums.txt +to https://release-assets.githubusercontent.com/...: host +release-assets.githubusercontent.com is not in the allowlist +``` + +The allowlist was introduced in #91 (`cb425a9`, 2026-07-04), when +GitHub was still redirecting to `objects.githubusercontent.com`. The CDN +host changed out from under the wrapper, and nothing failed loudly +until the next install — so the breakage was silent for four months. + +Both hosts are GitHub-controlled, so allowing both preserves the +security property #91 was after: an attacker still cannot redirect +the download to a host they control. + +The Homebrew tap, the Scoop bucket, and direct binary downloads were +never affected — they don't go through this redirect-validation path. + +### Fixed +- `nodeup-npm/scripts/install.js`: accept + `release-assets.githubusercontent.com` as a release-asset redirect + target, alongside `objects.githubusercontent.com`. Restores + `npm install -g nodeupx` on every platform. Closes #126. + ## [1.2.0] - 2026-11-01 The headline of this release is **Vite+ support** — `nodeup` now @@ -872,7 +909,8 @@ without weakening the body-length guard for hand-authored commits. ### Added - Project blueprint — internal design doc covering language choice, scope, detection engine, version resolution, package migration, architecture, CLI design, edge cases, git workflow, conventional commits, versioning, CI/CD, and distribution. (Superseded by `README.md`; the standalone doc was removed in the Unreleased section.) -[Unreleased]: https://github.com/dipto0321/nodeup/compare/v1.2.0...HEAD +[Unreleased]: https://github.com/dipto0321/nodeup/compare/v1.2.1...HEAD +[1.2.1]: https://github.com/dipto0321/nodeup/compare/v1.2.0...v1.2.1 [1.2.0]: https://github.com/dipto0321/nodeup/compare/v1.1.0...v1.2.0 [1.1.0]: https://github.com/dipto0321/nodeup/compare/v1.0.1...v1.1.0 [0.0.0]: https://github.com/dipto0321/nodeup/releases/tag/v0.0.0 \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index 7dbbb00..80504ff 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -133,7 +133,7 @@ orchestrator. | 5 — Config subsystem | Done | merged | | 6 — Cross-platform polish | Done | merged (interrupted-upgrade sentinel, `QuotePath`, system-node classifier) | | 7 — Distribution packaging | Done | GoReleaser, brew/scoop/npm wrappers all shipped (#17, #18); manual v1.0.0 npm publish (#35) bootstrapped the OIDC trust | -| 8 — v1.0.0 → v1.2.0 releases | Done | `nodeupx@1.2.0` live on npm, matching the Go binary; trusted-publisher OIDC flow active for every tag (`#109`) | +| 8 — v1.0.0 → v1.2.1 releases | Done | `nodeupx@1.2.1` live on npm (pinned to the v1.2.0 Go binary); trusted-publisher OIDC flow active for every tag (`#109`) | ## On-disk data layout diff --git a/README.md b/README.md index 6b85d99..795b656 100644 --- a/README.md +++ b/README.md @@ -244,9 +244,9 @@ See [`docs/configuration.md`](./docs/configuration.md) for the full schema. ## Project status -Current release: **v1.2.0** (`nodeupx@1.2.0` on npm — the wrapper +Current release: **v1.2.1** (`nodeupx@1.2.1` on npm — the wrapper publishes automatically alongside the tag). See `CHANGELOG.md` for the -v1.1.0 → v1.2.0 history. +v1.1.0 → v1.2.1 history. | Phase | Status | What shipped | |---|---|---| @@ -257,7 +257,7 @@ v1.1.0 → v1.2.0 history. | 5 — Config subsystem | ✅ | YAML config at `~/.nodeup/config.yaml` + `config {show,get,set,init}` + env vars | | 6 — Cross-platform polish | ✅ | `QuotePath`, interrupted-upgrade replay, system-node classifier, lock file | | 7 — Distribution packaging | ✅ | GoReleaser + brew tap + Scoop bucket + npm wrapper (`nodeupx` via OIDC Trusted Publishing) | -| 8 — Tagged releases | ✅ | `v1.0.0` shipped (PR #35); `v1.1.0` shipped with the `internal/ui` rollout (`#117`/`#118`/`#119`); `v1.2.0` adds Vite+ support (`#124`) | +| 8 — Tagged releases | ✅ | `v1.0.0` shipped (PR #35); `v1.1.0` shipped with the `internal/ui` rollout (`#117`/`#118`/`#119`); `v1.2.0` adds Vite+ support (`#124`); `v1.2.1` repairs the npm install path (`#126`) | **Release train:** every `v*.*.*` tag triggers GoReleaser → brew formula push → Scoop manifest push → `nodeupx` publish via OIDC. To ship a diff --git a/nodeup-npm/package.json b/nodeup-npm/package.json index 1aa4584..caa616e 100644 --- a/nodeup-npm/package.json +++ b/nodeup-npm/package.json @@ -1,6 +1,6 @@ { "name": "nodeupx", - "version": "1.2.0", + "version": "1.2.1", "description": "Automated Node.js version upgrade + global package migration CLI. Static Go binary wrapped for npm distribution.", "license": "MIT", "author": "Dipto Karmakar ", diff --git a/nodeup-npm/scripts/install.js b/nodeup-npm/scripts/install.js index 1e39424..565f75b 100644 --- a/nodeup-npm/scripts/install.js +++ b/nodeup-npm/scripts/install.js @@ -29,8 +29,8 @@ // // Integrity / supply-chain hygiene (see #64): // - The download URL follows one redirect via `request()`. Redirect -// targets must be on the `objects.githubusercontent.com` host -// (GitHub Releases' S3-backed CDN); any other host is rejected. +// targets must be on a GitHub-owned release-asset host (see +// ALLOWED_REDIRECT_HOSTS below); any other host is rejected. // This blocks the trivial MITM-via-redirect attack surface. // - Before extraction, we download `checksums.txt` from the same // release tag, parse out the SHA256 for our exact archive name, @@ -123,15 +123,31 @@ function archiveName(os, arch, version) { // keep them split so callers that already have a URL object don't pay // for the parse twice. -// Allowed redirect targets: GitHub Releases redirects to their S3- -// backed CDN. We allowlist that single host — anything else (a -// compromised CDN, a TLS-stripping proxy, a typo in `Location`) is -// rejected. See #64. -const ALLOWED_REDIRECT_HOSTS = new Set(['objects.githubusercontent.com']); +// Allowed redirect targets: GitHub Releases redirects release-asset +// downloads to a GitHub-owned host. We allowlist those hosts — anything +// else (a compromised CDN, a TLS-stripping proxy, a typo in `Location`) +// is rejected. See #64. +// +// The host has changed over time: +// - `objects.githubusercontent.com` — the older S3-backed CDN. +// - `release-assets.githubusercontent.com` — the current host +// (observed on v1.2.0, 2026-11-01). GitHub moved release-asset +// serving here, and the wrapper's install path broke as a result: +// the redirect was refused as "host not in the allowlist", so +// `npm install -g nodeupx` failed outright. +// +// Both are GitHub-controlled, so allowing both keeps the redirect +// allowlist intact (the security property #64 was after — an attacker +// can't redirect the download to a host they control) while surviving +// GitHub's CDN migration. +const ALLOWED_REDIRECT_HOSTS = new Set([ + 'objects.githubusercontent.com', + 'release-assets.githubusercontent.com', +]); // GitHub release URL → redirect target. Two hops is the known maximum -// (`github.com` → `objects.githubusercontent.com`). Anything beyond -// that is either a bug or an attack. +// (`github.com` → the release-asset CDN host). Anything beyond that is +// either a bug or an attack. const MAX_REDIRECT_HOPS = 2; function httpsGet(targetUrl) { diff --git a/nodeup-npm/scripts/install_test.js b/nodeup-npm/scripts/install_test.js index 7cb6c01..3b65ee6 100644 --- a/nodeup-npm/scripts/install_test.js +++ b/nodeup-npm/scripts/install_test.js @@ -54,7 +54,10 @@ const path = require('path'); // If you change a helper in install.js, mirror the change here in // the `// --- mirrored from install.js ---` block below. -const ALLOWED_REDIRECT_HOSTS = new Set(['objects.githubusercontent.com']); +const ALLOWED_REDIRECT_HOSTS = new Set([ + 'objects.githubusercontent.com', + 'release-assets.githubusercontent.com', +]); const MAX_REDIRECT_HOPS = 2; // --- mirrored from install.js: parseChecksumsTxt ------------------------- @@ -197,6 +200,25 @@ test('followHops_allowsGitHubToObjectsCdnRedirect', () => { assert.strictEqual(result.body, 'checksums body'); }); +// followHops: a redirect to `release-assets.githubusercontent.com` is +// accepted. GitHub moved release-asset serving to this host — observed +// on v1.2.0 — and the wrapper's install path broke as a result: the +// redirect was refused as "host not in the allowlist", so +// `npm install -g nodeupx` failed outright. Regression guard. +test('followHops_allowsGitHubToReleaseAssetsCdnRedirect', () => { + const result = followHopsTest( + 'https://github.com/dipto0321/nodeup/releases/download/v1.2.0/checksums.txt', + MAX_REDIRECT_HOPS, + [ + { status: 302, location: 'https://release-assets.githubusercontent.com/abc/checksums.txt' }, + { status: 200, body: 'checksums body' }, + ], + [200] + ); + assert.strictEqual(result.url, 'https://release-assets.githubusercontent.com/abc/checksums.txt'); + assert.strictEqual(result.body, 'checksums body'); +}); + // followHops: a redirect to a non-allowlisted host is rejected. test('followHops_rejectsOffCdnRedirect', () => { assert.throws(