@pmate/cli 0.37.2 → 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 +237 -274
- 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
|
@@ -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 }
|