Skip to content

Commit 1a34b5f

Browse files
committed
fix: harden post-3.10.1 code review findings
- Restore backward-compatible bundle-ID hash: name only included when --name is explicitly passed, so existing apps are not re-identified - Make camera/microphone entitlements opt-in via --camera/--microphone flags instead of being granted to all apps by default - Use fsExtra.move (atomic rename) instead of copy+remove in --install - Show popup window hostname instead of full URL as window title - Move resolvedName calculation after all name-resolution branches so GitHub-Actions fallback is reflected in the identifier - Refactor build_window to accept WindowBuildOptions struct (7 params -> 4) - Update docs (EN + CN) with macOS media permissions section
1 parent 3e46a72 commit 1a34b5f

12 files changed

Lines changed: 171 additions & 46 deletions

File tree

bin/builders/BaseBuilder.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -316,20 +316,16 @@ export default abstract class BaseBuilder {
316316
const appBundleName = path.basename(appBundlePath);
317317
const appDest = path.join('/Applications', appBundleName);
318318

319-
if (await fsExtra.pathExists(appDest)) {
320-
await fsExtra.remove(appDest);
321-
}
322-
323-
await fsExtra.copy(appBundlePath, appDest);
324-
await fsExtra.remove(appBundlePath);
319+
// fsExtra.move uses fs.rename (atomic on same filesystem) and falls back
320+
// to copy+remove only when moving across volumes.
321+
await fsExtra.move(appBundlePath, appDest, { overwrite: true });
325322

326323
logger.success(
327324
`✔ ${appBundleName.replace(/\.app$/, '')} installed to /Applications`,
328325
);
329-
logger.success('✔ Local app bundle removed');
330326
} catch (error) {
331327
logger.error(`✕ Failed to install ${appName}: ${error}`);
332-
logger.info(` The app bundle is still available at: ${appBundlePath}`);
328+
logger.info(` App bundle still available at: ${appBundlePath}`);
333329
}
334330
}
335331

bin/defaults.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ export const DEFAULT_PAKE_OPTIONS: PakeCliOptions = {
5151
ignoreCertificateErrors: false,
5252
newWindow: false,
5353
install: false,
54+
camera: false,
55+
microphone: false,
5456
};
5557

5658
// Just for cli development

