pakstr 0.30.0 → 1.0.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/README.md +12 -2
- package/android-template/app/build.gradle.kts +1 -0
- package/android-template/app/src/androidTest/java/com/pakstr/app/PakstrWebBridgeTest.kt +153 -0
- package/android-template/app/src/main/assets/nip07.js +124 -74
- package/android-template/app/src/main/java/com/pakstr/app/AppOrigin.kt +77 -0
- package/android-template/app/src/main/java/com/pakstr/app/PakstrBridge.kt +5 -5
- package/android-template/app/src/main/java/com/pakstr/app/PakstrWebBridge.kt +89 -0
- package/android-template/app/src/main/java/com/pakstr/app/WebMessageRouter.kt +186 -0
- package/android-template/app/src/main/java/com/pakstr/app/WebViewController.kt +65 -49
- package/android-template/app/src/main/java/com/pakstr/app/signer/NostrBridge.kt +69 -489
- package/android-template/app/src/test/java/com/pakstr/app/AppOriginTest.kt +114 -0
- package/android-template/app/src/test/java/com/pakstr/app/WebMessageRouterTest.kt +279 -0
- package/android-template/app/src/test/java/com/pakstr/app/signer/NostrBridgeDispatchTest.kt +113 -0
- package/android-template/gradle/libs.versions.toml +2 -0
- package/desktop-runtime/README.md +108 -0
- package/desktop-runtime/dist/main.js +22211 -0
- package/desktop-runtime/dist/preload.js +53 -0
- package/dist/cli.js +17 -3
- package/dist/commands/build.js +1 -0
- package/dist/commands/buildDesktop.js +140 -0
- package/dist/desktop/common/artifacts.js +16 -0
- package/dist/desktop/common/asar.js +60 -0
- package/dist/desktop/common/electron.js +50 -0
- package/dist/desktop/common/files.js +30 -0
- package/dist/desktop/common/fuses.js +62 -0
- package/dist/desktop/common/packager.js +46 -0
- package/dist/desktop/linux.js +425 -0
- package/dist/desktop/macos.js +125 -0
- package/dist/desktop/stage.js +117 -0
- package/dist/desktop/windows.js +384 -0
- package/dist/desktop/zip.js +204 -0
- package/dist/shared/nip46Pairing.js +278 -0
- package/package.json +14 -3
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// desktop-runtime/src/preload.js
|
|
4
|
+
var { contextBridge, ipcRenderer } = require("electron");
|
|
5
|
+
if (location.protocol === "pakstr:" && location.host === "app") {
|
|
6
|
+
const nostr = {
|
|
7
|
+
async call(method, payload) {
|
|
8
|
+
let response;
|
|
9
|
+
try {
|
|
10
|
+
response = await ipcRenderer.invoke("pakstr:nostr", method, payload);
|
|
11
|
+
} catch {
|
|
12
|
+
throw { error: "SIGNER_ERROR", message: "Invalid request" };
|
|
13
|
+
}
|
|
14
|
+
if (!response?.ok) throw response?.error ?? { error: "SIGNER_ERROR", message: "Unknown signer error" };
|
|
15
|
+
return response.result;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
contextBridge.executeInMainWorld({
|
|
19
|
+
func: (bridge) => {
|
|
20
|
+
if (window.nostr) return;
|
|
21
|
+
window.nostr = {
|
|
22
|
+
getPublicKey: async () => bridge.call("getPublicKey"),
|
|
23
|
+
signEvent: async (event) => bridge.call("signEvent", event),
|
|
24
|
+
nip04: {
|
|
25
|
+
encrypt: async (pubkey, plaintext) => bridge.call("nip04_encrypt", { pubkey, plaintext }),
|
|
26
|
+
decrypt: async (pubkey, ciphertext) => bridge.call("nip04_decrypt", { pubkey, ciphertext })
|
|
27
|
+
},
|
|
28
|
+
nip44: {
|
|
29
|
+
encrypt: async (pubkey, plaintext) => bridge.call("nip44_encrypt", { pubkey, plaintext }),
|
|
30
|
+
decrypt: async (pubkey, ciphertext) => bridge.call("nip44_decrypt", { pubkey, ciphertext })
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
args: [nostr]
|
|
35
|
+
});
|
|
36
|
+
contextBridge.exposeInMainWorld("PakstrBridge", {
|
|
37
|
+
getApiBaseUrl: () => {
|
|
38
|
+
try {
|
|
39
|
+
return ipcRenderer.sendSync("pakstr:get-api-base-url") ?? null;
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
setApiBaseUrl: (url) => {
|
|
45
|
+
if (typeof url !== "string") return false;
|
|
46
|
+
try {
|
|
47
|
+
return ipcRenderer.sendSync("pakstr:set-api-base-url", url) === true;
|
|
48
|
+
} catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.runCLI = runCLI;
|
|
7
7
|
const path_1 = __importDefault(require("path"));
|
|
8
8
|
const build_1 = require("./commands/build");
|
|
9
|
+
const buildDesktop_1 = require("./commands/buildDesktop");
|
|
9
10
|
const sign_1 = require("./commands/sign");
|
|
10
11
|
const publish_1 = require("./commands/publish");
|
|
11
12
|
const run_1 = require("./commands/run");
|
|
@@ -40,9 +41,19 @@ async function runCLI(argv) {
|
|
|
40
41
|
case "init":
|
|
41
42
|
await (0, init_1.initCommand)({ force: args.includes("--force"), out: configFlag });
|
|
42
43
|
return;
|
|
43
|
-
case "build":
|
|
44
|
-
|
|
44
|
+
case "build": {
|
|
45
|
+
const target = args.includes("--target") ? extractFlag(args, "--target") : "android";
|
|
46
|
+
if (target === "android") {
|
|
47
|
+
await (0, build_1.buildCommand)(configFlag ?? undefined);
|
|
48
|
+
}
|
|
49
|
+
else if (target && buildDesktop_1.DESKTOP_TARGETS.includes(target)) {
|
|
50
|
+
await (0, buildDesktop_1.buildDesktopCommand)(configFlag ?? undefined, target);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
throw new Error(`pakstr build --target must be one of: android, ${buildDesktop_1.DESKTOP_TARGETS.join(", ")}`);
|
|
54
|
+
}
|
|
45
55
|
return;
|
|
56
|
+
}
|
|
46
57
|
case "sign":
|
|
47
58
|
await (0, sign_1.signCommand)(configFlag ?? undefined);
|
|
48
59
|
return;
|
|
@@ -112,7 +123,7 @@ Pakstr — one-step nsec -> build -> sign -> publish.
|
|
|
112
123
|
|
|
113
124
|
Usage:
|
|
114
125
|
pakstr init [--force] [--config <path>]
|
|
115
|
-
pakstr build [--config <path>]
|
|
126
|
+
pakstr build [--config <path>] [--target android|macos-arm64|windows-x64|linux-x64]
|
|
116
127
|
pakstr sign [--config <path>]
|
|
117
128
|
pakstr nsite [--config <path>] [--dir <path>] [--dry-run]
|
|
118
129
|
pakstr publish [--config <path>] [--dry-run] [--verify] [--out-json <path>]
|
|
@@ -120,6 +131,9 @@ Usage:
|
|
|
120
131
|
pakstr delete --publisher [--config <path>] [--execute]
|
|
121
132
|
pakstr run [--config <path>] [--dry-run] [--verify-publish] [--publish-out-json <path>]
|
|
122
133
|
|
|
134
|
+
build --target macos-arm64 writes an ad-hoc signed <appId>-macos-arm64.zip next to build.out (macOS only).
|
|
135
|
+
build --target windows-x64 writes an unsigned <appId>-windows-x64-setup.exe and portable .zip (needs NSIS 3).
|
|
136
|
+
build --target linux-x64 writes <appId>-linux-x64.AppImage and portable .zip (macOS/Linux, needs squashfs-tools).
|
|
123
137
|
delete inventories app-scoped Zapstore events by default; --execute submits NIP-09 requests.
|
|
124
138
|
--delete-blobs inventories release blobs; HTTP DELETE requires --execute --delete-blobs.
|
|
125
139
|
--publisher inventories all configured-publisher events; --execute submits NIP-09 requests only.
|
package/dist/commands/build.js
CHANGED
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.buildCommand = buildCommand;
|
|
7
|
+
exports.validateOutputParent = validateOutputParent;
|
|
7
8
|
const fs_1 = __importDefault(require("fs"));
|
|
8
9
|
const path_1 = __importDefault(require("path"));
|
|
9
10
|
const template_1 = require("../android/template");
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.DESKTOP_MIN_NODE_VERSION = exports.DESKTOP_TARGETS = void 0;
|
|
7
|
+
exports.assertDesktopNodeVersion = assertDesktopNodeVersion;
|
|
8
|
+
exports.buildDesktopCommand = buildDesktopCommand;
|
|
9
|
+
const fs_1 = __importDefault(require("fs"));
|
|
10
|
+
const os_1 = __importDefault(require("os"));
|
|
11
|
+
const path_1 = __importDefault(require("path"));
|
|
12
|
+
const pakstrConfig_1 = require("../core/pakstrConfig");
|
|
13
|
+
const linux_1 = require("../desktop/linux");
|
|
14
|
+
const macos_1 = require("../desktop/macos");
|
|
15
|
+
const stage_1 = require("../desktop/stage");
|
|
16
|
+
const windows_1 = require("../desktop/windows");
|
|
17
|
+
const artifactPublisher_1 = require("./artifactPublisher");
|
|
18
|
+
const build_1 = require("./build");
|
|
19
|
+
exports.DESKTOP_TARGETS = [macos_1.MACOS_ARM64_TARGET, windows_1.WINDOWS_X64_TARGET, linux_1.LINUX_X64_TARGET];
|
|
20
|
+
/**
|
|
21
|
+
* Same as the package's engines minimum, checked at run time because npm only
|
|
22
|
+
* warns about engines (and skips the optional @electron tools) on older Node.
|
|
23
|
+
*/
|
|
24
|
+
exports.DESKTOP_MIN_NODE_VERSION = "22.12.0";
|
|
25
|
+
function assertDesktopNodeVersion(version = process.versions.node) {
|
|
26
|
+
const [major, minor] = version.split(".").map(Number);
|
|
27
|
+
const [minMajor, minMinor] = exports.DESKTOP_MIN_NODE_VERSION.split(".").map(Number);
|
|
28
|
+
if (!Number.isInteger(major) || !Number.isInteger(minor) || major < minMajor || (major === minMajor && minor < minMinor)) {
|
|
29
|
+
throw new Error(`Desktop builds require Node.js >= ${exports.DESKTOP_MIN_NODE_VERSION} (running ${version}). Upgrade Node.js.`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* `pakstr build --target macos-arm64` — an ad-hoc signed `<appName>.app` in
|
|
34
|
+
* `<appId>-macos-arm64.zip` next to build.out. No nsec, keystore, or Docker
|
|
35
|
+
* involved; the Android build, sign, and publish flows are untouched.
|
|
36
|
+
*/
|
|
37
|
+
async function buildDesktopCommand(configPath, target) {
|
|
38
|
+
if (target === windows_1.WINDOWS_X64_TARGET)
|
|
39
|
+
return buildWindowsDesktop(configPath);
|
|
40
|
+
if (target === linux_1.LINUX_X64_TARGET)
|
|
41
|
+
return buildLinuxDesktop(configPath);
|
|
42
|
+
if (target !== macos_1.MACOS_ARM64_TARGET)
|
|
43
|
+
throw new Error(`Unsupported desktop target: ${target}`);
|
|
44
|
+
assertDesktopNodeVersion();
|
|
45
|
+
if (process.platform !== "darwin") {
|
|
46
|
+
throw new Error("macOS desktop packages can only be built on macOS (codesign and ditto are required)");
|
|
47
|
+
}
|
|
48
|
+
const config = (0, pakstrConfig_1.loadPakstrConfig)(configPath);
|
|
49
|
+
(0, stage_1.assertDesktopAppName)(config.app.appName);
|
|
50
|
+
const finalPath = (0, stage_1.desktopOutputPath)(config.build.out, config.app.appId, target);
|
|
51
|
+
console.log("\n🚀 pakstr build --target", target);
|
|
52
|
+
console.log("📦 App:", config.app.appName, `(${(0, macos_1.macosBundleId)(config.app.appId)})`);
|
|
53
|
+
console.log("🌐 Web:", config.build.web);
|
|
54
|
+
console.log("🖥️ Out:", finalPath);
|
|
55
|
+
(0, build_1.validateOutputParent)(finalPath);
|
|
56
|
+
const workDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), "pakstr-desktop-"));
|
|
57
|
+
try {
|
|
58
|
+
const stagingDir = path_1.default.join(workDir, "app");
|
|
59
|
+
fs_1.default.mkdirSync(stagingDir);
|
|
60
|
+
(0, stage_1.stageDesktopApp)({ stagingDir, webDir: config.build.web, app: config.app, runtime: config.runtime });
|
|
61
|
+
const zipPath = await (0, macos_1.packageMacosApp)({ stagingDir, workDir, app: config.app });
|
|
62
|
+
(0, artifactPublisher_1.publishArtifact)((0, artifactPublisher_1.stageArtifact)(zipPath, finalPath, "macOS package"), finalPath);
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
fs_1.default.rmSync(workDir, { recursive: true, force: true });
|
|
66
|
+
}
|
|
67
|
+
console.log("\n✅ DESKTOP BUILD COMPLETE (ad-hoc signed, not notarized)");
|
|
68
|
+
console.log("📦 Output:", finalPath);
|
|
69
|
+
return finalPath;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* `pakstr build --target windows-x64` — an unsigned per-user NSIS installer
|
|
73
|
+
* `<appId>-windows-x64-setup.exe` and a portable `<appId>-windows-x64.zip`
|
|
74
|
+
* next to build.out. Builds on any host with NSIS 3 (`makensis`); returns the
|
|
75
|
+
* installer path.
|
|
76
|
+
*/
|
|
77
|
+
async function buildWindowsDesktop(configPath) {
|
|
78
|
+
assertDesktopNodeVersion();
|
|
79
|
+
const config = (0, pakstrConfig_1.loadPakstrConfig)(configPath);
|
|
80
|
+
(0, windows_1.assertWindowsAppIdentity)(config.app);
|
|
81
|
+
(0, windows_1.windowsVersion)(config.app.versionName);
|
|
82
|
+
const outputs = (0, windows_1.windowsOutputPaths)(config.build.out, config.app.appId);
|
|
83
|
+
console.log("\n🚀 pakstr build --target", windows_1.WINDOWS_X64_TARGET);
|
|
84
|
+
console.log("📦 App:", config.app.appName, `(${config.app.appId})`);
|
|
85
|
+
console.log("🌐 Web:", config.build.web);
|
|
86
|
+
console.log("🖥️ Out:", outputs.installer);
|
|
87
|
+
console.log("🖥️ Out:", outputs.portable);
|
|
88
|
+
(0, build_1.validateOutputParent)(outputs.installer);
|
|
89
|
+
const workDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), "pakstr-desktop-"));
|
|
90
|
+
try {
|
|
91
|
+
const stagingDir = path_1.default.join(workDir, "app");
|
|
92
|
+
fs_1.default.mkdirSync(stagingDir);
|
|
93
|
+
(0, stage_1.stageDesktopApp)({ stagingDir, webDir: config.build.web, app: config.app, runtime: config.runtime });
|
|
94
|
+
const built = await (0, windows_1.packageWindowsApp)({ stagingDir, workDir, app: config.app });
|
|
95
|
+
(0, artifactPublisher_1.publishArtifact)((0, artifactPublisher_1.stageArtifact)(built.installer, outputs.installer, "Windows installer"), outputs.installer);
|
|
96
|
+
(0, artifactPublisher_1.publishArtifact)((0, artifactPublisher_1.stageArtifact)(built.portable, outputs.portable, "Windows portable package"), outputs.portable);
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
fs_1.default.rmSync(workDir, { recursive: true, force: true });
|
|
100
|
+
}
|
|
101
|
+
console.log("\n✅ DESKTOP BUILD COMPLETE (unsigned)");
|
|
102
|
+
console.log("📦 Installer:", outputs.installer);
|
|
103
|
+
console.log("📦 Portable:", outputs.portable);
|
|
104
|
+
return outputs.installer;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* `pakstr build --target linux-x64` — an AppImage `<appId>-linux-x64.AppImage`
|
|
108
|
+
* and a portable `<appId>-linux-x64.zip` next to build.out. Builds on macOS or
|
|
109
|
+
* Linux with squashfs-tools; returns the AppImage path.
|
|
110
|
+
*/
|
|
111
|
+
async function buildLinuxDesktop(configPath) {
|
|
112
|
+
assertDesktopNodeVersion();
|
|
113
|
+
(0, linux_1.assertLinuxBuildHost)();
|
|
114
|
+
const config = (0, pakstrConfig_1.loadPakstrConfig)(configPath);
|
|
115
|
+
(0, stage_1.assertDesktopAppName)(config.app.appName);
|
|
116
|
+
const outputs = (0, linux_1.linuxOutputPaths)(config.build.out, config.app.appId);
|
|
117
|
+
console.log("\n🚀 pakstr build --target", linux_1.LINUX_X64_TARGET);
|
|
118
|
+
console.log("📦 App:", config.app.appName, `(${config.app.appId})`);
|
|
119
|
+
console.log("🌐 Web:", config.build.web);
|
|
120
|
+
for (const format of linux_1.LINUX_PACKAGE_FORMATS)
|
|
121
|
+
console.log("🖥️ Out:", outputs[format]);
|
|
122
|
+
(0, build_1.validateOutputParent)(outputs.appimage);
|
|
123
|
+
const workDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), "pakstr-desktop-"));
|
|
124
|
+
try {
|
|
125
|
+
const stagingDir = path_1.default.join(workDir, "app");
|
|
126
|
+
fs_1.default.mkdirSync(stagingDir);
|
|
127
|
+
(0, stage_1.stageDesktopApp)({ stagingDir, webDir: config.build.web, app: config.app, runtime: config.runtime });
|
|
128
|
+
const built = await (0, linux_1.packageLinuxApp)({ stagingDir, workDir, app: config.app, configDir: config.configDir });
|
|
129
|
+
for (const format of linux_1.LINUX_PACKAGE_FORMATS) {
|
|
130
|
+
(0, artifactPublisher_1.publishArtifact)((0, artifactPublisher_1.stageArtifact)(built[format], outputs[format], `Linux ${format} package`), outputs[format]);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
finally {
|
|
134
|
+
fs_1.default.rmSync(workDir, { recursive: true, force: true });
|
|
135
|
+
}
|
|
136
|
+
console.log("\n✅ DESKTOP BUILD COMPLETE (unsigned)");
|
|
137
|
+
for (const format of linux_1.LINUX_PACKAGE_FORMATS)
|
|
138
|
+
console.log(`📦 ${format === "appimage" ? "AppImage" : "Portable"}:`, outputs[format]);
|
|
139
|
+
return outputs.appimage;
|
|
140
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.desktopOutputDir = desktopOutputDir;
|
|
7
|
+
exports.desktopArtifactPath = desktopArtifactPath;
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
/** Desktop packages are written next to build.out, like the APK. */
|
|
10
|
+
function desktopOutputDir(buildOut) {
|
|
11
|
+
return path_1.default.dirname(path_1.default.resolve(buildOut));
|
|
12
|
+
}
|
|
13
|
+
/** `<dir>/<appId>-<target><suffix>`, e.g. com.example.app-windows-x64-setup.exe. */
|
|
14
|
+
function desktopArtifactPath(dir, appId, target, suffix) {
|
|
15
|
+
return path_1.default.join(dir, `${appId}-${target}${suffix}`);
|
|
16
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.verifyAppAsar = verifyAppAsar;
|
|
4
|
+
exports.assertAsarEntries = assertAsarEntries;
|
|
5
|
+
exports.asarHeaderHash = asarHeaderHash;
|
|
6
|
+
exports.assertAsarFileIntegrity = assertAsarFileIntegrity;
|
|
7
|
+
const stage_1 = require("../stage");
|
|
8
|
+
const files_1 = require("./files");
|
|
9
|
+
/** Files that must be in every packaged app.asar. */
|
|
10
|
+
const REQUIRED_ASAR_ENTRIES = [
|
|
11
|
+
"/package.json",
|
|
12
|
+
`/${stage_1.DESKTOP_DESCRIPTOR_FILE}`,
|
|
13
|
+
...stage_1.DESKTOP_RUNTIME_FILES.map((file) => `/runtime/${file}`),
|
|
14
|
+
"/web/index.html",
|
|
15
|
+
"/web/pakstr-runtime.json",
|
|
16
|
+
];
|
|
17
|
+
/** The packaged app.asar holds only the allowlisted layout and this app's descriptor. */
|
|
18
|
+
async function verifyAppAsar(asarPath, app) {
|
|
19
|
+
const asar = await import("@electron/asar");
|
|
20
|
+
assertAsarEntries(asar.listPackage(asarPath, { isPack: false }));
|
|
21
|
+
const descriptor = JSON.parse(asar.extractFile(asarPath, stage_1.DESKTOP_DESCRIPTOR_FILE).toString("utf8"));
|
|
22
|
+
if (descriptor.appId !== app.appId || descriptor.appName !== app.appName || Object.keys(descriptor).length !== 2) {
|
|
23
|
+
throw new Error(`Packaged ${stage_1.DESKTOP_DESCRIPTOR_FILE} does not match the app identity`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** Every packaged path is in the allowlist and the required files are present. */
|
|
27
|
+
function assertAsarEntries(entries) {
|
|
28
|
+
const normalized = entries.map((entry) => entry.replace(/\\/g, "/"));
|
|
29
|
+
const unexpected = normalized.filter((entry) => !(0, stage_1.isAllowedAppPath)(entry));
|
|
30
|
+
if (unexpected.length > 0)
|
|
31
|
+
throw new Error(`app.asar contains files outside the allowlist: ${unexpected.join(", ")}`);
|
|
32
|
+
const missing = REQUIRED_ASAR_ENTRIES.filter((entry) => !normalized.includes(entry));
|
|
33
|
+
if (missing.length > 0)
|
|
34
|
+
throw new Error(`app.asar is missing: ${missing.join(", ")}`);
|
|
35
|
+
}
|
|
36
|
+
/** SHA-256 of the archive's header: the value Electron's embedded asar integrity records. */
|
|
37
|
+
async function asarHeaderHash(asarPath) {
|
|
38
|
+
const asar = await import("@electron/asar");
|
|
39
|
+
return (0, files_1.sha256)(asar.getRawHeader(asarPath).headerString);
|
|
40
|
+
}
|
|
41
|
+
/** Every file in the archive is inside it (no unpacked or linked entries) and matches its SHA-256. */
|
|
42
|
+
async function assertAsarFileIntegrity(asarPath) {
|
|
43
|
+
const asar = await import("@electron/asar");
|
|
44
|
+
const walk = (node, prefix) => {
|
|
45
|
+
for (const [name, child] of Object.entries(node.files ?? {})) {
|
|
46
|
+
const rel = prefix ? `${prefix}/${name}` : name;
|
|
47
|
+
if (child.files) {
|
|
48
|
+
walk(child, rel);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (child.unpacked || child.link !== undefined)
|
|
52
|
+
throw new Error(`app.asar entry ${rel} is not stored in the archive`);
|
|
53
|
+
const hash = (0, files_1.sha256)(asar.extractFile(asarPath, rel));
|
|
54
|
+
if (child.integrity?.algorithm !== "SHA256" || child.integrity.hash !== hash) {
|
|
55
|
+
throw new Error(`app.asar entry ${rel} does not match its integrity hash`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
walk(asar.getRawHeader(asarPath).header, "");
|
|
60
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.LINUX_ELECTRON_ZIP = exports.WINDOWS_ELECTRON_ZIP = exports.MACOS_ELECTRON_ZIP = exports.ELECTRON_VERSION = void 0;
|
|
7
|
+
exports.fetchElectronZip = fetchElectronZip;
|
|
8
|
+
exports.assertPinnedElectronZip = assertPinnedElectronZip;
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const files_1 = require("./files");
|
|
11
|
+
/** The Electron the desktop runtime is built and tested against (desktop-runtime/package.json). */
|
|
12
|
+
exports.ELECTRON_VERSION = "45.0.0";
|
|
13
|
+
// Every pin is from the release's SHASUMS256.txt, so a swapped download or cache entry is rejected.
|
|
14
|
+
exports.MACOS_ELECTRON_ZIP = {
|
|
15
|
+
platform: "darwin",
|
|
16
|
+
arch: "arm64",
|
|
17
|
+
name: `electron-v${exports.ELECTRON_VERSION}-darwin-arm64.zip`,
|
|
18
|
+
sha256: "896c3a50b148e89a158dca08a6996a022ddf39b23aac871e107f5b4087f8caa9",
|
|
19
|
+
};
|
|
20
|
+
exports.WINDOWS_ELECTRON_ZIP = {
|
|
21
|
+
platform: "win32",
|
|
22
|
+
arch: "x64",
|
|
23
|
+
name: `electron-v${exports.ELECTRON_VERSION}-win32-x64.zip`,
|
|
24
|
+
sha256: "ae53887a23bb640369c93dbd38c408a2af5c0deb86f2fa76c6c244cb444b7f47",
|
|
25
|
+
};
|
|
26
|
+
exports.LINUX_ELECTRON_ZIP = {
|
|
27
|
+
platform: "linux",
|
|
28
|
+
arch: "x64",
|
|
29
|
+
name: `electron-v${exports.ELECTRON_VERSION}-linux-x64.zip`,
|
|
30
|
+
sha256: "22f6a10bd3f7fef987ddbb379d3375ce664ad0c98a9a50f80c89f1718a0c3e3a",
|
|
31
|
+
};
|
|
32
|
+
/** Download (or reuse the cached) pinned Electron zip and verify its SHA-256 ourselves. */
|
|
33
|
+
async function fetchElectronZip(pinned) {
|
|
34
|
+
const { downloadArtifact } = await import("@electron/get");
|
|
35
|
+
const zip = await downloadArtifact({
|
|
36
|
+
version: exports.ELECTRON_VERSION,
|
|
37
|
+
platform: pinned.platform,
|
|
38
|
+
arch: pinned.arch,
|
|
39
|
+
artifactName: "electron",
|
|
40
|
+
checksums: { [pinned.name]: pinned.sha256 },
|
|
41
|
+
});
|
|
42
|
+
await assertPinnedElectronZip(zip, pinned);
|
|
43
|
+
return zip;
|
|
44
|
+
}
|
|
45
|
+
/** The file is the pinned release zip: its name and SHA-256 both match. */
|
|
46
|
+
async function assertPinnedElectronZip(zip, pinned) {
|
|
47
|
+
if (path_1.default.basename(zip) !== pinned.name || (await (0, files_1.sha256File)(zip)) !== pinned.sha256) {
|
|
48
|
+
throw new Error(`Electron download does not match the pinned ${pinned.name} checksum: ${zip}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.sha256 = sha256;
|
|
7
|
+
exports.sha256File = sha256File;
|
|
8
|
+
exports.readHead = readHead;
|
|
9
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
10
|
+
const fs_1 = __importDefault(require("fs"));
|
|
11
|
+
const promises_1 = require("stream/promises");
|
|
12
|
+
function sha256(data) {
|
|
13
|
+
return crypto_1.default.createHash("sha256").update(data).digest("hex");
|
|
14
|
+
}
|
|
15
|
+
async function sha256File(file) {
|
|
16
|
+
const hash = crypto_1.default.createHash("sha256");
|
|
17
|
+
await (0, promises_1.pipeline)(fs_1.default.createReadStream(file), hash);
|
|
18
|
+
return hash.digest("hex");
|
|
19
|
+
}
|
|
20
|
+
/** Up to `length` bytes from the start of `file` (fewer if the file is shorter). */
|
|
21
|
+
function readHead(file, length) {
|
|
22
|
+
const fd = fs_1.default.openSync(file, "r");
|
|
23
|
+
try {
|
|
24
|
+
const head = Buffer.alloc(length);
|
|
25
|
+
return head.subarray(0, fs_1.default.readSync(fd, head, 0, length, 0));
|
|
26
|
+
}
|
|
27
|
+
finally {
|
|
28
|
+
fs_1.default.closeSync(fd);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DESKTOP_FUSES = void 0;
|
|
4
|
+
exports.desktopFuseConfig = desktopFuseConfig;
|
|
5
|
+
exports.assertFuseWire = assertFuseWire;
|
|
6
|
+
exports.flipDesktopFuses = flipDesktopFuses;
|
|
7
|
+
exports.verifyDesktopFuses = verifyDesktopFuses;
|
|
8
|
+
/**
|
|
9
|
+
* Electron fuses for every desktop package, by @electron/fuses option name.
|
|
10
|
+
* All are set explicitly (strictlyRequireAllFuses), so an Electron upgrade
|
|
11
|
+
* that adds a fuse fails the build instead of inheriting its default.
|
|
12
|
+
*/
|
|
13
|
+
exports.DESKTOP_FUSES = {
|
|
14
|
+
RunAsNode: false,
|
|
15
|
+
EnableCookieEncryption: true,
|
|
16
|
+
EnableNodeOptionsEnvironmentVariable: false,
|
|
17
|
+
EnableNodeCliInspectArguments: false,
|
|
18
|
+
EnableEmbeddedAsarIntegrityValidation: true,
|
|
19
|
+
OnlyLoadAppFromAsar: true,
|
|
20
|
+
LoadBrowserProcessSpecificV8Snapshot: false,
|
|
21
|
+
GrantFileProtocolExtraPrivileges: false,
|
|
22
|
+
WasmTrapHandlers: true,
|
|
23
|
+
};
|
|
24
|
+
// @electron/fuses FuseState byte values.
|
|
25
|
+
const FUSE_ENABLED = 0x31;
|
|
26
|
+
const FUSE_DISABLED = 0x30;
|
|
27
|
+
/** @electron/fuses config with wire indices resolved from the library's FuseV1Options enum. */
|
|
28
|
+
function desktopFuseConfig(fuseOptions) {
|
|
29
|
+
const config = { version: "1", strictlyRequireAllFuses: true };
|
|
30
|
+
for (const [name, enabled] of Object.entries(exports.DESKTOP_FUSES)) {
|
|
31
|
+
config[fuseIndex(fuseOptions, name)] = enabled;
|
|
32
|
+
}
|
|
33
|
+
return config;
|
|
34
|
+
}
|
|
35
|
+
/** Throws unless the binary's fuse wire (getCurrentFuseWire) matches DESKTOP_FUSES exactly. */
|
|
36
|
+
function assertFuseWire(wire, fuseOptions) {
|
|
37
|
+
const fuseCount = Object.keys(wire).filter((key) => /^\d+$/.test(key)).length;
|
|
38
|
+
if (wire.version !== "1" || fuseCount !== Object.keys(exports.DESKTOP_FUSES).length) {
|
|
39
|
+
throw new Error(`Unexpected Electron fuse wire (version ${String(wire.version)}, ${fuseCount} fuses)`);
|
|
40
|
+
}
|
|
41
|
+
for (const [name, enabled] of Object.entries(exports.DESKTOP_FUSES)) {
|
|
42
|
+
if (wire[fuseIndex(fuseOptions, name)] !== (enabled ? FUSE_ENABLED : FUSE_DISABLED)) {
|
|
43
|
+
throw new Error(`Electron fuse ${name} is not ${enabled ? "enabled" : "disabled"} in the packaged app`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function fuseIndex(fuseOptions, name) {
|
|
48
|
+
const index = fuseOptions[name];
|
|
49
|
+
if (typeof index !== "number")
|
|
50
|
+
throw new Error(`Unknown Electron fuse: ${name}`);
|
|
51
|
+
return index;
|
|
52
|
+
}
|
|
53
|
+
/** Flip DESKTOP_FUSES in a packaged app: the .app bundle on macOS, the executable elsewhere. */
|
|
54
|
+
async function flipDesktopFuses(target) {
|
|
55
|
+
const fuses = await import("@electron/fuses");
|
|
56
|
+
await fuses.flipFuses(target, desktopFuseConfig(fuses.FuseV1Options));
|
|
57
|
+
}
|
|
58
|
+
/** Read the fuse wire back from a packaged app and check it against DESKTOP_FUSES. */
|
|
59
|
+
async function verifyDesktopFuses(target) {
|
|
60
|
+
const fuses = await import("@electron/fuses");
|
|
61
|
+
assertFuseWire((await fuses.getCurrentFuseWire(target)), fuses.FuseV1Options);
|
|
62
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.basePackagerOptions = basePackagerOptions;
|
|
7
|
+
exports.runPackager = runPackager;
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const stage_1 = require("../stage");
|
|
11
|
+
const electron_1 = require("./electron");
|
|
12
|
+
/**
|
|
13
|
+
* @electron/packager options every desktop package shares. The staging
|
|
14
|
+
* directory already holds only the allowlisted layout; `ignore` enforces the
|
|
15
|
+
* same allowlist again. Platform builders add platform, arch, and identity.
|
|
16
|
+
*/
|
|
17
|
+
function basePackagerOptions(options) {
|
|
18
|
+
const { stagingDir, outDir, electronZipDir } = options;
|
|
19
|
+
return {
|
|
20
|
+
dir: stagingDir,
|
|
21
|
+
out: outDir,
|
|
22
|
+
electronZipDir,
|
|
23
|
+
electronVersion: electron_1.ELECTRON_VERSION,
|
|
24
|
+
asar: true,
|
|
25
|
+
prune: false,
|
|
26
|
+
derefSymlinks: false,
|
|
27
|
+
junk: true,
|
|
28
|
+
overwrite: false,
|
|
29
|
+
quiet: true,
|
|
30
|
+
ignore: (file) => file !== "" && !(0, stage_1.isAllowedAppPath)(file.replace(/\\/g, "/")),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Run @electron/packager and check its output folder holds `product` (the
|
|
35
|
+
* .app bundle or the app executable). Returns the folder and the product path.
|
|
36
|
+
*/
|
|
37
|
+
async function runPackager(options, product) {
|
|
38
|
+
const { packager } = await import("@electron/packager");
|
|
39
|
+
const [appDir] = await packager(options);
|
|
40
|
+
const productPath = path_1.default.join(appDir, product.name);
|
|
41
|
+
const stat = fs_1.default.lstatSync(productPath, { throwIfNoEntry: false });
|
|
42
|
+
if (!(product.type === "directory" ? stat?.isDirectory() : stat?.isFile())) {
|
|
43
|
+
throw new Error(`Packager did not produce ${product.name}`);
|
|
44
|
+
}
|
|
45
|
+
return { appDir, productPath };
|
|
46
|
+
}
|