feat: add 2-stage update system (asar swap + full installer)
This commit is contained in:
Generated
+191
-21
@@ -1,18 +1,19 @@
|
|||||||
{
|
{
|
||||||
"name": "krunker-civilian-client",
|
"name": "krunker-civilian-client",
|
||||||
"version": "0.7.1",
|
"version": "0.7.5",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "krunker-civilian-client",
|
"name": "krunker-civilian-client",
|
||||||
"version": "0.7.1",
|
"version": "0.7.5",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"electron-store": "^8.2.0"
|
"electron-store": "^8.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@electron/asar": "^4.2.0",
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
"@types/node": "^22.0.0",
|
"@types/node": "^22.0.0",
|
||||||
"electron": "npm:electron-nightly@42.0.0-nightly.20260227",
|
"electron": "npm:electron-nightly@42.0.0-nightly.20260227",
|
||||||
@@ -45,34 +46,66 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@electron/asar": {
|
"node_modules/@electron/asar": {
|
||||||
"version": "3.4.1",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/@electron/asar/-/asar-4.2.0.tgz",
|
||||||
"integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==",
|
"integrity": "sha512-npW1NW5yy8EB9XY/vEw9sUdgmq0sJEhmSBb6bqyFOAw1CSkrhvAvO6QWlW8CdIMo8VN1lkdF345l/MeW0LrY0Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"commander": "^5.0.0",
|
"commander": "^13.1.0",
|
||||||
"glob": "^7.1.6",
|
"glob": "^13.0.2",
|
||||||
"minimatch": "^3.0.4"
|
"minimatch": "^10.0.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"asar": "bin/asar.js"
|
"asar": "bin/asar.mjs"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10.12.0"
|
"node": ">=22.12.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@electron/asar/node_modules/minimatch": {
|
"node_modules/@electron/asar/node_modules/glob": {
|
||||||
"version": "3.1.5",
|
"version": "13.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz",
|
||||||
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
"integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "BlueOak-1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"brace-expansion": "^1.1.7"
|
"minimatch": "^10.2.2",
|
||||||
|
"minipass": "^7.1.3",
|
||||||
|
"path-scurry": "^2.0.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "*"
|
"node": "18 || 20 || >=22"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@electron/asar/node_modules/lru-cache": {
|
||||||
|
"version": "11.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz",
|
||||||
|
"integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BlueOak-1.0.0",
|
||||||
|
"engines": {
|
||||||
|
"node": "20 || >=22"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@electron/asar/node_modules/path-scurry": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BlueOak-1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"lru-cache": "^11.0.0",
|
||||||
|
"minipass": "^7.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "18 || 20 || >=22"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@electron/fuses": {
|
"node_modules/@electron/fuses": {
|
||||||
@@ -338,6 +371,48 @@
|
|||||||
"node": ">=16.4"
|
"node": ">=16.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@electron/universal/node_modules/@electron/asar": {
|
||||||
|
"version": "3.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz",
|
||||||
|
"integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"commander": "^5.0.0",
|
||||||
|
"glob": "^7.1.6",
|
||||||
|
"minimatch": "^3.0.4"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"asar": "bin/asar.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@electron/universal/node_modules/@electron/asar/node_modules/brace-expansion": {
|
||||||
|
"version": "1.1.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
|
||||||
|
"integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"balanced-match": "^1.0.0",
|
||||||
|
"concat-map": "0.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@electron/universal/node_modules/@electron/asar/node_modules/minimatch": {
|
||||||
|
"version": "3.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||||
|
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"brace-expansion": "^1.1.7"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@electron/universal/node_modules/brace-expansion": {
|
"node_modules/@electron/universal/node_modules/brace-expansion": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz",
|
||||||
@@ -348,6 +423,16 @@
|
|||||||
"balanced-match": "^1.0.0"
|
"balanced-match": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@electron/universal/node_modules/commander": {
|
||||||
|
"version": "5.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz",
|
||||||
|
"integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@electron/universal/node_modules/fs-extra": {
|
"node_modules/@electron/universal/node_modules/fs-extra": {
|
||||||
"version": "11.3.3",
|
"version": "11.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz",
|
||||||
@@ -2359,6 +2444,37 @@
|
|||||||
"electron-builder-squirrel-windows": "26.8.1"
|
"electron-builder-squirrel-windows": "26.8.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/app-builder-lib/node_modules/@electron/asar": {
|
||||||
|
"version": "3.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz",
|
||||||
|
"integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"commander": "^5.0.0",
|
||||||
|
"glob": "^7.1.6",
|
||||||
|
"minimatch": "^3.0.4"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"asar": "bin/asar.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/app-builder-lib/node_modules/@electron/asar/node_modules/minimatch": {
|
||||||
|
"version": "3.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||||
|
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"brace-expansion": "^1.1.7"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/app-builder-lib/node_modules/@electron/get": {
|
"node_modules/app-builder-lib/node_modules/@electron/get": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@electron/get/-/get-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@electron/get/-/get-3.1.0.tgz",
|
||||||
@@ -2422,6 +2538,16 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/app-builder-lib/node_modules/commander": {
|
||||||
|
"version": "5.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz",
|
||||||
|
"integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/app-builder-lib/node_modules/fs-extra": {
|
"node_modules/app-builder-lib/node_modules/fs-extra": {
|
||||||
"version": "10.1.0",
|
"version": "10.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
|
||||||
@@ -3032,13 +3158,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/commander": {
|
"node_modules/commander": {
|
||||||
"version": "5.1.0",
|
"version": "13.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz",
|
||||||
"integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==",
|
"integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 6"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/compare-version": {
|
"node_modules/compare-version": {
|
||||||
@@ -3715,6 +3841,36 @@
|
|||||||
"@electron/windows-sign": "^1.1.2"
|
"@electron/windows-sign": "^1.1.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/electron-winstaller/node_modules/@electron/asar": {
|
||||||
|
"version": "3.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz",
|
||||||
|
"integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"commander": "^5.0.0",
|
||||||
|
"glob": "^7.1.6",
|
||||||
|
"minimatch": "^3.0.4"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"asar": "bin/asar.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/electron-winstaller/node_modules/commander": {
|
||||||
|
"version": "5.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz",
|
||||||
|
"integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/electron-winstaller/node_modules/fs-extra": {
|
"node_modules/electron-winstaller/node_modules/fs-extra": {
|
||||||
"version": "7.0.1",
|
"version": "7.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz",
|
||||||
@@ -3731,6 +3887,20 @@
|
|||||||
"node": ">=6 <7 || >=8"
|
"node": ">=6 <7 || >=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/electron-winstaller/node_modules/minimatch": {
|
||||||
|
"version": "3.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||||
|
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"brace-expansion": "^1.1.7"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/electron/node_modules/@types/node": {
|
"node_modules/electron/node_modules/@types/node": {
|
||||||
"version": "24.11.0",
|
"version": "24.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.11.0.tgz",
|
||||||
|
|||||||
+3
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "krunker-civilian-client",
|
"name": "krunker-civilian-client",
|
||||||
"version": "0.7.5",
|
"version": "0.7.6",
|
||||||
"description": "Cross-platform Krunker game client",
|
"description": "Cross-platform Krunker game client",
|
||||||
"main": "dist/main/index.js",
|
"main": "dist/main/index.js",
|
||||||
"homepage": "https://github.com/bigjakk/Krunker-Civilian-Client",
|
"homepage": "https://github.com/bigjakk/Krunker-Civilian-Client",
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
"dist:win": "npm run build && electron-builder --win",
|
"dist:win": "npm run build && electron-builder --win",
|
||||||
"dist:linux": "npm run build && electron-builder --linux",
|
"dist:linux": "npm run build && electron-builder --linux",
|
||||||
"dist:all": "npm run build && electron-builder --win --linux",
|
"dist:all": "npm run build && electron-builder --win --linux",
|
||||||
|
"build:asar": "node scripts/build-asar.js",
|
||||||
"clean": "rimraf dist out",
|
"clean": "rimraf dist out",
|
||||||
"lint": "eslint src/",
|
"lint": "eslint src/",
|
||||||
"prepare": "husky"
|
"prepare": "husky"
|
||||||
@@ -28,6 +29,7 @@
|
|||||||
"electron-store": "^8.2.0"
|
"electron-store": "^8.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@electron/asar": "^4.2.0",
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
"@types/node": "^22.0.0",
|
"@types/node": "^22.0.0",
|
||||||
"electron": "npm:electron-nightly@42.0.0-nightly.20260227",
|
"electron": "npm:electron-nightly@42.0.0-nightly.20260227",
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
/**
|
||||||
|
* Build a standalone app.asar + checksums.sha256 for minor (patch) updates.
|
||||||
|
*
|
||||||
|
* Replicates what electron-builder packs into the asar (dist/ + package.json +
|
||||||
|
* required node_modules) without running the full installer build.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* npm run build:asar # default (github) update source
|
||||||
|
* UPDATE_SOURCE=gitea npm run build:asar # gitea update source
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { execSync } = require('child_process');
|
||||||
|
const { cpSync, mkdirSync, rmSync, readFileSync, writeFileSync, createReadStream } = require('fs');
|
||||||
|
const { join } = require('path');
|
||||||
|
const { createHash } = require('crypto');
|
||||||
|
const asar = require('@electron/asar');
|
||||||
|
|
||||||
|
const ROOT = join(__dirname, '..');
|
||||||
|
const STAGING = join(ROOT, 'out', 'asar-staging');
|
||||||
|
const OUTPUT = join(ROOT, 'out', 'asar');
|
||||||
|
|
||||||
|
// Same node_modules list as electron-builder.yml
|
||||||
|
const NODE_MODULES = [
|
||||||
|
'electron-store', 'conf', 'dot-prop', 'type-fest', 'pkg-up',
|
||||||
|
'find-up', 'locate-path', 'p-locate', 'p-limit', 'yocto-queue',
|
||||||
|
'path-exists', 'env-paths', 'json-schema-typed', 'ajv', 'ajv-formats',
|
||||||
|
'atomically', 'debounce-fn', 'mimic-fn', 'semver', 'onetime',
|
||||||
|
];
|
||||||
|
|
||||||
|
function shouldExclude(src) {
|
||||||
|
return src.endsWith('.ts') || src.endsWith('.map');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
// 1. Run Vite build (pass UPDATE_SOURCE through env)
|
||||||
|
const env = { ...process.env };
|
||||||
|
if (env.UPDATE_SOURCE) {
|
||||||
|
console.log(`Building with UPDATE_SOURCE=${env.UPDATE_SOURCE}`);
|
||||||
|
}
|
||||||
|
execSync('npm run build', { cwd: ROOT, stdio: 'inherit', env });
|
||||||
|
|
||||||
|
// 2. Prepare staging directory
|
||||||
|
rmSync(STAGING, { recursive: true, force: true });
|
||||||
|
mkdirSync(STAGING, { recursive: true });
|
||||||
|
|
||||||
|
// Copy dist/
|
||||||
|
cpSync(join(ROOT, 'dist'), join(STAGING, 'dist'), { recursive: true });
|
||||||
|
|
||||||
|
// Copy package.json (Electron reads "main" from it)
|
||||||
|
cpSync(join(ROOT, 'package.json'), join(STAGING, 'package.json'));
|
||||||
|
|
||||||
|
// Copy required node_modules (excluding .ts and .map files)
|
||||||
|
for (const mod of NODE_MODULES) {
|
||||||
|
const src = join(ROOT, 'node_modules', mod);
|
||||||
|
const dest = join(STAGING, 'node_modules', mod);
|
||||||
|
cpSync(src, dest, {
|
||||||
|
recursive: true,
|
||||||
|
filter: (s) => !shouldExclude(s),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Pack into asar
|
||||||
|
mkdirSync(OUTPUT, { recursive: true });
|
||||||
|
const asarPath = join(OUTPUT, 'app.asar');
|
||||||
|
await asar.createPackage(STAGING, asarPath);
|
||||||
|
console.log('Created:', asarPath);
|
||||||
|
|
||||||
|
// 4. Generate checksums.sha256
|
||||||
|
const hex = await fileHash(asarPath);
|
||||||
|
const checksumsPath = join(OUTPUT, 'checksums.sha256');
|
||||||
|
writeFileSync(checksumsPath, `${hex} app.asar\n`);
|
||||||
|
console.log(`SHA-256: ${hex}`);
|
||||||
|
console.log('Created:', checksumsPath);
|
||||||
|
|
||||||
|
// 5. Cleanup staging
|
||||||
|
rmSync(STAGING, { recursive: true, force: true });
|
||||||
|
|
||||||
|
// Print size
|
||||||
|
const size = readFileSync(asarPath).length;
|
||||||
|
console.log(`\napp.asar size: ${(size / 1024).toFixed(1)} KB`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fileHash(filePath) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const hash = createHash('sha256');
|
||||||
|
const stream = createReadStream(filePath);
|
||||||
|
stream.on('data', (chunk) => hash.update(chunk));
|
||||||
|
stream.on('end', () => resolve(hash.digest('hex')));
|
||||||
|
stream.on('error', reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
console.error('build-asar failed:', err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
+72
-30
@@ -1,6 +1,6 @@
|
|||||||
import { app, BrowserWindow, Menu, clipboard, ipcMain, safeStorage, session, shell } from 'electron';
|
import { app, BrowserWindow, Menu, clipboard, ipcMain, safeStorage, session, shell } from 'electron';
|
||||||
import { join } from 'path';
|
import { join, dirname } from 'path';
|
||||||
import { existsSync, mkdirSync, promises as fsp } from 'fs';
|
import { existsSync, mkdirSync, unlinkSync, promises as fsp } from 'fs';
|
||||||
import { get as httpsGet } from 'https';
|
import { get as httpsGet } from 'https';
|
||||||
import { execFile } from 'child_process';
|
import { execFile } from 'child_process';
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
@@ -10,7 +10,7 @@ import { initSwapperProtocol, registerSwapperFileProtocol, ResourceSwapper } fro
|
|||||||
import { UserscriptManager } from './userscripts';
|
import { UserscriptManager } from './userscripts';
|
||||||
import { ALL_CLIENT_CSS } from './client-ui';
|
import { ALL_CLIENT_CSS } from './client-ui';
|
||||||
import { electronLog, getLogPath, closeLogStreams } from './logger';
|
import { electronLog, getLogPath, closeLogStreams } from './logger';
|
||||||
import { checkForUpdate, downloadUpdate, installUpdate } from './updater';
|
import { checkForUpdate, downloadUpdate, installUpdate, applyMinorUpdate } from './updater';
|
||||||
import { showUpdateWindow } from './update-window';
|
import { showUpdateWindow } from './update-window';
|
||||||
import { DiscordRPC } from './discord-rpc';
|
import { DiscordRPC } from './discord-rpc';
|
||||||
import { listThemes, getThemeCSS, listLoadingThemes, getLoadingScreenCSS } from './css-themes';
|
import { listThemes, getThemeCSS, listLoadingThemes, getLoadingScreenCSS } from './css-themes';
|
||||||
@@ -164,51 +164,93 @@ function saveWindowState(win: BrowserWindow): void {
|
|||||||
|
|
||||||
app.whenReady().then(async () => {
|
app.whenReady().then(async () => {
|
||||||
electronLog.log('[KCC] App ready');
|
electronLog.log('[KCC] App ready');
|
||||||
|
electronLog.log('[KCC] Minor update test — asar swap successful');
|
||||||
|
|
||||||
// ── Auto-update check (mandatory, Windows NSIS install only) ──
|
// ── Auto-update check (2-stage: minor asar swap or major installer) ──
|
||||||
const isPortable = !!process.env.PORTABLE_EXECUTABLE_DIR;
|
const isPortable = !!process.env.PORTABLE_EXECUTABLE_DIR;
|
||||||
const isAppImage = !!process.env.APPIMAGE;
|
const isAppImage = !!process.env.APPIMAGE;
|
||||||
const isDev = !app.isPackaged;
|
const isDev = !app.isPackaged;
|
||||||
if (isDev || process.platform !== 'win32' || isPortable || isAppImage) {
|
const canMajorUpdate = process.platform === 'win32' && !isPortable;
|
||||||
electronLog.log('[KCC] Skipping auto-update (portable or non-Windows)');
|
const canMinorUpdate = !isPortable && !isAppImage;
|
||||||
|
|
||||||
|
if (isDev || (!canMajorUpdate && !canMinorUpdate)) {
|
||||||
|
electronLog.log('[KCC] Skipping auto-update');
|
||||||
} else {
|
} else {
|
||||||
|
// Clean up stale pending asar from a previous failed swap
|
||||||
|
const resourcesDir = join(dirname(app.getPath('exe')), 'resources');
|
||||||
|
const stalePending = join(resourcesDir, 'app-pending.asar');
|
||||||
|
if (existsSync(stalePending)) {
|
||||||
|
try { unlinkSync(stalePending); } catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
electronLog.log('[KCC] Checking for updates...');
|
electronLog.log('[KCC] Checking for updates...');
|
||||||
const update = await checkForUpdate(appVersion);
|
const update = await checkForUpdate(appVersion);
|
||||||
if (update) {
|
if (update) {
|
||||||
electronLog.log(`[KCC] Update available: v${update.version}`);
|
electronLog.log(`[KCC] Update available: v${update.version} (${update.updateType})`);
|
||||||
const { window: updateWin, sendProgress } = showUpdateWindow();
|
const { window: updateWin, sendProgress } = showUpdateWindow();
|
||||||
sendProgress(`Update available (v${update.version})`, 0);
|
|
||||||
|
|
||||||
const tempDir = join(app.getPath('temp'), 'kcc-update');
|
let cancelled = false;
|
||||||
if (!existsSync(tempDir)) mkdirSync(tempDir, { recursive: true });
|
updateWin.on('closed', () => { cancelled = true; });
|
||||||
const installerPath = join(tempDir, `KCC-${update.version}-Setup.exe`);
|
|
||||||
|
|
||||||
let cancelled = false;
|
if (update.updateType === 'minor' && canMinorUpdate) {
|
||||||
updateWin.on('closed', () => { cancelled = true; });
|
// Minor update: download app.asar, swap via external script, restart
|
||||||
|
sendProgress(`Patch available (v${update.version})`, 0);
|
||||||
|
const pendingPath = join(resourcesDir, 'app-pending.asar');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await downloadUpdate(update.downloadUrl, installerPath, (pct) => {
|
await downloadUpdate(update.downloadUrl, pendingPath, (pct) => {
|
||||||
if (!cancelled && !updateWin.isDestroyed()) {
|
if (!cancelled && !updateWin.isDestroyed()) {
|
||||||
sendProgress(`Downloading update... ${pct}%`, pct);
|
sendProgress(`Downloading patch... ${pct}%`, pct);
|
||||||
|
}
|
||||||
|
}, update.sha256 || undefined);
|
||||||
|
|
||||||
|
if (!cancelled) {
|
||||||
|
sendProgress('Applying patch...', 100);
|
||||||
|
applyMinorUpdate(pendingPath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
electronLog.error('[KCC] Patch download failed:', err);
|
||||||
|
// Clean up failed download
|
||||||
|
if (existsSync(pendingPath)) {
|
||||||
|
try { unlinkSync(pendingPath); } catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
if (!updateWin.isDestroyed()) updateWin.close();
|
||||||
}
|
}
|
||||||
}, update.sha256);
|
} else if (update.updateType === 'major' && canMajorUpdate) {
|
||||||
|
// Major update: download Setup.exe, run installer
|
||||||
|
sendProgress(`Update available (v${update.version})`, 0);
|
||||||
|
const tempDir = join(app.getPath('temp'), 'kcc-update');
|
||||||
|
if (!existsSync(tempDir)) mkdirSync(tempDir, { recursive: true });
|
||||||
|
const installerPath = join(tempDir, `KCC-${update.version}-Setup.exe`);
|
||||||
|
|
||||||
if (!cancelled) {
|
try {
|
||||||
sendProgress('Installing update...', 100);
|
await downloadUpdate(update.downloadUrl, installerPath, (pct) => {
|
||||||
installUpdate(installerPath);
|
if (!cancelled && !updateWin.isDestroyed()) {
|
||||||
return; // app.quit() called by installUpdate
|
sendProgress(`Downloading update... ${pct}%`, pct);
|
||||||
|
}
|
||||||
|
}, update.sha256 || undefined);
|
||||||
|
|
||||||
|
if (!cancelled) {
|
||||||
|
sendProgress('Installing update...', 100);
|
||||||
|
installUpdate(installerPath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
electronLog.error('[KCC] Update download failed:', err);
|
||||||
|
if (!updateWin.isDestroyed()) updateWin.close();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
electronLog.log('[KCC] Update available but cannot auto-install on this platform');
|
||||||
|
if (!updateWin.isDestroyed()) updateWin.close();
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} else {
|
||||||
electronLog.error('[KCC] Update download failed:', err);
|
electronLog.log('[KCC] No updates available');
|
||||||
if (!updateWin.isDestroyed()) updateWin.close();
|
|
||||||
}
|
}
|
||||||
} else {
|
} catch (err) {
|
||||||
electronLog.log('[KCC] No updates available');
|
electronLog.error('[KCC] Update check failed:', err);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
|
||||||
electronLog.error('[KCC] Update check failed:', err);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await launchApp();
|
await launchApp();
|
||||||
|
|||||||
+487
-279
@@ -1,279 +1,487 @@
|
|||||||
import { get as httpsGet } from 'https';
|
import { get as httpsGet } from 'https';
|
||||||
import { createReadStream, createWriteStream, renameSync, unlinkSync, existsSync } from 'fs';
|
import { createReadStream, createWriteStream, writeFileSync, renameSync, unlinkSync, existsSync, mkdirSync } from 'fs';
|
||||||
import { createHash } from 'crypto';
|
import { join, dirname } from 'path';
|
||||||
import { spawn } from 'child_process';
|
import { createHash } from 'crypto';
|
||||||
import { app } from 'electron';
|
import { spawn } from 'child_process';
|
||||||
import { electronLog } from './logger';
|
import { app } from 'electron';
|
||||||
|
import { electronLog } from './logger';
|
||||||
export interface UpdateInfo {
|
|
||||||
version: string;
|
// ── Types ──
|
||||||
downloadUrl: string;
|
|
||||||
fileSize: number;
|
export type UpdateType = 'minor' | 'major';
|
||||||
sha256: string;
|
|
||||||
}
|
export interface UpdateInfo {
|
||||||
|
version: string;
|
||||||
export type ProgressCallback = (percent: number) => void;
|
updateType: UpdateType;
|
||||||
|
downloadUrl: string;
|
||||||
const UPDATE_CONFIG = {
|
fileSize: number;
|
||||||
checkUrl: 'https://api.github.com/repos/bigjakk/Krunker-Civilian-Client/releases/latest',
|
sha256: string;
|
||||||
assetPattern: /Setup\.exe$/i,
|
}
|
||||||
allowedHosts: ['github.com', 'githubusercontent.com'],
|
|
||||||
};
|
export type ProgressCallback = (percent: number) => void;
|
||||||
|
|
||||||
const CHECK_TIMEOUT_MS = 10000;
|
// ── Build-time update source (injected by Vite define) ──
|
||||||
const DOWNLOAD_TIMEOUT_MS = 300000; // 5 minutes
|
|
||||||
|
declare const __UPDATE_SOURCE__: 'github' | 'gitea';
|
||||||
/**
|
|
||||||
* Validate that a redirect URL stays on an allowed host.
|
interface UpdateSourceConfig {
|
||||||
*/
|
checkUrl: string;
|
||||||
function isAllowedRedirect(url: string): boolean {
|
allowedHosts: string[];
|
||||||
try {
|
checksumSource: 'digest' | 'file';
|
||||||
const parsed = new URL(url);
|
}
|
||||||
return UPDATE_CONFIG.allowedHosts.some(h => parsed.hostname === h || parsed.hostname.endsWith('.' + h));
|
|
||||||
} catch {
|
const UPDATE_SOURCES: Record<string, UpdateSourceConfig> = {
|
||||||
return false;
|
github: {
|
||||||
}
|
checkUrl: 'https://api.github.com/repos/bigjakk/Krunker-Civilian-Client/releases/latest',
|
||||||
}
|
allowedHosts: ['github.com', 'githubusercontent.com'],
|
||||||
|
checksumSource: 'digest',
|
||||||
/**
|
},
|
||||||
* Simple semver comparison: returns true if a < b.
|
gitea: {
|
||||||
* Handles versions like "0.1.0", "1.2.3".
|
checkUrl: 'https://gitea.crjlab.net/api/v1/repos/bigjakk/Krunker-Civilian-Client-Test/releases/latest',
|
||||||
*/
|
allowedHosts: ['gitea.crjlab.net'],
|
||||||
function versionLessThan(a: string, b: string): boolean {
|
checksumSource: 'file',
|
||||||
const pa = a.split('.').map(Number);
|
},
|
||||||
const pb = b.split('.').map(Number);
|
};
|
||||||
const len = Math.max(pa.length, pb.length);
|
|
||||||
for (let i = 0; i < len; i++) {
|
const sourceKey = typeof __UPDATE_SOURCE__ !== 'undefined' ? __UPDATE_SOURCE__ : 'github';
|
||||||
const na = pa[i] || 0;
|
const UPDATE_CONFIG = UPDATE_SOURCES[sourceKey] || UPDATE_SOURCES.github;
|
||||||
const nb = pb[i] || 0;
|
|
||||||
if (na < nb) return true;
|
const ASSET_PATTERNS = {
|
||||||
if (na > nb) return false;
|
asar: /^app\.asar$/i,
|
||||||
}
|
setup: /Setup\.exe$/i,
|
||||||
return false;
|
checksums: /^checksums\.sha256$/i,
|
||||||
}
|
};
|
||||||
|
|
||||||
export function checkForUpdate(currentVersion: string): Promise<UpdateInfo | null> {
|
const CHECK_TIMEOUT_MS = 10000;
|
||||||
return new Promise((resolve) => {
|
const DOWNLOAD_TIMEOUT_MS = 300000; // 5 minutes
|
||||||
electronLog.log('[KCC-Update] Checking for updates at:', UPDATE_CONFIG.checkUrl);
|
|
||||||
electronLog.log('[KCC-Update] Current version:', currentVersion);
|
// ── Swap scripts (embedded, written to temp at runtime) ──
|
||||||
|
|
||||||
const req = httpsGet(UPDATE_CONFIG.checkUrl, {
|
const SWAP_SCRIPT_PS1 = `param(
|
||||||
headers: { 'User-Agent': 'KrunkerCivilianClient/' + currentVersion },
|
[int]$ProcessId,
|
||||||
}, (res) => {
|
[string]$ResourcesDir,
|
||||||
electronLog.log('[KCC-Update] Check response status:', res.statusCode);
|
[string]$ExePath
|
||||||
// Follow redirects (with domain validation)
|
)
|
||||||
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
||||||
const redirectUrl = res.headers.location;
|
try {
|
||||||
electronLog.log('[KCC-Update] Redirected to:', redirectUrl);
|
$proc = Get-Process -Id $ProcessId -ErrorAction SilentlyContinue
|
||||||
if (!isAllowedRedirect(redirectUrl)) {
|
if ($proc) { $proc.WaitForExit(30000) | Out-Null }
|
||||||
electronLog.error('[KCC-Update] Redirect to untrusted host blocked:', redirectUrl);
|
} catch {}
|
||||||
resolve(null);
|
|
||||||
return;
|
Start-Sleep -Milliseconds 500
|
||||||
}
|
|
||||||
httpsGet(redirectUrl, {
|
$asar = Join-Path $ResourcesDir "app.asar"
|
||||||
headers: { 'User-Agent': 'KrunkerCivilianClient/' + currentVersion },
|
$pending = Join-Path $ResourcesDir "app-pending.asar"
|
||||||
}, (redirectRes) => {
|
$backup = Join-Path $ResourcesDir "app-backup.asar"
|
||||||
electronLog.log('[KCC-Update] Redirect response status:', redirectRes.statusCode);
|
|
||||||
handleResponse(redirectRes);
|
if (-not (Test-Path $pending)) { exit 1 }
|
||||||
}).on('error', (err) => {
|
|
||||||
electronLog.error('[KCC-Update] Redirect error:', err);
|
try {
|
||||||
resolve(null);
|
if (Test-Path $backup) { Remove-Item $backup -Force }
|
||||||
});
|
Rename-Item $asar $backup -Force
|
||||||
return;
|
Rename-Item $pending $asar -Force
|
||||||
}
|
if (Test-Path $backup) { Remove-Item $backup -Force }
|
||||||
handleResponse(res);
|
} catch {
|
||||||
});
|
if ((Test-Path $backup) -and -not (Test-Path $asar)) {
|
||||||
|
Rename-Item $backup $asar -Force
|
||||||
function handleResponse(res: import('http').IncomingMessage): void {
|
}
|
||||||
if (res.statusCode !== 200) {
|
exit 1
|
||||||
electronLog.error('[KCC-Update] Check returned status', res.statusCode);
|
}
|
||||||
resolve(null);
|
|
||||||
return;
|
Start-Process $ExePath
|
||||||
}
|
`;
|
||||||
|
|
||||||
let data = '';
|
const SWAP_SCRIPT_BASH = `#!/bin/bash
|
||||||
res.on('data', (chunk: string) => { data += chunk; });
|
PID="$1"
|
||||||
res.on('end', () => {
|
RESOURCES_DIR="$2"
|
||||||
try {
|
EXE_PATH="$3"
|
||||||
const release = JSON.parse(data);
|
|
||||||
const tagName: string = release.tag_name || '';
|
while kill -0 "$PID" 2>/dev/null; do sleep 0.2; done
|
||||||
const remoteVersion = tagName.replace(/^v/i, '');
|
sleep 0.5
|
||||||
electronLog.log('[KCC-Update] Latest release:', remoteVersion, '| Current:', currentVersion);
|
|
||||||
|
ASAR="$RESOURCES_DIR/app.asar"
|
||||||
if (!remoteVersion || !versionLessThan(currentVersion, remoteVersion)) {
|
PENDING="$RESOURCES_DIR/app-pending.asar"
|
||||||
electronLog.log('[KCC-Update] Already up to date');
|
BACKUP="$RESOURCES_DIR/app-backup.asar"
|
||||||
resolve(null);
|
|
||||||
return;
|
[ -f "$PENDING" ] || exit 1
|
||||||
}
|
|
||||||
|
rm -f "$BACKUP"
|
||||||
const assets: Array<{ name: string; browser_download_url: string; size: number; digest: string }> = release.assets || [];
|
mv "$ASAR" "$BACKUP" && mv "$PENDING" "$ASAR" && rm -f "$BACKUP" || {
|
||||||
const setupAsset = assets.find((a) => UPDATE_CONFIG.assetPattern.test(a.name));
|
[ -f "$BACKUP" ] && [ ! -f "$ASAR" ] && mv "$BACKUP" "$ASAR"
|
||||||
if (!setupAsset) {
|
exit 1
|
||||||
electronLog.error('[KCC-Update] No Setup.exe asset found in release', remoteVersion);
|
}
|
||||||
resolve(null);
|
|
||||||
return;
|
"$EXE_PATH" &
|
||||||
}
|
`;
|
||||||
|
|
||||||
// Validate the download URL points to an allowed host
|
// ── Helpers ──
|
||||||
if (!isAllowedRedirect(setupAsset.browser_download_url)) {
|
|
||||||
electronLog.error('[KCC-Update] Download URL points to untrusted host:', setupAsset.browser_download_url);
|
function isAllowedRedirect(url: string): boolean {
|
||||||
resolve(null);
|
try {
|
||||||
return;
|
const parsed = new URL(url);
|
||||||
}
|
return UPDATE_CONFIG.allowedHosts.some(h => parsed.hostname === h || parsed.hostname.endsWith('.' + h));
|
||||||
|
} catch {
|
||||||
// Extract SHA-256 digest from GitHub API (format: "sha256:<hex>")
|
return false;
|
||||||
const sha256 = (setupAsset.digest || '').replace(/^sha256:/i, '');
|
}
|
||||||
if (!sha256) {
|
}
|
||||||
electronLog.error('[KCC-Update] No SHA-256 digest found for asset');
|
|
||||||
resolve(null);
|
function versionLessThan(a: string, b: string): boolean {
|
||||||
return;
|
const pa = a.split('.').map(Number);
|
||||||
}
|
const pb = b.split('.').map(Number);
|
||||||
|
const len = Math.max(pa.length, pb.length);
|
||||||
electronLog.log('[KCC-Update] Update available:', remoteVersion, '| SHA-256:', sha256.substring(0, 16) + '...');
|
for (let i = 0; i < len; i++) {
|
||||||
resolve({
|
const na = pa[i] || 0;
|
||||||
version: remoteVersion,
|
const nb = pb[i] || 0;
|
||||||
downloadUrl: setupAsset.browser_download_url,
|
if (na < nb) return true;
|
||||||
fileSize: setupAsset.size,
|
if (na > nb) return false;
|
||||||
sha256,
|
}
|
||||||
});
|
return false;
|
||||||
} catch (err) {
|
}
|
||||||
electronLog.error('[KCC-Update] Failed to parse release data:', err);
|
|
||||||
resolve(null);
|
function simpleGet(url: string): Promise<string> {
|
||||||
}
|
return new Promise((resolve, reject) => {
|
||||||
});
|
function doGet(getUrl: string, redirectCount = 0): void {
|
||||||
res.on('error', (err) => {
|
if (redirectCount > 5) {
|
||||||
electronLog.error('[KCC-Update] Response error:', err);
|
reject(new Error('Too many redirects'));
|
||||||
resolve(null);
|
return;
|
||||||
});
|
}
|
||||||
}
|
const req = httpsGet(getUrl, {
|
||||||
|
headers: { 'User-Agent': 'KrunkerCivilianClient' },
|
||||||
req.setTimeout(CHECK_TIMEOUT_MS, () => {
|
}, (res) => {
|
||||||
electronLog.error('[KCC-Update] Check timed out after', CHECK_TIMEOUT_MS, 'ms');
|
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
||||||
req.destroy();
|
if (!isAllowedRedirect(res.headers.location)) {
|
||||||
resolve(null);
|
reject(new Error('Redirect to untrusted host: ' + res.headers.location));
|
||||||
});
|
return;
|
||||||
|
}
|
||||||
req.on('error', (err) => {
|
doGet(res.headers.location, redirectCount + 1);
|
||||||
electronLog.error('[KCC-Update] Check error:', err);
|
return;
|
||||||
resolve(null);
|
}
|
||||||
});
|
if (res.statusCode !== 200) {
|
||||||
});
|
reject(new Error('HTTP ' + res.statusCode));
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
function verifyChecksum(filePath: string, expectedSha256: string): Promise<boolean> {
|
let data = '';
|
||||||
return new Promise((resolve, reject) => {
|
res.on('data', (chunk: string) => { data += chunk; });
|
||||||
const hash = createHash('sha256');
|
res.on('end', () => resolve(data));
|
||||||
const stream = createReadStream(filePath);
|
res.on('error', reject);
|
||||||
stream.on('data', (chunk) => hash.update(chunk));
|
});
|
||||||
stream.on('end', () => {
|
req.setTimeout(CHECK_TIMEOUT_MS, () => {
|
||||||
const actual = hash.digest('hex');
|
req.destroy();
|
||||||
electronLog.log('[KCC-Update] SHA-256 expected:', expectedSha256);
|
reject(new Error('Request timed out'));
|
||||||
electronLog.log('[KCC-Update] SHA-256 actual: ', actual);
|
});
|
||||||
resolve(actual === expectedSha256);
|
req.on('error', reject);
|
||||||
});
|
}
|
||||||
stream.on('error', reject);
|
doGet(url);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function downloadUpdate(url: string, destPath: string, onProgress: ProgressCallback, expectedSha256?: string): Promise<void> {
|
/**
|
||||||
return new Promise((resolve, reject) => {
|
* Fetch and parse a checksums.sha256 file from a release asset URL.
|
||||||
const tmpPath = destPath + '.tmp';
|
* Format: "<sha256> <filename>" per line.
|
||||||
|
*/
|
||||||
function doDownload(downloadUrl: string, redirectCount = 0): void {
|
async function fetchChecksums(url: string): Promise<Map<string, string>> {
|
||||||
if (redirectCount > 5) {
|
const text = await simpleGet(url);
|
||||||
reject(new Error('Too many redirects'));
|
const map = new Map<string, string>();
|
||||||
return;
|
for (const line of text.split('\n')) {
|
||||||
}
|
const match = line.trim().match(/^([a-f0-9]{64})\s+(.+)$/i);
|
||||||
electronLog.log('[KCC-Update] Downloading from:', downloadUrl);
|
if (match) map.set(match[2].trim(), match[1].toLowerCase());
|
||||||
const req = httpsGet(downloadUrl, {
|
}
|
||||||
headers: { 'User-Agent': 'KrunkerCivilianClient' },
|
return map;
|
||||||
}, (res) => {
|
}
|
||||||
// Follow redirects (with domain validation)
|
|
||||||
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
// ── Update check ──
|
||||||
const redirectUrl = res.headers.location;
|
|
||||||
electronLog.log('[KCC-Update] Download redirected to:', redirectUrl);
|
interface ReleaseAsset {
|
||||||
if (!isAllowedRedirect(redirectUrl)) {
|
name: string;
|
||||||
electronLog.error('[KCC-Update] Download redirect to untrusted host blocked:', redirectUrl);
|
browser_download_url: string;
|
||||||
reject(new Error('Download redirect to untrusted host: ' + redirectUrl));
|
size: number;
|
||||||
return;
|
digest?: string;
|
||||||
}
|
}
|
||||||
doDownload(redirectUrl, redirectCount + 1);
|
|
||||||
return;
|
export function checkForUpdate(currentVersion: string): Promise<UpdateInfo | null> {
|
||||||
}
|
return new Promise((resolve) => {
|
||||||
|
electronLog.log('[KCC-Update] Checking for updates at:', UPDATE_CONFIG.checkUrl);
|
||||||
if (res.statusCode !== 200) {
|
electronLog.log('[KCC-Update] Current version:', currentVersion);
|
||||||
electronLog.error('[KCC-Update] Download returned status', res.statusCode, 'from:', downloadUrl);
|
|
||||||
reject(new Error('Download returned status ' + res.statusCode));
|
const req = httpsGet(UPDATE_CONFIG.checkUrl, {
|
||||||
return;
|
headers: { 'User-Agent': 'KrunkerCivilianClient/' + currentVersion },
|
||||||
}
|
}, (res) => {
|
||||||
|
electronLog.log('[KCC-Update] Check response status:', res.statusCode);
|
||||||
const total = parseInt(res.headers['content-length'] || '0', 10);
|
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
||||||
let received = 0;
|
const redirectUrl = res.headers.location;
|
||||||
|
electronLog.log('[KCC-Update] Redirected to:', redirectUrl);
|
||||||
const file = createWriteStream(tmpPath);
|
if (!isAllowedRedirect(redirectUrl)) {
|
||||||
res.on('data', (chunk: Buffer) => {
|
electronLog.error('[KCC-Update] Redirect to untrusted host blocked:', redirectUrl);
|
||||||
received += chunk.length;
|
resolve(null);
|
||||||
if (total > 0) {
|
return;
|
||||||
onProgress(Math.round(100 * received / total));
|
}
|
||||||
}
|
httpsGet(redirectUrl, {
|
||||||
});
|
headers: { 'User-Agent': 'KrunkerCivilianClient/' + currentVersion },
|
||||||
res.pipe(file);
|
}, (redirectRes) => {
|
||||||
|
electronLog.log('[KCC-Update] Redirect response status:', redirectRes.statusCode);
|
||||||
file.on('finish', () => {
|
handleResponse(redirectRes);
|
||||||
file.close(async () => {
|
}).on('error', (err) => {
|
||||||
try {
|
electronLog.error('[KCC-Update] Redirect error:', err);
|
||||||
if (expectedSha256) {
|
resolve(null);
|
||||||
const valid = await verifyChecksum(tmpPath, expectedSha256);
|
});
|
||||||
if (!valid) {
|
return;
|
||||||
electronLog.error('[KCC-Update] Checksum mismatch — file may be corrupted or tampered');
|
}
|
||||||
try { unlinkSync(tmpPath); } catch { /* ignore */ }
|
handleResponse(res);
|
||||||
reject(new Error('SHA-256 checksum mismatch'));
|
});
|
||||||
return;
|
|
||||||
}
|
async function handleResponse(res: import('http').IncomingMessage): Promise<void> {
|
||||||
electronLog.log('[KCC-Update] Checksum verified');
|
if (res.statusCode !== 200) {
|
||||||
}
|
electronLog.error('[KCC-Update] Check returned status', res.statusCode);
|
||||||
if (existsSync(destPath)) unlinkSync(destPath);
|
resolve(null);
|
||||||
renameSync(tmpPath, destPath);
|
return;
|
||||||
resolve();
|
}
|
||||||
} catch (err) {
|
|
||||||
reject(err);
|
let data = '';
|
||||||
}
|
res.on('data', (chunk: string) => { data += chunk; });
|
||||||
});
|
res.on('end', async () => {
|
||||||
});
|
try {
|
||||||
|
const release = JSON.parse(data);
|
||||||
file.on('error', (err) => {
|
const tagName: string = release.tag_name || '';
|
||||||
try { unlinkSync(tmpPath); } catch { /* ignore */ }
|
const remoteVersion = tagName.replace(/^v/i, '');
|
||||||
reject(err);
|
electronLog.log('[KCC-Update] Latest release:', remoteVersion, '| Current:', currentVersion);
|
||||||
});
|
|
||||||
|
if (!remoteVersion || !versionLessThan(currentVersion, remoteVersion)) {
|
||||||
res.on('error', (err) => {
|
electronLog.log('[KCC-Update] Already up to date');
|
||||||
try { unlinkSync(tmpPath); } catch { /* ignore */ }
|
resolve(null);
|
||||||
reject(err);
|
return;
|
||||||
});
|
}
|
||||||
});
|
|
||||||
|
const assets: ReleaseAsset[] = release.assets || [];
|
||||||
req.setTimeout(DOWNLOAD_TIMEOUT_MS, () => {
|
|
||||||
req.destroy();
|
// Determine update type: prefer minor (asar) over major (setup)
|
||||||
try { unlinkSync(tmpPath); } catch { /* ignore */ }
|
const asarAsset = assets.find((a) => ASSET_PATTERNS.asar.test(a.name));
|
||||||
reject(new Error('Download timed out'));
|
const setupAsset = assets.find((a) => ASSET_PATTERNS.setup.test(a.name));
|
||||||
});
|
const chosenAsset = asarAsset || setupAsset;
|
||||||
|
const updateType: UpdateType = asarAsset ? 'minor' : 'major';
|
||||||
req.on('error', (err) => {
|
|
||||||
try { unlinkSync(tmpPath); } catch { /* ignore */ }
|
if (!chosenAsset) {
|
||||||
reject(err);
|
electronLog.error('[KCC-Update] No app.asar or Setup.exe asset found in release', remoteVersion);
|
||||||
});
|
resolve(null);
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
doDownload(url);
|
|
||||||
});
|
if (!isAllowedRedirect(chosenAsset.browser_download_url)) {
|
||||||
}
|
electronLog.error('[KCC-Update] Download URL points to untrusted host:', chosenAsset.browser_download_url);
|
||||||
|
resolve(null);
|
||||||
export function installUpdate(installerPath: string): void {
|
return;
|
||||||
electronLog.log('[KCC-Update] Launching installer:', installerPath);
|
}
|
||||||
const child = spawn(installerPath, [], {
|
|
||||||
detached: true,
|
// Resolve SHA-256 checksum
|
||||||
stdio: 'ignore',
|
let sha256 = '';
|
||||||
});
|
if (UPDATE_CONFIG.checksumSource === 'digest') {
|
||||||
child.unref();
|
sha256 = (chosenAsset.digest || '').replace(/^sha256:/i, '');
|
||||||
app.quit();
|
if (!sha256) {
|
||||||
}
|
electronLog.error('[KCC-Update] No SHA-256 digest found for asset');
|
||||||
|
resolve(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fetch checksums.sha256 companion file
|
||||||
|
const checksumAsset = assets.find((a) => ASSET_PATTERNS.checksums.test(a.name));
|
||||||
|
if (checksumAsset) {
|
||||||
|
try {
|
||||||
|
const checksums = await fetchChecksums(checksumAsset.browser_download_url);
|
||||||
|
sha256 = checksums.get(chosenAsset.name) || '';
|
||||||
|
} catch (err) {
|
||||||
|
electronLog.error('[KCC-Update] Failed to fetch checksums:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!sha256) {
|
||||||
|
electronLog.warn('[KCC-Update] No checksum available — proceeding without verification');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
electronLog.log('[KCC-Update] Update available:', remoteVersion,
|
||||||
|
'| Type:', updateType,
|
||||||
|
'| SHA-256:', sha256 ? sha256.substring(0, 16) + '...' : 'none');
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
version: remoteVersion,
|
||||||
|
updateType,
|
||||||
|
downloadUrl: chosenAsset.browser_download_url,
|
||||||
|
fileSize: chosenAsset.size,
|
||||||
|
sha256,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
electronLog.error('[KCC-Update] Failed to parse release data:', err);
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
res.on('error', (err) => {
|
||||||
|
electronLog.error('[KCC-Update] Response error:', err);
|
||||||
|
resolve(null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
req.setTimeout(CHECK_TIMEOUT_MS, () => {
|
||||||
|
electronLog.error('[KCC-Update] Check timed out after', CHECK_TIMEOUT_MS, 'ms');
|
||||||
|
req.destroy();
|
||||||
|
resolve(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('error', (err) => {
|
||||||
|
electronLog.error('[KCC-Update] Check error:', err);
|
||||||
|
resolve(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Download ──
|
||||||
|
|
||||||
|
function verifyChecksum(filePath: string, expectedSha256: string): Promise<boolean> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const hash = createHash('sha256');
|
||||||
|
const stream = createReadStream(filePath);
|
||||||
|
stream.on('data', (chunk) => hash.update(chunk));
|
||||||
|
stream.on('end', () => {
|
||||||
|
const actual = hash.digest('hex');
|
||||||
|
electronLog.log('[KCC-Update] SHA-256 expected:', expectedSha256);
|
||||||
|
electronLog.log('[KCC-Update] SHA-256 actual: ', actual);
|
||||||
|
resolve(actual === expectedSha256);
|
||||||
|
});
|
||||||
|
stream.on('error', reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function downloadUpdate(url: string, destPath: string, onProgress: ProgressCallback, expectedSha256?: string): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const tmpPath = destPath + '.tmp';
|
||||||
|
|
||||||
|
function doDownload(downloadUrl: string, redirectCount = 0): void {
|
||||||
|
if (redirectCount > 5) {
|
||||||
|
reject(new Error('Too many redirects'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
electronLog.log('[KCC-Update] Downloading from:', downloadUrl);
|
||||||
|
const req = httpsGet(downloadUrl, {
|
||||||
|
headers: { 'User-Agent': 'KrunkerCivilianClient' },
|
||||||
|
}, (res) => {
|
||||||
|
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
||||||
|
const redirectUrl = res.headers.location;
|
||||||
|
electronLog.log('[KCC-Update] Download redirected to:', redirectUrl);
|
||||||
|
if (!isAllowedRedirect(redirectUrl)) {
|
||||||
|
electronLog.error('[KCC-Update] Download redirect to untrusted host blocked:', redirectUrl);
|
||||||
|
reject(new Error('Download redirect to untrusted host: ' + redirectUrl));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
doDownload(redirectUrl, redirectCount + 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.statusCode !== 200) {
|
||||||
|
electronLog.error('[KCC-Update] Download returned status', res.statusCode, 'from:', downloadUrl);
|
||||||
|
reject(new Error('Download returned status ' + res.statusCode));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const total = parseInt(res.headers['content-length'] || '0', 10);
|
||||||
|
let received = 0;
|
||||||
|
|
||||||
|
const file = createWriteStream(tmpPath);
|
||||||
|
res.on('data', (chunk: Buffer) => {
|
||||||
|
received += chunk.length;
|
||||||
|
if (total > 0) {
|
||||||
|
onProgress(Math.round(100 * received / total));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
res.pipe(file);
|
||||||
|
|
||||||
|
file.on('finish', () => {
|
||||||
|
file.close(async () => {
|
||||||
|
try {
|
||||||
|
if (expectedSha256) {
|
||||||
|
const valid = await verifyChecksum(tmpPath, expectedSha256);
|
||||||
|
if (!valid) {
|
||||||
|
electronLog.error('[KCC-Update] Checksum mismatch — file may be corrupted or tampered');
|
||||||
|
try { unlinkSync(tmpPath); } catch { /* ignore */ }
|
||||||
|
reject(new Error('SHA-256 checksum mismatch'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
electronLog.log('[KCC-Update] Checksum verified');
|
||||||
|
}
|
||||||
|
if (existsSync(destPath)) unlinkSync(destPath);
|
||||||
|
renameSync(tmpPath, destPath);
|
||||||
|
resolve();
|
||||||
|
} catch (err) {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
file.on('error', (err) => {
|
||||||
|
try { unlinkSync(tmpPath); } catch { /* ignore */ }
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
res.on('error', (err) => {
|
||||||
|
try { unlinkSync(tmpPath); } catch { /* ignore */ }
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
req.setTimeout(DOWNLOAD_TIMEOUT_MS, () => {
|
||||||
|
req.destroy();
|
||||||
|
try { unlinkSync(tmpPath); } catch { /* ignore */ }
|
||||||
|
reject(new Error('Download timed out'));
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('error', (err) => {
|
||||||
|
try { unlinkSync(tmpPath); } catch { /* ignore */ }
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
doDownload(url);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Install / Apply ──
|
||||||
|
|
||||||
|
export function installUpdate(installerPath: string): void {
|
||||||
|
electronLog.log('[KCC-Update] Launching installer:', installerPath);
|
||||||
|
const child = spawn(installerPath, [], {
|
||||||
|
detached: true,
|
||||||
|
stdio: 'ignore',
|
||||||
|
});
|
||||||
|
child.unref();
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyMinorUpdate(pendingAsarPath: string): void {
|
||||||
|
const resourcesDir = dirname(pendingAsarPath);
|
||||||
|
const exePath = app.getPath('exe');
|
||||||
|
const tempDir = join(app.getPath('temp'), 'kcc-update');
|
||||||
|
if (!existsSync(tempDir)) mkdirSync(tempDir, { recursive: true });
|
||||||
|
|
||||||
|
electronLog.log('[KCC-Update] Applying minor update via swap script');
|
||||||
|
electronLog.log('[KCC-Update] Resources dir:', resourcesDir);
|
||||||
|
electronLog.log('[KCC-Update] Exe path:', exePath);
|
||||||
|
electronLog.log('[KCC-Update] PID:', process.pid);
|
||||||
|
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
const scriptPath = join(tempDir, 'swap-asar.ps1');
|
||||||
|
writeFileSync(scriptPath, SWAP_SCRIPT_PS1);
|
||||||
|
const child = spawn('powershell.exe', [
|
||||||
|
'-ExecutionPolicy', 'Bypass',
|
||||||
|
'-File', scriptPath,
|
||||||
|
'-ProcessId', String(process.pid),
|
||||||
|
'-ResourcesDir', resourcesDir,
|
||||||
|
'-ExePath', exePath,
|
||||||
|
], { detached: true, stdio: 'ignore' });
|
||||||
|
child.unref();
|
||||||
|
} else {
|
||||||
|
const scriptPath = join(tempDir, 'swap-asar.sh');
|
||||||
|
writeFileSync(scriptPath, SWAP_SCRIPT_BASH, { mode: 0o755 });
|
||||||
|
const child = spawn('bash', [
|
||||||
|
scriptPath, String(process.pid), resourcesDir, exePath,
|
||||||
|
], { detached: true, stdio: 'ignore' });
|
||||||
|
child.unref();
|
||||||
|
}
|
||||||
|
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ const nodeBuiltins = builtinModules.flatMap((m) => [m, `node:${m}`]);
|
|||||||
const isProd = process.env.NODE_ENV === 'production' || !process.argv.includes('--mode');
|
const isProd = process.env.NODE_ENV === 'production' || !process.argv.includes('--mode');
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
define: {
|
||||||
|
__UPDATE_SOURCE__: JSON.stringify(process.env.UPDATE_SOURCE || 'gitea'),
|
||||||
|
},
|
||||||
build: {
|
build: {
|
||||||
lib: {
|
lib: {
|
||||||
entry: resolve(__dirname, 'src/main/index.ts'),
|
entry: resolve(__dirname, 'src/main/index.ts'),
|
||||||
|
|||||||
Reference in New Issue
Block a user