@pmate/cli 0.37.1 → 0.38.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 -7
- package/dist/index.js +20 -53
- package/dist/templates/electron/call-preload.cjs +2 -1
- package/dist/templates/electron/calls.cjs +2 -2
- package/dist/templates/electron/main.cjs +6 -9
- package/dist/templates/electron/preload.cjs +5 -4
- package/package.json +1 -1
- package/dist/templates/electron/resources.cjs +0 -32
package/README.md
CHANGED
|
@@ -270,31 +270,36 @@ Meili's Host may pass `--actor-profile-id` with `PMATE_LOG_DELEGATE_TOKEN` in th
|
|
|
270
270
|
|
|
271
271
|
Packaged installers are automatically uploaded to Aliyun OSS (`parrot-static`), using the same credentials as Android APK releases. The completion output and build report include public download URLs, SHA-256 checksums, and a `release.json` URL. Configure `oss.public.region` and `aliyun.ak/sk` in your PMate configuration. Conflicting published versions require an app version bump. `--dir` produces local unpacked output only; `--dry-run` does not upload.
|
|
272
272
|
|
|
273
|
-
`pmate app build <ns:app> --target electron --config <file>`
|
|
273
|
+
`pmate app build <ns:app> --target electron --config <file>` packages a native shell that loads its configured HTTPS origin directly, without building or embedding web resources, and produces macOS DMG/ZIP or Windows NSIS/ZIP artifacts. Packaged releases require a CLI login with admin permission in the app namespace; `--dir` and `--dry-run` stay local. The first build downloads pinned Electron and electron-builder versions. Current targets: macOS and Windows, `arm64` and `x64`; macOS packages require a macOS build host.
|
|
274
274
|
|
|
275
275
|
```sh
|
|
276
276
|
pmate app build pmate:hello --target electron --config apps/hello/desktop.config.json --platform mac --arch arm64 --out-dir release/desktop
|
|
277
277
|
```
|
|
278
278
|
|
|
279
|
-
Config (
|
|
279
|
+
Config schema 2 (`icon` is relative to the config file; migrate schema 1 by removing `web` and changing `schemaVersion` to 2):
|
|
280
280
|
|
|
281
281
|
```json
|
|
282
282
|
{
|
|
283
|
-
"schemaVersion":
|
|
283
|
+
"schemaVersion": 2,
|
|
284
284
|
"app": "pmate:hello",
|
|
285
285
|
"productName": "Going Merry",
|
|
286
286
|
"bundleId": "chat.pmate.goingmerry",
|
|
287
287
|
"version": "0.1.0",
|
|
288
288
|
"origin": "https://hello.pmate.chat",
|
|
289
289
|
"authOrigins": ["https://auth.pmate.chat"],
|
|
290
|
-
"icon": "desktop/icon.png"
|
|
291
|
-
"web": { "cwd": ".", "build": ["pnpm", "run", "build"], "dist": "dist" }
|
|
290
|
+
"icon": "desktop/icon.png"
|
|
292
291
|
}
|
|
293
292
|
```
|
|
294
293
|
|
|
295
294
|
Use `--dry-run` to inspect the plan, `--dir` for an unpacked `.app`, or `--signed` to require a configured macOS signing identity. Default builds are ad-hoc signed for local use; they are not notarized distribution releases. Signed builds use electron-builder's standard signing/notarization environment variables; verify notarization separately before distributing.
|
|
296
295
|
|
|
297
|
-
The shell
|
|
296
|
+
The shell loads the deployed HTTPS website. Packaging requires only the desktop configuration, icon and native runtime dependencies: no frontend package manager, source tree or `dist` is needed. Web UI, business logic and compatible frontend bridge SDK changes ship through web deployment; the next open or user refresh receives them. Do not reload an active conversation or call automatically. Rebuild installers only when installed preload/main-process bridge code, native capabilities/permissions, or the Electron runtime must change. The first migration from an embedded renderer needs one new installer.
|
|
297
|
+
|
|
298
|
+
`@pmate/bridge` provides `getDesktopBridge()`, runtime `appVersion`, `electronVersion`, `bridgeVersion`, `contentSource`, and callable capability detection. npm SDK versions are independent of the installed host protocol. Missing host capabilities must be shown as unsupported or requiring a shell upgrade; never infer them from the Electron engine version. Preload APIs are exposed only to the configured origin's main frame; auth windows do not receive native APIs. IPC validates the sender and frame, with Node disabled, sandbox and context isolation enabled. The local offline page offers retry without clearing the existing user-data partition. Website CSP and cache policy apply to remote pages.
|
|
299
|
+
|
|
300
|
+
Verify changes with `pnpm exec vitest run src/tests/electron*.test.ts` and `pnpm typecheck`. With `PMATE_TEST_ELECTRON_PATH` pointing to an Electron executable, run `node scripts/test-electron-online-runtime.cjs` for real HTTPS loading, web revision changes without a rebuild, IPC isolation, offline retry and data persistence. Set `PMATE_TEST_LIVE_ORIGIN=https://merry.pmate.chat` to include production SPA loading. Run `node scripts/test-electron-calls-runtime.cjs` for the isolated incoming-call flow. Certificate bypass is restricted to the local test process; production code keeps certificate validation enabled.
|
|
301
|
+
|
|
302
|
+
The native application menu includes **检查更新…**, which checks installed shell releases through apps-api and opens the verified installer after confirmation. Installation remains manual. Each build prints its artifact directory and signing report; packaged releases upload installers and register the version; `--dir` stays local.
|
|
298
303
|
|
|
299
304
|
### 日志统计默认口径
|
|
300
305
|
|
|
@@ -330,6 +335,6 @@ pmate service ios build td:time-duck-ios --scheme TimeDuck --team D9D59BE76S
|
|
|
330
335
|
|
|
331
336
|
Register an `ios` service with `repository` and `dir`. `--cwd` overrides the repository root; `--project` / `--workspace` paths are relative to the app directory. Exactly one discovered project/workspace is required unless explicitly selected. Always provide `--scheme`; `--team` overrides the project's signing team. `--json` returns version, Build, archive and log paths after upload.
|
|
332
337
|
|
|
333
|
-
The first invocation requires `--last-build <highest-used-build>` (0 only for a new
|
|
338
|
+
The first invocation for each marketing version requires `--last-build <highest-used-build-for-this-version>` (0 only for a new version). The next Build is one greater than the maximum of the project value, imported value and locally reserved sequence for that version. Reservations, including failed builds, are kept separately per version. To migrate published date-based numbers, first increase MARKETING_VERSION and set CURRENT_PROJECT_VERSION to 0, then import 0 for the new version to start at Build 1. Import the current Apple maximum again after uploads outside this CLI or on another machine; this command does not query Apple build history. Do not lower or delete the sequence to reuse a number. Burgess performs the Apple history check and version migration automatically.
|
|
334
339
|
|
|
335
340
|
Sequences, retained archives, derived data and logs live in `~/.pmate/ios-builds/`. Project/source version files are not changed. A bundle-level exclusive lock prevents simultaneous builds on the same machine; it does not coordinate different machines. Build/upload failures keep the reserved number and logs; retry consumes the next number. After an interrupted process, remove a leftover `.lock` file only after confirming no build is still running. Builds require automatic signing; resolve Apple account/certificate/profile errors in Xcode. Extension or App targets with fixed plist versions must produce matching archive metadata or the upload is stopped.
|
package/dist/index.js
CHANGED
|
@@ -17537,7 +17537,6 @@ var StoreCommandHandler = class {
|
|
|
17537
17537
|
|
|
17538
17538
|
// src/electronBuild.ts
|
|
17539
17539
|
var import_node_child_process9 = require("child_process");
|
|
17540
|
-
var import_node_crypto5 = require("crypto");
|
|
17541
17540
|
var import_node_fs19 = require("fs");
|
|
17542
17541
|
var import_node_os9 = require("os");
|
|
17543
17542
|
var import_node_path24 = __toESM(require("path"));
|
|
@@ -17585,7 +17584,7 @@ var ELECTRON_VERSION = "44.5.1";
|
|
|
17585
17584
|
var BUILDER_VERSION = "26.15.3";
|
|
17586
17585
|
function readDesktopConfig(file, appId) {
|
|
17587
17586
|
const c = JSON.parse((0, import_node_fs19.readFileSync)(file, "utf8"));
|
|
17588
|
-
if (c.schemaVersion !==
|
|
17587
|
+
if (c.schemaVersion !== 2 || c.app !== appId) throw new Error("Desktop config schemaVersion/app mismatch");
|
|
17589
17588
|
for (const key of ["productName", "bundleId", "version", "icon"]) {
|
|
17590
17589
|
if (typeof c[key] !== "string" || !c[key].trim()) throw new Error(`Missing desktop ${key}`);
|
|
17591
17590
|
}
|
|
@@ -17598,26 +17597,9 @@ function readDesktopConfig(file, appId) {
|
|
|
17598
17597
|
origin(c.origin);
|
|
17599
17598
|
if (!Array.isArray(c.authOrigins)) throw new Error("authOrigins must be an array");
|
|
17600
17599
|
c.authOrigins.forEach(origin);
|
|
17601
|
-
if (
|
|
17600
|
+
if ("web" in c) throw new Error("Remote desktop builds do not accept web build configuration; remove web");
|
|
17602
17601
|
return c;
|
|
17603
17602
|
}
|
|
17604
|
-
function collectStaticFiles(root) {
|
|
17605
|
-
const files = [];
|
|
17606
|
-
const walk = (dir) => {
|
|
17607
|
-
for (const entry of (0, import_node_fs19.readdirSync)(dir, { withFileTypes: true })) {
|
|
17608
|
-
const full = import_node_path24.default.join(dir, entry.name);
|
|
17609
|
-
if (entry.isSymbolicLink()) throw new Error(`Symlinks are not allowed in desktop resources: ${entry.name}`);
|
|
17610
|
-
if (entry.name.startsWith(".") || /\.(?:pem|key|p12|pfx)$/i.test(entry.name) || /bot-key|session\.json/i.test(entry.name)) throw new Error(`Private or hidden file in desktop resources: ${entry.name}`);
|
|
17611
|
-
if (entry.isDirectory()) walk(full);
|
|
17612
|
-
else if (entry.isFile() && !entry.name.endsWith(".map")) files.push(import_node_path24.default.relative(root, full));
|
|
17613
|
-
else if (!entry.isFile()) throw new Error(`Unsupported resource: ${entry.name}`);
|
|
17614
|
-
}
|
|
17615
|
-
};
|
|
17616
|
-
if ((0, import_node_fs19.lstatSync)(root).isSymbolicLink()) throw new Error("Static resource root cannot be a symlink");
|
|
17617
|
-
walk(root);
|
|
17618
|
-
if (!files.includes("index.html")) throw new Error("Web build must produce index.html");
|
|
17619
|
-
return files.sort();
|
|
17620
|
-
}
|
|
17621
17603
|
async function runBuildCommand(command, cwd, env = process.env) {
|
|
17622
17604
|
await new Promise((resolve, reject) => {
|
|
17623
17605
|
const child = (0, import_node_child_process9.spawn)(command[0], command.slice(1), { cwd, env, stdio: "inherit", shell: false });
|
|
@@ -17629,40 +17611,21 @@ async function buildDesktop(args) {
|
|
|
17629
17611
|
const file = import_node_path24.default.resolve(args.config);
|
|
17630
17612
|
const c = readDesktopConfig(file, args.id);
|
|
17631
17613
|
const base = import_node_path24.default.dirname(file);
|
|
17632
|
-
const cwd = import_node_path24.default.resolve(base, c.web.cwd);
|
|
17633
|
-
const dist = import_node_path24.default.resolve(cwd, c.web.dist);
|
|
17634
17614
|
const icon = import_node_path24.default.resolve(base, c.icon);
|
|
17635
17615
|
const output = import_node_path24.default.resolve(args.outDir);
|
|
17636
17616
|
if (!(0, import_node_fs19.existsSync)(icon) || !(0, import_node_fs19.statSync)(icon).isFile()) throw new Error(`Desktop icon not found: ${icon}`);
|
|
17637
17617
|
if (!["mac", "win"].includes(args.platform) || !["arm64", "x64"].includes(args.arch)) throw new Error("Supported target: mac|win --arch arm64|x64");
|
|
17638
17618
|
if (args.platform === "mac" && process.platform !== "darwin") throw new Error("macOS desktop packaging requires a macOS build host");
|
|
17639
|
-
const
|
|
17640
|
-
const rel = import_node_path24.default.relative(parent, child);
|
|
17641
|
-
return !rel || !rel.startsWith("..") && !import_node_path24.default.isAbsolute(rel);
|
|
17642
|
-
};
|
|
17643
|
-
if (within(dist, cwd) || within(dist, output) || within(output, cwd)) throw new Error("Build output, web source and static resource paths overlap");
|
|
17644
|
-
const plan = { app: c.app, productName: c.productName, version: c.version, target: args.platform, arch: args.arch, command: c.web.build, cwd, dist, output, electron: ELECTRON_VERSION, builder: BUILDER_VERSION, signed: args.signed };
|
|
17619
|
+
const plan = { app: c.app, productName: c.productName, version: c.version, target: args.platform, arch: args.arch, contentSource: "remote", origin: c.origin, output, electron: ELECTRON_VERSION, builder: BUILDER_VERSION, signed: args.signed };
|
|
17645
17620
|
console.log(JSON.stringify({ phase: "plan", ...plan }, null, 2));
|
|
17646
17621
|
if (args.dryRun) return;
|
|
17647
|
-
const started = Date.now();
|
|
17648
|
-
await runBuildCommand(c.web.build, cwd);
|
|
17649
|
-
if (!(0, import_node_fs19.existsSync)(import_node_path24.default.join(dist, "index.html")) || (0, import_node_fs19.statSync)(import_node_path24.default.join(dist, "index.html")).mtimeMs < started - 1e3) throw new Error("Build did not produce a fresh index.html; refusing stale resources");
|
|
17650
|
-
const files = collectStaticFiles(dist);
|
|
17651
17622
|
(0, import_node_fs19.mkdirSync)(output, { recursive: true });
|
|
17652
17623
|
const staging = (0, import_node_fs19.mkdtempSync)(import_node_path24.default.join(output, ".desktop-build-"));
|
|
17653
17624
|
const appDir = import_node_path24.default.join(staging, "app");
|
|
17654
|
-
(0, import_node_fs19.mkdirSync)(
|
|
17655
|
-
const manifest = {};
|
|
17656
|
-
for (const name of files) {
|
|
17657
|
-
const source = import_node_path24.default.join(dist, name), target = import_node_path24.default.join(appDir, "renderer", name);
|
|
17658
|
-
(0, import_node_fs19.mkdirSync)(import_node_path24.default.dirname(target), { recursive: true });
|
|
17659
|
-
(0, import_node_fs19.cpSync)(source, target);
|
|
17660
|
-
manifest[name] = (0, import_node_crypto5.createHash)("sha256").update((0, import_node_fs19.readFileSync)(source)).digest("hex");
|
|
17661
|
-
}
|
|
17625
|
+
(0, import_node_fs19.mkdirSync)(appDir);
|
|
17662
17626
|
const templateDir = import_node_path24.default.join(__dirname, "templates", "electron");
|
|
17663
17627
|
(0, import_node_fs19.cpSync)(templateDir, appDir, { recursive: true });
|
|
17664
17628
|
(0, import_node_fs19.writeFileSync)(import_node_path24.default.join(appDir, "desktop-config.json"), JSON.stringify({ app: c.app, productName: c.productName, origin: c.origin, authOrigins: c.authOrigins, bundleId: c.bundleId }));
|
|
17665
|
-
(0, import_node_fs19.writeFileSync)(import_node_path24.default.join(appDir, "resource-manifest.json"), JSON.stringify(manifest, null, 2));
|
|
17666
17629
|
(0, import_node_fs19.writeFileSync)(import_node_path24.default.join(appDir, "package.json"), JSON.stringify({ name: c.bundleId.replace(/\./g, "-"), version: c.version, description: `${c.productName} desktop application`, author: "PMate", main: "main.cjs", dependencies: { "@pmate/chat-sdk": "0.11.7" } }));
|
|
17667
17630
|
await runBuildCommand(["npm", "install", "--omit=dev", "--no-audit", "--no-fund"], appDir);
|
|
17668
17631
|
const cache = import_node_path24.default.join((0, import_node_os9.homedir)(), ".cache", "pmate", "electron", `${ELECTRON_VERSION}-${BUILDER_VERSION}`);
|
|
@@ -17687,7 +17650,7 @@ async function buildDesktop(args) {
|
|
|
17687
17650
|
const configPath = import_node_path24.default.join(staging, "builder.json");
|
|
17688
17651
|
(0, import_node_fs19.writeFileSync)(configPath, JSON.stringify(builderConfig));
|
|
17689
17652
|
await runBuildCommand([process.execPath, import_node_path24.default.join(cache, "node_modules", "electron-builder", "cli.js"), "--config", configPath, args.platform === "mac" ? "--mac" : "--win", `--${args.arch}`, "--publish", "never"], appDir, { ...process.env, ...args.signed ? {} : { CSC_IDENTITY_AUTO_DISCOVERY: "false" } });
|
|
17690
|
-
const result = { ...plan, signature: args.signed ? args.platform === "mac" ? "configured-signing-required; verify notarization before distribution" : "configured-signing-required; verify Authenticode before distribution" : args.platform === "mac" ? "ad-hoc; local development" : "unsigned; local development", artifacts: import_node_path24.default.join(staging, "artifacts")
|
|
17653
|
+
const result = { ...plan, signature: args.signed ? args.platform === "mac" ? "configured-signing-required; verify notarization before distribution" : "configured-signing-required; verify Authenticode before distribution" : args.platform === "mac" ? "ad-hoc; local development" : "unsigned; local development", artifacts: import_node_path24.default.join(staging, "artifacts") };
|
|
17691
17654
|
(0, import_node_fs19.writeFileSync)(import_node_path24.default.join(staging, "build-report.json"), JSON.stringify(result, null, 2));
|
|
17692
17655
|
const publication = args.dir ? void 0 : await uploadDesktopArtifacts(result.artifacts, c.app, c.version, args.platform, args.arch);
|
|
17693
17656
|
(0, import_node_fs19.writeFileSync)(import_node_path24.default.join(staging, "build-report.json"), JSON.stringify({ ...result, publication }, null, 2));
|
|
@@ -18412,14 +18375,14 @@ var import_promises6 = require("readline/promises");
|
|
|
18412
18375
|
|
|
18413
18376
|
// src/services/IosBuildService.ts
|
|
18414
18377
|
var import_node_child_process11 = require("child_process");
|
|
18415
|
-
var
|
|
18378
|
+
var import_node_crypto6 = require("crypto");
|
|
18416
18379
|
var import_node_fs23 = require("fs");
|
|
18417
18380
|
var import_node_os12 = __toESM(require("os"));
|
|
18418
18381
|
var import_node_path28 = __toESM(require("path"));
|
|
18419
18382
|
|
|
18420
18383
|
// src/services/AndroidApkReleaseService.ts
|
|
18421
18384
|
var import_node_child_process10 = require("child_process");
|
|
18422
|
-
var
|
|
18385
|
+
var import_node_crypto5 = require("crypto");
|
|
18423
18386
|
var import_node_fs22 = require("fs");
|
|
18424
18387
|
var import_node_os11 = __toESM(require("os"));
|
|
18425
18388
|
var import_node_path27 = __toESM(require("path"));
|
|
@@ -18890,7 +18853,7 @@ function listVersionedToolCandidates(parentDir, relativeToolPath) {
|
|
|
18890
18853
|
}
|
|
18891
18854
|
function sha256File(filePath) {
|
|
18892
18855
|
return new Promise((resolve, reject) => {
|
|
18893
|
-
const hash = (0,
|
|
18856
|
+
const hash = (0, import_node_crypto5.createHash)("sha256");
|
|
18894
18857
|
const stream = (0, import_node_fs22.createReadStream)(filePath);
|
|
18895
18858
|
stream.on("data", (chunk) => hash.update(chunk));
|
|
18896
18859
|
stream.on("error", reject);
|
|
@@ -18967,7 +18930,7 @@ var IosBuildService = class {
|
|
|
18967
18930
|
if (!bundleId || !/^\d+\.\d+(?:\.\d+)?$/.test(version ?? "") || !/^[A-Z0-9]{10}$/.test(team ?? "")) throw new Error("Missing or invalid bundle ID, MARKETING_VERSION or signing team; use --team if needed");
|
|
18968
18931
|
if (service.mobile && "bundleId" in service.mobile && service.mobile.bundleId !== bundleId) throw new Error("Service bundle ID does not match application target");
|
|
18969
18932
|
const currentBuild = iosBuildInteger(cfg.CURRENT_PROJECT_VERSION);
|
|
18970
|
-
const key = (0,
|
|
18933
|
+
const key = (0, import_node_crypto6.createHash)("sha256").update(bundleId).digest("hex");
|
|
18971
18934
|
const statePath = import_node_path28.default.join(root, `${key}.json`);
|
|
18972
18935
|
const lockPath = import_node_path28.default.join(root, `${key}.lock`);
|
|
18973
18936
|
let lock;
|
|
@@ -18981,14 +18944,18 @@ var IosBuildService = class {
|
|
|
18981
18944
|
(0, import_node_fs23.writeFileSync)(lock, JSON.stringify({ pid: process.pid, bundleId }));
|
|
18982
18945
|
const previous = (0, import_node_fs23.existsSync)(statePath) ? JSON.parse((0, import_node_fs23.readFileSync)(statePath, "utf8")) : void 0;
|
|
18983
18946
|
if (previous && previous.bundleId !== bundleId) throw new Error("iOS build state bundle ID mismatch");
|
|
18984
|
-
|
|
18985
|
-
|
|
18947
|
+
const buildsByVersion = { ...previous?.buildsByVersion };
|
|
18948
|
+
if (previous) buildsByVersion[previous.version] = Math.max(buildsByVersion[previous.version] ?? 0, iosBuildInteger(previous.build));
|
|
18949
|
+
const priorBuild = buildsByVersion[version];
|
|
18950
|
+
if (priorBuild === void 0 && options.lastBuild === void 0) throw new Error("First build for this marketing version requires --last-build <highest Apple build for this version>; use 0 only for a new version. Import again after external uploads or changing machines.");
|
|
18951
|
+
const build = Math.max(currentBuild, priorBuild ?? 0, options.lastBuild ?? 0) + 1;
|
|
18986
18952
|
iosBuildInteger(build);
|
|
18987
18953
|
const buildDir = import_node_path28.default.join(root, key, `${version}-${build}`);
|
|
18988
18954
|
(0, import_node_fs23.mkdirSync)(buildDir, { recursive: true, mode: 448 });
|
|
18989
18955
|
const archivePath = import_node_path28.default.join(buildDir, "App.xcarchive");
|
|
18990
18956
|
const logPath = import_node_path28.default.join(buildDir, "build.log");
|
|
18991
|
-
|
|
18957
|
+
buildsByVersion[version] = build;
|
|
18958
|
+
const state = { bundleId, version, build, buildsByVersion, status: "reserved", archivePath, logPath };
|
|
18992
18959
|
const persist = () => {
|
|
18993
18960
|
const temp = `${statePath}.${process.pid}.tmp`;
|
|
18994
18961
|
(0, import_node_fs23.writeFileSync)(temp, JSON.stringify(state, null, 2) + "\n", { mode: 384 });
|
|
@@ -22260,7 +22227,7 @@ var RoleCommandHandler = class {
|
|
|
22260
22227
|
|
|
22261
22228
|
// src/commands/AccountCommandHandler.ts
|
|
22262
22229
|
var import_node_fs26 = require("fs");
|
|
22263
|
-
var
|
|
22230
|
+
var import_node_crypto7 = require("crypto");
|
|
22264
22231
|
var import_node_path31 = __toESM(require("path"));
|
|
22265
22232
|
init_sessionStore();
|
|
22266
22233
|
var AccountCommandHandler = class {
|
|
@@ -22860,7 +22827,7 @@ var AccountCommandHandler = class {
|
|
|
22860
22827
|
return process.env.HOME || process.env.USERPROFILE || process.cwd();
|
|
22861
22828
|
}
|
|
22862
22829
|
generateBotKeyFile(app, name) {
|
|
22863
|
-
const { publicKey, privateKey } = (0,
|
|
22830
|
+
const { publicKey, privateKey } = (0, import_node_crypto7.generateKeyPairSync)("ed25519");
|
|
22864
22831
|
return {
|
|
22865
22832
|
type: "pmate-bot-key",
|
|
22866
22833
|
version: 1,
|
|
@@ -22982,12 +22949,12 @@ var AccountCommandHandler = class {
|
|
|
22982
22949
|
});
|
|
22983
22950
|
}
|
|
22984
22951
|
signBotChallenge(challenge, privateKey) {
|
|
22985
|
-
const key = (0,
|
|
22952
|
+
const key = (0, import_node_crypto7.createPrivateKey)({
|
|
22986
22953
|
key: Buffer.from(privateKey, "base64url"),
|
|
22987
22954
|
format: "der",
|
|
22988
22955
|
type: "pkcs8"
|
|
22989
22956
|
});
|
|
22990
|
-
return (0,
|
|
22957
|
+
return (0, import_node_crypto7.sign)(
|
|
22991
22958
|
null,
|
|
22992
22959
|
Buffer.from(JSON.stringify({
|
|
22993
22960
|
id: challenge.id,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const { contextBridge, ipcRenderer } = require('electron')
|
|
2
|
-
|
|
2
|
+
const origin = process.argv.find(value => value.startsWith('--pmate-origin='))?.slice('--pmate-origin='.length)
|
|
3
|
+
if (process.isMainFrame && location.origin === origin && location.pathname === '/_desktop/call') contextBridge.exposeInMainWorld('pmateIncomingCall', Object.freeze({
|
|
3
4
|
getSession: () => ipcRenderer.invoke('pmate:call:session'),
|
|
4
5
|
updateState: state => ipcRenderer.invoke('pmate:call:state', state),
|
|
5
6
|
onCloseRequested: handler => {
|
|
@@ -19,12 +19,12 @@ function createDesktopCalls({ appId, instanceId, origin, BrowserWindow, ipcMain,
|
|
|
19
19
|
if (current?.callId === callId) return current.ready
|
|
20
20
|
destroy()
|
|
21
21
|
const window = new BrowserWindow({ width: 420, height: 680, minWidth: 380, minHeight: 540, title: '来电', show: false,
|
|
22
|
-
backgroundColor: '#ffffff', webPreferences: { preload: path.join(__dirname, 'call-preload.cjs'), partition: 'persist:desktop',
|
|
22
|
+
backgroundColor: '#ffffff', webPreferences: { preload: path.join(__dirname, 'call-preload.cjs'), additionalArguments: [`--pmate-origin=${origin}`], partition: 'persist:desktop',
|
|
23
23
|
nodeIntegration: false, contextIsolation: true, sandbox: true, webSecurity: true, autoplayPolicy: 'no-user-gesture-required' } })
|
|
24
24
|
let resolve, reject
|
|
25
25
|
const ready = new Promise((yes, no) => { resolve = yes; reject = no })
|
|
26
26
|
const entry = current = { callId, window, ready, resolve, reject, phase: 'incoming', confirming: false }
|
|
27
|
-
entry.timer = setTimeout(() => { if (current === entry) { reject(new Error('Incoming call window timeout')); destroy() } },
|
|
27
|
+
entry.timer = setTimeout(() => { if (current === entry) { reject(new Error('Incoming call window timeout')); destroy() } }, 15000)
|
|
28
28
|
window.webContents.setWindowOpenHandler(() => ({ action: 'deny' }))
|
|
29
29
|
for (const event of ['will-navigate', 'will-redirect', 'will-attach-webview']) window.webContents.on(event, e => e.preventDefault())
|
|
30
30
|
window.webContents.on('did-fail-load', (_event, code) => { if (code !== -3 && current === entry) { reject(new Error('Incoming call window failed')); destroy() } })
|
|
@@ -2,14 +2,12 @@ const { createUpdateChecker, installUpdateHandlers } = require('./updates.cjs')
|
|
|
2
2
|
const { app, BrowserWindow, session, shell, Menu, desktopCapturer, ipcMain, Notification, dialog } = require('electron')
|
|
3
3
|
const path = require('node:path')
|
|
4
4
|
const { installNotificationSettings } = require('./notification-settings.cjs')
|
|
5
|
-
const { createResourceHandler } = require('./resources.cjs')
|
|
6
5
|
const { installMediaPermissions } = require('./media.cjs')
|
|
7
6
|
const { createDesktopNotifications } = require('./notifications.cjs')
|
|
8
7
|
const { desktopInstanceId, createDesktopReach } = require('./reach.cjs')
|
|
9
8
|
const { createRendererReach } = require('./reach-renderer.cjs')
|
|
10
9
|
const { createDesktopCalls, supportsDesktopCalls } = require('./calls.cjs')
|
|
11
10
|
const config = require('./desktop-config.json')
|
|
12
|
-
const manifest = require('./resource-manifest.json')
|
|
13
11
|
app.setName(config.productName)
|
|
14
12
|
if (process.platform === 'win32') app.setAppUserModelId(config.bundleId)
|
|
15
13
|
// Stable across installer versions; separate from every other packaged PMate app.
|
|
@@ -32,13 +30,13 @@ function restrict(window) {
|
|
|
32
30
|
window.webContents.on('did-create-window', child => restrict(child))
|
|
33
31
|
}
|
|
34
32
|
function createWindow() {
|
|
35
|
-
mainWindow = new BrowserWindow({ width: 1180, height: 820, minWidth: 380, minHeight: 540, title: config.productName, backgroundColor: '#f6f8fb', webPreferences: { additionalArguments: [`--pmate-app-version=${app.getVersion()}`], preload: path.join(__dirname, 'preload.cjs'), partition: 'persist:desktop', nodeIntegration: false, contextIsolation: true, sandbox: true, webSecurity: true } })
|
|
33
|
+
mainWindow = new BrowserWindow({ width: 1180, height: 820, minWidth: 380, minHeight: 540, title: config.productName, backgroundColor: '#f6f8fb', webPreferences: { additionalArguments: [`--pmate-app-version=${app.getVersion()}`, `--pmate-origin=${config.origin}`, `--pmate-desktop-calls=${supportsDesktopCalls(config.app)}`], preload: path.join(__dirname, 'preload.cjs'), partition: 'persist:desktop', nodeIntegration: false, contextIsolation: true, sandbox: true, webSecurity: true } })
|
|
36
34
|
restrict(mainWindow)
|
|
37
35
|
mainWindow.webContents.on('did-start-navigation', (_event, _url, _inPlace, mainFrame) => { if (mainFrame) desktopNotifications?.clearContext() })
|
|
38
36
|
mainWindow.webContents.on('did-fail-load', (_event, code, _description, url, mainFrame) => {
|
|
39
|
-
if (mainFrame && code !== -3 && url !== `${config.origin}/_desktop/offline`) void mainWindow.loadURL(`${config.origin}/_desktop/offline`)
|
|
37
|
+
if (mainFrame && code !== -3 && url !== `${config.origin}/_desktop/offline`) void mainWindow.loadURL(`${config.origin}/_desktop/offline`).catch(error => console.error("Offline page failed:", error.message))
|
|
40
38
|
})
|
|
41
|
-
void mainWindow.loadURL(config.origin)
|
|
39
|
+
void mainWindow.loadURL(config.origin).catch(error => console.error("Page load failed:", error.message))
|
|
42
40
|
mainWindow.on('closed', () => { mainWindow = null })
|
|
43
41
|
}
|
|
44
42
|
if (!app.requestSingleInstanceLock()) app.quit()
|
|
@@ -46,13 +44,12 @@ else {
|
|
|
46
44
|
app.on('second-instance', () => { if (mainWindow) { if (mainWindow.isMinimized()) mainWindow.restore(); mainWindow.focus() } })
|
|
47
45
|
app.whenReady().then(() => {
|
|
48
46
|
const desktopSession = session.fromPartition('persist:desktop')
|
|
49
|
-
const resources = createResourceHandler(path.join(__dirname, 'renderer'), config.origin, manifest)
|
|
50
47
|
desktopSession.protocol.handle('https', request => {
|
|
51
48
|
if (request.url === `${config.origin}/_desktop/offline`) {
|
|
52
49
|
const escape = text => text.replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c])
|
|
53
|
-
return new Response(`<!doctype html><html><head><meta charset="utf-8"><title>${escape(config.productName)}</title></head><body style="font:18px system-ui;max-width:560px;margin:15vh auto;padding:24px"><h1>${escape(config.productName)}</h1><p>Connection unavailable.
|
|
50
|
+
return new Response(`<!doctype html><html><head><meta charset="utf-8"><title>${escape(config.productName)}</title></head><body style="font:18px system-ui;max-width:560px;margin:15vh auto;padding:24px"><h1>${escape(config.productName)}</h1><p>Connection unavailable. Connect to the internet to load the app. Your account data has not been cleared.</p><form action="/"><button style="padding:12px 24px">Try again</button></form></body></html>`, { headers: { 'content-type': 'text/html; charset=utf-8', 'content-security-policy': "default-src 'none'; style-src 'unsafe-inline'; form-action 'self'" } })
|
|
54
51
|
}
|
|
55
|
-
return
|
|
52
|
+
return desktopSession.fetch(request, { bypassCustomProtocolHandlers: true })
|
|
56
53
|
})
|
|
57
54
|
installMediaPermissions(desktopSession, { origin: config.origin, getWindow: () => mainWindow, getWindows: () => [mainWindow, desktopCalls?.getWindow()], desktopCapturer, BrowserWindow, ipcMain })
|
|
58
55
|
ipcMain.handle('pmate:screen-settings', event => {
|
|
@@ -64,7 +61,7 @@ else {
|
|
|
64
61
|
const checkForUpdates = createUpdateChecker({ app, dialog, shell, appId: config.app })
|
|
65
62
|
Menu.setApplicationMenu(Menu.buildFromTemplate([
|
|
66
63
|
{ role: 'appMenu' }, { role: 'editMenu' }, { role: 'viewMenu' }, { role: 'windowMenu' },
|
|
67
|
-
{ label: config.productName, submenu: [{ label: '检查更新…', click: checkForUpdates }, { type: 'separator' }, { label: 'Back to app', click: () => { if (!mainWindow) createWindow(); else void mainWindow.loadURL(config.origin) } }] },
|
|
64
|
+
{ label: config.productName, submenu: [{ label: '检查更新…', click: checkForUpdates }, { type: 'separator' }, { label: 'Back to app', click: () => { if (!mainWindow) createWindow(); else void mainWindow.loadURL(config.origin).catch(error => console.error("Page load failed:", error.message)) } }] },
|
|
68
65
|
]))
|
|
69
66
|
desktopNotifications = createDesktopNotifications({ ipcMain, Notification, origin: config.origin, appId: config.app, getWindow: () => mainWindow, showWindow: () => {
|
|
70
67
|
if (!mainWindow) createWindow()
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const { contextBridge, ipcRenderer } = require('electron')
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
const origin = process.argv.find(value => value.startsWith('--pmate-origin='))?.slice('--pmate-origin='.length)
|
|
3
|
+
if (process.isMainFrame && location.origin === origin) contextBridge.exposeInMainWorld('pmateDesktop', Object.freeze({
|
|
4
|
+
runtime: Object.freeze({ platform: process.platform, appVersion: process.argv.find(value => value.startsWith('--pmate-app-version='))?.slice('--pmate-app-version='.length), electronVersion: process.versions.electron, bridgeVersion: '1.0.0', contentSource: 'remote' }),
|
|
4
5
|
updates: Object.freeze({
|
|
5
6
|
check: () => ipcRenderer.invoke('pmate:update:check'),
|
|
6
7
|
download: () => ipcRenderer.invoke('pmate:update:download'),
|
|
@@ -19,11 +20,11 @@ contextBridge.exposeInMainWorld('pmateDesktop', Object.freeze({
|
|
|
19
20
|
configure: identity => ipcRenderer.invoke('pmate:reach:configure', identity),
|
|
20
21
|
stop: () => ipcRenderer.invoke('pmate:reach:stop'),
|
|
21
22
|
}),
|
|
22
|
-
calls: Object.freeze({
|
|
23
|
+
calls: process.argv.includes('--pmate-desktop-calls=true') ? Object.freeze({
|
|
23
24
|
incoming: (callId, teamId) => ipcRenderer.invoke('pmate:call:incoming', callId, teamId),
|
|
24
25
|
handleReach: event => ipcRenderer.invoke('pmate:call:reach', event),
|
|
25
26
|
setPeople: people => ipcRenderer.invoke('pmate:call:people', people),
|
|
26
|
-
}),
|
|
27
|
+
}) : undefined,
|
|
27
28
|
notifications: Object.freeze({
|
|
28
29
|
openSettings: () => ipcRenderer.invoke('pmate:notification:settings'),
|
|
29
30
|
present: input => ipcRenderer.invoke('pmate:notification:present', input),
|
package/package.json
CHANGED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
const fs = require('node:fs')
|
|
2
|
-
const path = require('node:path')
|
|
3
|
-
const crypto = require('node:crypto')
|
|
4
|
-
const TYPES = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript', '.mjs': 'text/javascript', '.css': 'text/css', '.json': 'application/json', '.svg': 'image/svg+xml', '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.webp': 'image/webp', '.ico': 'image/x-icon', '.woff': 'font/woff', '.woff2': 'font/woff2', '.ttf': 'font/ttf', '.wasm': 'application/wasm', '.mp3': 'audio/mpeg', '.mp4': 'video/mp4' }
|
|
5
|
-
function createResourceHandler(root, origin, manifest) {
|
|
6
|
-
// Reject a stale or altered renderer before displaying it.
|
|
7
|
-
for (const [file, digest] of Object.entries(manifest)) {
|
|
8
|
-
if (file.split(/[\\/]/).includes('..') || path.isAbsolute(file)) throw new Error('Invalid resource manifest')
|
|
9
|
-
if (crypto.createHash('sha256').update(fs.readFileSync(path.join(root, file))).digest('hex') !== digest) throw new Error(`Resource integrity check failed: ${file}`)
|
|
10
|
-
}
|
|
11
|
-
return (request) => {
|
|
12
|
-
const url = new URL(request.url)
|
|
13
|
-
if (url.origin !== origin) return null
|
|
14
|
-
if (!['GET', 'HEAD'].includes(request.method)) return new Response('Method not allowed', { status: 405 })
|
|
15
|
-
let name
|
|
16
|
-
try { name = decodeURIComponent(url.pathname).slice(1) } catch { return new Response('Invalid path', { status: 400 }) }
|
|
17
|
-
if (name.includes('\\') || name.includes('\0') || name.split('/').some(p => p === '..' || p === '.')) return new Response('Invalid path', { status: 400 })
|
|
18
|
-
// Exact files come from the build manifest. Only extensionless document routes get the SPA shell.
|
|
19
|
-
if (!Object.hasOwn(manifest, name)) {
|
|
20
|
-
if (name === '_desktop/call' || (!path.extname(name) && request.headers.get('sec-fetch-dest') === 'document')) name = 'index.html'
|
|
21
|
-
else if (!name) name = 'index.html'
|
|
22
|
-
else return new Response('Resource not found', { status: 404 })
|
|
23
|
-
}
|
|
24
|
-
const bytes = fs.readFileSync(path.join(root, name))
|
|
25
|
-
return new Response(request.method === 'HEAD' ? null : bytes, { headers: {
|
|
26
|
-
'content-type': TYPES[path.extname(name)] || 'application/octet-stream',
|
|
27
|
-
'content-length': String(bytes.length), 'x-content-type-options': 'nosniff',
|
|
28
|
-
'content-security-policy': "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; font-src 'self' data:; connect-src https: wss:; media-src 'self' blob: https:; worker-src 'self' blob:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'",
|
|
29
|
-
} })
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
module.exports = { createResourceHandler }
|