mover-os 4.7.2 → 4.7.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.
Files changed (2) hide show
  1. package/install.js +49 -3
  2. package/package.json +8 -1
package/install.js CHANGED
@@ -749,10 +749,32 @@ const POLAR_ORG_ID = process.env.POLAR_ORG_ID || "ba863394-6bca-4965-952a-06b7c0
749
749
  // ─── Payload Download ────────────────────────────────────────────────────────
750
750
  const DOWNLOAD_URL = "https://moveros.dev/api/download";
751
751
 
752
+ // Generates or reads a stable machine ID. Hardware-derived hash so the same
753
+ // physical machine produces the same ID even if the file is deleted (defeats
754
+ // casual sharing where someone copies just the license key but not the file).
755
+ function getMachineId(moverDir) {
756
+ const idPath = path.join(moverDir, ".machine-id");
757
+ // If file exists and is well-formed, reuse it
758
+ try {
759
+ const existing = fs.readFileSync(idPath, "utf8").trim();
760
+ if (existing && /^[a-f0-9]{32,}$/i.test(existing)) return existing;
761
+ } catch { /* file doesn't exist, fall through */ }
762
+ // Generate hardware-derived ID: hash(cpu_model + hostname + os_release + platform)
763
+ const crypto = require("crypto");
764
+ const cpuModel = (os.cpus()[0] && os.cpus()[0].model) || "unknown-cpu";
765
+ const fingerprint = `${cpuModel}|${os.hostname()}|${os.release()}|${os.platform()}|${os.arch()}`;
766
+ const id = crypto.createHash("sha256").update(fingerprint).digest("hex");
767
+ try {
768
+ fs.writeFileSync(idPath, id, { mode: 0o600 });
769
+ } catch { /* non-fatal — we'll just regenerate next run */ }
770
+ return id;
771
+ }
772
+
752
773
  async function downloadPayload(key) {
753
774
  const https = require("https");
754
775
  const moverDir = path.join(os.homedir(), ".mover");
755
776
  fs.mkdirSync(moverDir, { recursive: true, mode: 0o700 });
777
+ const machineId = getMachineId(moverDir);
756
778
  // Clean old staged src/ before extracting fresh
757
779
  const stagedSrc = path.join(moverDir, "src");
758
780
  if (fs.existsSync(stagedSrc)) {
@@ -767,12 +789,16 @@ async function downloadPayload(key) {
767
789
  hostname: url.hostname,
768
790
  path: url.pathname,
769
791
  method: "GET",
770
- headers: { "X-License-Key": key.trim() },
792
+ headers: { "X-License-Key": key.trim(), "X-Machine-Id": machineId },
771
793
  timeout: 60000,
772
794
  }, (res) => {
773
795
  if (res.statusCode === 301 || res.statusCode === 302) {
774
796
  // Follow redirect — only to trusted domains
775
797
  const redirectUrl = new URL(res.headers.location);
798
+ if (redirectUrl.protocol !== 'https:') {
799
+ reject(new Error('Refusing non-HTTPS redirect'));
800
+ return;
801
+ }
776
802
  const trusted = ["github.com", "objects.githubusercontent.com", "moveros.dev"];
777
803
  if (!trusted.some((d) => redirectUrl.hostname === d || redirectUrl.hostname.endsWith("." + d))) {
778
804
  reject(new Error("Untrusted redirect domain"));
@@ -802,6 +828,12 @@ async function downloadPayload(key) {
802
828
  res.on("end", () => reject(new Error("License key rejected by server")));
803
829
  return;
804
830
  }
831
+ if (res.statusCode === 403) {
832
+ let body = "";
833
+ res.on("data", (c) => body += c);
834
+ res.on("end", () => reject(new Error("License at activation cap. Email support@moveros.dev to free a slot.")));
835
+ return;
836
+ }
805
837
  if (res.statusCode !== 200) {
806
838
  reject(new Error(`Download failed (HTTP ${res.statusCode})`));
807
839
  return;
@@ -818,6 +850,15 @@ async function downloadPayload(key) {
818
850
  req.end();
819
851
  });
820
852
 
853
+ // Validate tar contents before extraction (zip-slip prevention)
854
+ const listing = execSync(`tar -tzf "${tarPath}"`, { encoding: 'utf8' });
855
+ const entries = listing.split('\n').filter(Boolean);
856
+ const badPaths = entries.filter(e => e.startsWith('/') || e.includes('..'));
857
+ if (badPaths.length > 0) {
858
+ fs.unlinkSync(tarPath);
859
+ throw new Error('Payload contains unsafe paths: ' + badPaths.join(', '));
860
+ }
861
+
821
862
  // Extract tarball into ~/.mover/ (produces ~/.mover/src/)
822
863
  try {
823
864
  execSync(`tar -xzf "${tarPath}" -C "${moverDir}"`, { stdio: "ignore" });
@@ -5340,8 +5381,10 @@ async function cmdUpdateComprehensive(opts, bundleDir, startTime) {
5340
5381
  createVaultStructure(vaultPath);
5341
5382
  // NOTE: templates are now handled via smart merge below (after manifest load), NOT installTemplateFiles()
5342
5383
 
5343
- // Update version marker
5344
- fs.writeFileSync(path.join(vaultPath, ".mover-version"), `${require("./package.json").version}\n`, "utf8");
5384
+ // Version marker is NOT stamped here during interactive update.
5385
+ // /update workflow stamps it after migrations complete.
5386
+ // Stamping early would make /update see current-version == target-version
5387
+ // and skip all migrations (the "V4→V4" bug).
5345
5388
 
5346
5389
  // ── Smart Update: hash-based change detection + auto-merge ──
5347
5390
  const manifest = loadUpdateManifest();
@@ -5796,6 +5839,9 @@ async function main() {
5796
5839
  } else if (msg.includes("401") || msg.includes("rejected")) {
5797
5840
  barLn(red(" License key rejected by server."));
5798
5841
  barLn(dim(" Has your key expired? Check polar.sh or email support@moveros.dev."));
5842
+ } else if (msg.includes("activation cap") || msg.includes("403")) {
5843
+ barLn(red(" License is registered to other devices."));
5844
+ barLn(dim(" Email support@moveros.dev to deactivate one and free a slot."));
5799
5845
  } else if (msg.includes("500") || msg.includes("502") || msg.includes("503")) {
5800
5846
  barLn(red(" Server error."));
5801
5847
  barLn(dim(" Try again in a few minutes. If it persists, email support@moveros.dev."));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mover-os",
3
- "version": "4.7.2",
3
+ "version": "4.7.3",
4
4
  "description": "Your AI co-founder. Remembers your goals, pushes back when you drift. Works with 15 AI agents.",
5
5
  "bin": {
6
6
  "moveros": "install.js"
@@ -8,6 +8,13 @@
8
8
  "files": [
9
9
  "install.js"
10
10
  ],
11
+ "scripts": {
12
+ "test": "node test/runner.mjs",
13
+ "test:smoke": "node test/smoke.mjs",
14
+ "test:structural": "node test/structural.mjs",
15
+ "test:machineid": "node test/getMachineId.test.mjs",
16
+ "test:full": "node test/runner.mjs --include-slow"
17
+ },
11
18
  "keywords": [
12
19
  "obsidian",
13
20
  "productivity",