bin/helpers/cli-program.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,16 @@ ${green('|_| \\__,_|_|\\_\\___| can turn any webpage into a desktop app with
256256
'Auto-install app to /Applications (macOS) after build and remove local bundle',
257257
DEFAULT.install,
258258
)
259+
.addOption(
260+
new Option('--camera', 'Request camera permission on macOS')
261+
.default(DEFAULT.camera)
262+
.hideHelp(),
263+
)
264+
.addOption(
265+
new Option('--microphone', 'Request microphone permission on macOS')
266+
.default(DEFAULT.microphone)
267+
.hideHelp(),
268+
)
259269
.version(packageJson.version, '-v, --version')
260270
.configureHelp({
261271
sortSubcommands: true,

bin/helpers/merge.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ export async function mergeConfig(
7979
minHeight,
8080
ignoreCertificateErrors,
8181
newWindow,
82+
camera,
83+
microphone,
8284
} = options;
8385

8486
const { platform } = process;
@@ -385,6 +387,35 @@ Terminal=false
385387
};
386388
}
387389

390+
// Write entitlements dynamically on macOS so camera/microphone are opt-in
391+
if (platform === 'darwin') {
392+
const entitlementEntries: string[] = [];
393+
if (camera) {
394+
entitlementEntries.push(
395+
' <key>com.apple.security.device.camera</key>\n <true/>',
396+
);
397+
}
398+
if (microphone) {
399+
entitlementEntries.push(
400+
' <key>com.apple.security.device.audio-input</key>\n <true/>',
401+
);
402+
}
403+
const entitlementsContent = `<?xml version="1.0" encoding="UTF-8"?>
404+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
405+
<plist version="1.0">
406+
<dict>
407+
${entitlementEntries.join('\n')}
408+
</dict>
409+
</plist>
410+
`;
411+
const entitlementsPath = path.join(
412+
npmDirectory,
413+
'src-tauri',
414+
'entitlements.plist',
415+
);
416+
await fsExtra.writeFile(entitlementsPath, entitlementsContent);
417+
}
418+
388419
// Save config file.
389420
const platformConfigPaths: PlatformMap = {
390421
win32: 'tauri.windows.conf.json',

bin/options/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,6 @@ export default async function handleOptions(
6464
name = generateLinuxPackageName(name);
6565
}
6666

67-
const resolvedName = name || 'pake-app';
68-
6967
if (name && !isValidName(name, platform)) {
7068
const LINUX_NAME_ERROR = `✕ Name should only include lowercase letters, numbers, and dashes (not leading dashes). Examples: com-123-xxx, 123pan, pan123, weread, we-read, 123.`;
7169
const DEFAULT_NAME_ERROR = `✕ Name should only include letters, numbers, dashes, and spaces (not leading dashes and spaces). Examples: 123pan, 123Pan, Pan123, weread, WeRead, WERead, we-read, We Read, 123.`;
@@ -80,10 +78,12 @@ export default async function handleOptions(
8078
}
8179
}
8280

81+
const resolvedName = name || 'pake-app';
82+
8383
const appOptions: PakeAppOptions = {
8484
...options,
8585
name: resolvedName,
86-
identifier: resolveIdentifier(url, resolvedName, options.identifier),
86+
identifier: resolveIdentifier(url, options.name, options.identifier),
8787
};
8888

8989
const iconPath = await handleIcon(appOptions, url);

bin/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,12 @@ export interface PakeCliOptions {
129129

130130
// Auto-install app to /Applications (macOS) after build, default false
131131
install: boolean;
132+
133+
// Request camera entitlement on macOS, default false
134+
camera: boolean;
135+
136+
// Request microphone entitlement on macOS, default false
137+
microphone: boolean;
132138
}
133139

134140
export interface PakeAppOptions extends PakeCliOptions {

bin/utils/info.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,31 @@ import prompts from 'prompts';
33
import ora from 'ora';
44
import chalk from 'chalk';
55

6-
// Generates a stable identifier based on the app URL and name.
7-
export function getIdentifier(url: string, name: string) {
6+
// Generates a stable identifier based on the app URL (and optionally name).
7+
// When name is provided it is included in the hash so two apps wrapping
8+
// the same URL can coexist. Omitting name preserves backward compatibility
9+
// with identifiers generated before V3.10.1.
10+
export function getIdentifier(url: string, name?: string) {
11+
const hashInput = name ? `${url}::${name}` : url;
812
const postFixHash = crypto
913
.createHash('md5')
10-
.update(`${url}::${name}`)
14+
.update(hashInput)
1115
.digest('hex')
1216
.substring(0, 6);
1317
return `com.pake.${postFixHash}`;
1418
}
1519

1620
export function resolveIdentifier(
1721
url: string,
18-
name: string,
22+
explicitName: string | undefined,
1923
customIdentifier?: string,
2024
) {
2125
const trimmedIdentifier = customIdentifier?.trim();
2226
if (trimmedIdentifier) {
2327
return trimmedIdentifier;
2428
}
2529

26-
return getIdentifier(url, name);
30+
return getIdentifier(url, explicitName);
2731
}
2832

2933
export async function promptText(

dist/cli.js

Lines changed: 44 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -171,21 +171,25 @@ let tauriConfig = {
171171
pake: pakeConf,
172172
};
173173

174-
// Generates a stable identifier based on the app URL and name.
174+
// Generates a stable identifier based on the app URL (and optionally name).
175+
// When name is provided it is included in the hash so two apps wrapping
176+
// the same URL can coexist. Omitting name preserves backward compatibility
177+
// with identifiers generated before V3.10.1.
175178
function getIdentifier(url, name) {
179+
const hashInput = name ? `${url}::${name}` : url;
176180
const postFixHash = crypto
177181
.createHash('md5')
178-
.update(`${url}::${name}`)
182+
.update(hashInput)
179183
.digest('hex')
180184
.substring(0, 6);
181185
return `com.pake.${postFixHash}`;
182186
}
183-
function resolveIdentifier(url, name, customIdentifier) {
187+
function resolveIdentifier(url, explicitName, customIdentifier) {
184188
const trimmedIdentifier = customIdentifier?.trim();
185189
if (trimmedIdentifier) {
186190
return trimmedIdentifier;
187191
}
188-
return getIdentifier(url, name);
192+
return getIdentifier(url, explicitName);
189193
}
190194
async function promptText(message, initial) {
191195
const response = await prompts({
@@ -491,7 +495,7 @@ async function mergeConfig(url, options, tauriConf) {
491495
await fsExtra.copy(sourcePath, destPath);
492496
}
493497
}));
494-
const { width, height, fullscreen, maximize, hideTitleBar, alwaysOnTop, appVersion, darkMode, disabledWebShortcuts, activationShortcut, userAgent, showSystemTray, systemTrayIcon, useLocalFile, identifier, name = 'pake-app', resizable = true, inject, proxyUrl, installerLanguage, hideOnClose, incognito, title, wasm, enableDragDrop, multiInstance, multiWindow, startToTray, forceInternalNavigation, internalUrlRegex, zoom, minWidth, minHeight, ignoreCertificateErrors, newWindow, } = options;
498+
const { width, height, fullscreen, maximize, hideTitleBar, alwaysOnTop, appVersion, darkMode, disabledWebShortcuts, activationShortcut, userAgent, showSystemTray, systemTrayIcon, useLocalFile, identifier, name = 'pake-app', resizable = true, inject, proxyUrl, installerLanguage, hideOnClose, incognito, title, wasm, enableDragDrop, multiInstance, multiWindow, startToTray, forceInternalNavigation, internalUrlRegex, zoom, minWidth, minHeight, ignoreCertificateErrors, newWindow, camera, microphone, } = options;
495499
const { platform } = process;
496500
const platformHideOnClose = hideOnClose ?? platform === 'darwin';
497501
const tauriConfWindowOptions = {
@@ -746,6 +750,26 @@ Terminal=false
746750
},
747751
};
748752
}
753+
// Write entitlements dynamically on macOS so camera/microphone are opt-in
754+
if (platform === 'darwin') {
755+
const entitlementEntries = [];
756+
if (camera) {
757+
entitlementEntries.push(' <key>com.apple.security.device.camera</key>\n <true/>');
758+
}
759+
if (microphone) {
760+
entitlementEntries.push(' <key>com.apple.security.device.audio-input</key>\n <true/>');
761+
}
762+
const entitlementsContent = `<?xml version="1.0" encoding="UTF-8"?>
763+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
764+
<plist version="1.0">
765+
<dict>
766+
${entitlementEntries.join('\n')}
767+
</dict>
768+
</plist>
769+
`;
770+
const entitlementsPath = path.join(npmDirectory, 'src-tauri', 'entitlements.plist');
771+
await fsExtra.writeFile(entitlementsPath, entitlementsContent);
772+
}
749773
// Save config file.
750774
const platformConfigPaths = {
751775
win32: 'tauri.windows.conf.json',
@@ -978,17 +1002,14 @@ class BaseBuilder {
9781002
logger.info(`- Installing ${appName} to /Applications...`);
9791003
const appBundleName = path.basename(appBundlePath);
9801004
const appDest = path.join('/Applications', appBundleName);
981-
if (await fsExtra.pathExists(appDest)) {
982-
await fsExtra.remove(appDest);
983-
}
984-
await fsExtra.copy(appBundlePath, appDest);
985-
await fsExtra.remove(appBundlePath);
1005+
// fsExtra.move uses fs.rename (atomic on same filesystem) and falls back
1006+
// to copy+remove only when moving across volumes.
1007+
await fsExtra.move(appBundlePath, appDest, { overwrite: true });
9861008
logger.success(`✔ ${appBundleName.replace(/\.app$/, '')} installed to /Applications`);
987-
logger.success('✔ Local app bundle removed');
9881009
}
9891010
catch (error) {
9901011
logger.error(`✕ Failed to install ${appName}: ${error}`);
991-
logger.info(` The app bundle is still available at: ${appBundlePath}`);
1012+
logger.info(` App bundle still available at: ${appBundlePath}`);
9921013
}
9931014
}
9941015
getFileType(target) {
@@ -2009,7 +2030,6 @@ async function handleOptions(options, url) {
20092030
if (name && platform === 'linux') {
20102031
name = generateLinuxPackageName(name);
20112032
}
2012-
const resolvedName = name || 'pake-app';
20132033
if (name && !isValidName(name, platform)) {
20142034
const LINUX_NAME_ERROR = `✕ Name should only include lowercase letters, numbers, and dashes (not leading dashes). Examples: com-123-xxx, 123pan, pan123, weread, we-read, 123.`;
20152035
const DEFAULT_NAME_ERROR = `✕ Name should only include letters, numbers, dashes, and spaces (not leading dashes and spaces). Examples: 123pan, 123Pan, Pan123, weread, WeRead, WERead, we-read, We Read, 123.`;
@@ -2023,10 +2043,11 @@ async function handleOptions(options, url) {
20232043
process.exit(1);
20242044
}
20252045
}
2046+
const resolvedName = name || 'pake-app';
20262047
const appOptions = {
20272048
...options,
20282049
name: resolvedName,
2029-
identifier: resolveIdentifier(url, resolvedName, options.identifier),
2050+
identifier: resolveIdentifier(url, options.name, options.identifier),
20302051
};
20312052
const iconPath = await handleIcon(appOptions, url);
20322053
appOptions.icon = iconPath || '';
@@ -2083,6 +2104,8 @@ const DEFAULT_PAKE_OPTIONS = {
20832104
ignoreCertificateErrors: false,
20842105
newWindow: false,
20852106
install: false,
2107+
camera: false,
2108+
microphone: false,
20862109
};
20872110

20882111
function validateNumberInput(value) {
@@ -2122,8 +2145,7 @@ ${green('|_| \\__,_|_|\\_\\___| can turn any webpage into a desktop app with
21222145
.showHelpAfterError()
21232146
.argument('[url]', 'The web URL you want to package', validateUrlInput)
21242147
.option('--name <string>', 'Application name')
2125-
.addOption(new Option('--identifier <string>', 'Application identifier / bundle ID')
2126-
.hideHelp())
2148+
.addOption(new Option('--identifier <string>', 'Application identifier / bundle ID').hideHelp())
21272149
.option('--icon <string>', 'Application icon', DEFAULT_PAKE_OPTIONS.icon)
21282150
.option('--width <number>', 'Window width', validateNumberInput, DEFAULT_PAKE_OPTIONS.width)
21292151
.option('--height <number>', 'Window height', validateNumberInput, DEFAULT_PAKE_OPTIONS.height)
@@ -2245,6 +2267,12 @@ ${green('|_| \\__,_|_|\\_\\___| can turn any webpage into a desktop app with
22452267
.default(DEFAULT_PAKE_OPTIONS.newWindow)
22462268
.hideHelp())
22472269
.option('--install', 'Auto-install app to /Applications (macOS) after build and remove local bundle', DEFAULT_PAKE_OPTIONS.install)
2270+
.addOption(new Option('--camera', 'Request camera permission on macOS')
2271+
.default(DEFAULT_PAKE_OPTIONS.camera)
2272+
.hideHelp())
2273+
.addOption(new Option('--microphone', 'Request microphone permission on macOS')
2274+
.default(DEFAULT_PAKE_OPTIONS.microphone)
2275+
.hideHelp())
22482276
.version(packageJson.version, '-v, --version')
22492277
.configureHelp({
22502278
sortSubcommands: true,

docs/advanced-usage.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,20 @@ pake ./my-app/index.html --name my-static-app --use-local-file
124124

125125
Requirements: Pake CLI >= 3.0.0
126126

127+
## macOS Media Permissions
128+
129+
By default, apps built with Pake do not request camera or microphone access. For sites that require these (for example, video conferencing or voice input), pass the relevant flags at build time:
130+
131+
```bash
132+
pake https://chatgpt.com --name ChatGPT --microphone
133+
pake https://meet.google.com --name GoogleMeet --camera --microphone
134+
```
135+
136+
- `--microphone` — grants microphone access (`com.apple.security.device.audio-input`)
137+
- `--camera` — grants camera access (`com.apple.security.device.camera`)
138+
139+
macOS will prompt the user for permission on first use. Only add these flags for sites that actually need them.
140+
127141
## Multiple Apps For The Same Site
128142

129143
If you need separate apps for the same site, for example two Gmail accounts with different login state, build them with different app names:

docs/advanced-usage_CN.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,20 @@ pake ./my-app/index.html --name my-static-app --use-local-file
124124

125125
要求:Pake CLI >= 3.0.0
126126

127+
## macOS 摄像头与麦克风权限
128+
129+
Pake 构建的应用默认不申请摄像头或麦克风权限。对于需要这些权限的站点(例如视频会议或语音输入),在构建时传入对应的标志:
130+
131+
```bash
132+
pake https://chatgpt.com --name ChatGPT --microphone
133+
pake https://meet.google.com --name GoogleMeet --camera --microphone
134+
```
135+
136+
- `--microphone` — 申请麦克风权限(`com.apple.security.device.audio-input`
137+
- `--camera` — 申请摄像头权限(`com.apple.security.device.camera`
138+
139+
macOS 会在首次使用时向用户弹出权限确认对话框。请仅在确实需要的站点上添加这些标志。
140+
127141
## 同一站点生成多个独立应用
128142

129143
如果你需要为同一个站点生成多个彼此独立的应用,例如两个不同登录态的 Gmail,可以直接使用不同的应用名称进行构建:

0 commit comments

Comments
 (0)