@modelstatus/cli 0.1.73 → 0.1.74

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/updater.js +29 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@modelstatus/cli",
3
- "version": "0.1.73",
3
+ "version": "0.1.74",
4
4
  "description": "Track which AI models you use, where, and never get surprised by a retirement. Free offline model-health for any repo (mm status), browser sign-in for cloud inventory + alerts.",
5
5
  "keywords": [
6
6
  "llm",
package/src/updater.js CHANGED
@@ -88,6 +88,33 @@ async function fetchJson(url) {
88
88
  return res.json();
89
89
  }
90
90
 
91
+ // Ed25519 RELEASE public key (committed: packages/cli/release-pubkey.pem). The
92
+ // manifest is signed with the matching private key; verifying that signature is
93
+ // what makes the manifest's sha256 trustworthy on every platform, independent of
94
+ // the CDN. An auto-update that can't verify this signature does not proceed.
95
+ const RELEASE_PUBKEY = `-----BEGIN PUBLIC KEY-----
96
+ MCowBQYDK2VwAyEApu4VP7vB6iwxsPcK5KaosjK7SIp9HrWMt5IvcUwjUOM=
97
+ -----END PUBLIC KEY-----`;
98
+
99
+ /** Fetch the manifest + its detached Ed25519 signature and verify before
100
+ * parsing. Returns the parsed manifest, or throws if the signature is missing or
101
+ * invalid (fail closed — never self-update against an unverifiable manifest). */
102
+ async function fetchVerifiedManifest(base) {
103
+ const [mRes, sRes] = await Promise.all([
104
+ fetch(`${base}/version.json`, { cache: "no-store" }),
105
+ fetch(`${base}/version.json.sig`, { cache: "no-store" }),
106
+ ]);
107
+ if (!mRes.ok) throw new Error(`GET version.json -> ${mRes.status}`);
108
+ if (!sRes.ok) throw new Error("manifest signature unavailable — refusing to self-update");
109
+ const manifestBytes = Buffer.from(await mRes.arrayBuffer());
110
+ const sig = Buffer.from((await sRes.text()).trim(), "base64");
111
+ const key = crypto.createPublicKey(RELEASE_PUBKEY);
112
+ if (!crypto.verify(null, manifestBytes, key, sig)) {
113
+ throw new Error("manifest signature INVALID — refusing to self-update");
114
+ }
115
+ return JSON.parse(manifestBytes.toString("utf8"));
116
+ }
117
+
91
118
  /** True if we can create/rename files in `dir` (needed for an atomic swap). */
92
119
  function dirWritable(dir) {
93
120
  try {
@@ -189,7 +216,7 @@ export async function forceUpdate() {
189
216
  const key = platformKey();
190
217
  if (!key) return { status: "unsupported" };
191
218
 
192
- const manifest = await fetchJson(`${CDN}/cli/${CHANNEL_PATH}/version.json`);
219
+ const manifest = await fetchVerifiedManifest(`${CDN}/cli/${CHANNEL_PATH}`);
193
220
  writeCache({ ...(readCache() || {}), last_check: Date.now(), latest_known: manifest.version });
194
221
  if (!manifest.version) return { status: "error", message: "manifest has no version" };
195
222
  if (compareVer(manifest.version, BUILD_VERSION) <= 0) return { status: "latest", version: dispVer(BUILD_VERSION) };
@@ -223,7 +250,7 @@ export async function maybeCheckForUpdate(flags = {}) {
223
250
  const key = platformKey();
224
251
  if (!key) return null;
225
252
 
226
- const manifest = await fetchJson(`${CDN}/cli/${CHANNEL_PATH}/version.json`);
253
+ const manifest = await fetchVerifiedManifest(`${CDN}/cli/${CHANNEL_PATH}`);
227
254
  // Always update the cache so we don't re-check for 24 h.
228
255
  writeCache({ ...cache, last_check: Date.now(), latest_known: manifest.version });
229
256