pake-cli 3.11.9 โ†’ 3.12.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.
@@ -0,0 +1,20 @@
1
+ Pake Output Exception
2
+
3
+ This is an exception to the GNU General Public License version 3 (GPLv3),
4
+ under which Pake is licensed (see the LICENSE file).
5
+
6
+ As an additional permission under section 7 of the GPLv3:
7
+
8
+ When you use Pake, or a build of Pake produced by the standard Pake build
9
+ and packaging process, to generate a target application ("Pake Output"),
10
+ the portions of Pake incorporated into that Pake Output do not by
11
+ themselves cause the Pake Output as a whole to be or become subject to the
12
+ GPLv3. You may distribute such Pake Output under license terms of your own
13
+ choosing, including proprietary terms.
14
+
15
+ This permission does NOT apply to Pake itself or to any modified version of
16
+ Pake's own source code: anyone who distributes Pake, or a work derived from
17
+ Pake's source code beyond the configuration and packaging performed by the
18
+ standard build process, remains fully bound by the GPLv3.
19
+
20
+ Copyright (c) 2024 Tw93 and the Pake contributors.
package/README.md CHANGED
@@ -202,12 +202,17 @@ Pake's development can not be without these Hackers. They contributed a lot of c
202
202
 
203
203
  ## Support
204
204
 
205
+ - The most direct way to support me is getting [Mole for Mac](https://mole.fit), my paid Mac cleanup app.
205
206
  - If Pake helped you, [share it](https://twitter.com/intent/tweet?url=https://github.com/tw93/Pake&text=Pake%20-%20Turn%20any%20webpage%20into%20a%20desktop%20app%20with%20one%20command.%20Nearly%2020x%20smaller%20than%20Electron%20packages,%20supports%20macOS%20Windows%20Linux) with friends or give it a star.
206
207
  - Got ideas or bugs? Open an issue or PR, feel free to contribute your best AI model.
207
208
  - I have two cats, TangYuan and Coke. If you think Pake delights your life, you can feed them <a href="https://cats.tw93.fun?name=Pake" target="_blank">canned food ๐Ÿฅฉ</a>.
208
209
 
210
+ <details>
211
+ <summary>These lovely people already did ๐Ÿฑ</summary>
212
+ <br/>
209
213
  <a href="https://cats.tw93.fun?name=Pake"><img src="https://cdn.jsdelivr.net/gh/tw93/sponsors@main/assets/sponsors.svg" width="1000" loading="lazy" /></a>
214
+ </details>
210
215
 
211
216
  ## License
212
217
 
213
- Pake is open source under GPL-3.0, see [LICENSE](./LICENSE); apps you build with Pake are entirely yours to use and distribute. If you fork Pake into your own product, to avoid confusion please give it a different name and credit Pake as the source.
218
+ Pake is open source under GPL-3.0, see [LICENSE](./LICENSE) and [Pake Output Exception](./LICENSE-EXCEPTION); apps you build with Pake are entirely yours to use and distribute. If you fork Pake into your own product, to avoid confusion please give it a different name and credit Pake as the source.
package/dist/cli.js CHANGED
@@ -20,7 +20,7 @@ import { InvalidArgumentError, program as program$1, Option } from 'commander';
20
20
  import fs$1 from 'fs';
21
21
 
22
22
  var name = "pake-cli";
23
- var version = "3.11.9";
23
+ var version = "3.12.0";
24
24
  var description = "๐Ÿคฑ๐Ÿป Turn any webpage into a desktop app with one command. ๐Ÿคฑ๐Ÿป ไธ€้”ฎๆ‰“ๅŒ…็ฝ‘้กต็”Ÿๆˆ่ฝป้‡ๆกŒ้ขๅบ”็”จใ€‚";
25
25
  var engines = {
26
26
  node: ">=18.0.0"
@@ -46,6 +46,7 @@ var keywords = [
46
46
  "productivity"
47
47
  ];
48
48
  var files = [
49
+ "LICENSE-EXCEPTION",
49
50
  "dist",
50
51
  "src-tauri"
51
52
  ];
@@ -417,6 +418,9 @@ function filterLinuxTargets(targets) {
417
418
  const requested = targets.split(',').map((target) => target.trim());
418
419
  return LINUX_TARGET_TYPES.filter((target) => requested.includes(target));
419
420
  }
421
+ function needsTemporaryDebForZst(targets) {
422
+ return targets.includes('zst') && !targets.includes('deb');
423
+ }
420
424
 
421
425
  /**
422
426
  * Pure transform from CLI options to the window-config slice that gets
@@ -427,13 +431,14 @@ function filterLinuxTargets(targets) {
427
431
  */
428
432
  function buildWindowConfigOverrides(options, platform = asSupportedPlatform(process.platform)) {
429
433
  const platformHideOnClose = options.hideOnClose ?? platform === 'darwin';
434
+ const platformHideTitleBar = platform === 'darwin' ? options.hideTitleBar : false;
430
435
  return {
431
436
  width: options.width,
432
437
  height: options.height,
433
438
  fullscreen: options.fullscreen,
434
439
  maximize: options.maximize,
435
440
  resizable: options.resizable ?? true,
436
- hide_title_bar: options.hideTitleBar,
441
+ hide_title_bar: platformHideTitleBar,
437
442
  activation_shortcut: options.activationShortcut,
438
443
  always_on_top: options.alwaysOnTop,
439
444
  dark_mode: options.darkMode,
@@ -708,6 +713,9 @@ async function mergeConfig(url, options, tauriConf) {
708
713
  await copyTemplateConfigs();
709
714
  const { appVersion, userAgent, showSystemTray, useLocalFile, identifier, name = 'pake-app', installerLanguage, wasm, camera, microphone, } = options;
710
715
  const platform = asSupportedPlatform(process.platform);
716
+ if (options.hideTitleBar && platform !== 'darwin') {
717
+ logger.warn('โœผ --hide-title-bar is only supported on macOS and will be ignored on this platform.');
718
+ }
711
719
  const tauriConfWindowOptions = buildWindowConfigOverrides(options, platform);
712
720
  Object.assign(tauriConf.pake.windows[0], { url, ...tauriConfWindowOptions });
713
721
  tauriConf.productName = name;
@@ -1158,14 +1166,22 @@ class BaseBuilder {
1158
1166
  return 0; // Disable proxy feature if version detection fails
1159
1167
  }
1160
1168
  }
1169
+ getCargoTargetDir() {
1170
+ return process.env.CARGO_TARGET_DIR || path.join('src-tauri', 'target');
1171
+ }
1172
+ resolveBuildPath(npmDirectory, buildPath) {
1173
+ return path.isAbsolute(buildPath)
1174
+ ? buildPath
1175
+ : path.join(npmDirectory, buildPath);
1176
+ }
1161
1177
  getBasePath() {
1162
1178
  const basePath = this.options.debug ? 'debug' : 'release';
1163
- return `src-tauri/target/${basePath}/bundle/`;
1179
+ return path.join(this.getCargoTargetDir(), basePath, 'bundle');
1164
1180
  }
1165
1181
  getBuildAppPath(npmDirectory, fileName, fileType) {
1166
1182
  // For app bundles on macOS, the directory is 'macos', not 'app'
1167
1183
  const bundleDir = fileType.toLowerCase() === 'app' ? 'macos' : fileType.toLowerCase();
1168
- return path.join(npmDirectory, this.getBasePath(), bundleDir, `${fileName}.${fileType}`);
1184
+ return path.join(this.resolveBuildPath(npmDirectory, this.getBasePath()), bundleDir, `${fileName}.${fileType}`);
1169
1185
  }
1170
1186
  /**
1171
1187
  * Copy raw binary file to output directory
@@ -1192,9 +1208,9 @@ class BaseBuilder {
1192
1208
  const binaryName = this.getBinaryName(appName);
1193
1209
  // Handle cross-platform builds
1194
1210
  if (this.options.multiArch || this.hasArchSpecificTarget()) {
1195
- return path.join(npmDirectory, this.getArchSpecificPath(), basePath, binaryName);
1211
+ return path.join(this.resolveBuildPath(npmDirectory, this.getArchSpecificPath()), basePath, binaryName);
1196
1212
  }
1197
- return path.join(npmDirectory, 'src-tauri/target', basePath, binaryName);
1213
+ return path.join(this.resolveBuildPath(npmDirectory, this.getCargoTargetDir()), basePath, binaryName);
1198
1214
  }
1199
1215
  /**
1200
1216
  * Get the output path for the raw binary file
@@ -1225,7 +1241,7 @@ class BaseBuilder {
1225
1241
  * Get architecture-specific path for binary
1226
1242
  */
1227
1243
  getArchSpecificPath() {
1228
- return 'src-tauri/target'; // Override in subclasses if needed
1244
+ return this.getCargoTargetDir(); // Override in subclasses if needed
1229
1245
  }
1230
1246
  }
1231
1247
  BaseBuilder.ARCH_MAPPINGS = {
@@ -1311,7 +1327,10 @@ class MacBuilder extends BaseBuilder {
1311
1327
  const basePath = this.options.debug ? 'debug' : 'release';
1312
1328
  const actualArch = this.getActualArch();
1313
1329
  const target = this.getTauriTarget(actualArch, 'darwin');
1314
- return `src-tauri/target/${target}/${basePath}/bundle`;
1330
+ if (!target) {
1331
+ throw new Error(`Unsupported architecture: ${actualArch} for macOS`);
1332
+ }
1333
+ return path.join(this.getCargoTargetDir(), target, basePath, 'bundle');
1315
1334
  }
1316
1335
  hasArchSpecificTarget() {
1317
1336
  return true;
@@ -1319,7 +1338,10 @@ class MacBuilder extends BaseBuilder {
1319
1338
  getArchSpecificPath() {
1320
1339
  const actualArch = this.getActualArch();
1321
1340
  const target = this.getTauriTarget(actualArch, 'darwin');
1322
- return `src-tauri/target/${target}`;
1341
+ if (!target) {
1342
+ throw new Error(`Unsupported architecture: ${actualArch} for macOS`);
1343
+ }
1344
+ return path.join(this.getCargoTargetDir(), target);
1323
1345
  }
1324
1346
  }
1325
1347
 
@@ -1350,14 +1372,26 @@ class WinBuilder extends BaseBuilder {
1350
1372
  getBasePath() {
1351
1373
  const basePath = this.options.debug ? 'debug' : 'release';
1352
1374
  const target = this.getTauriTarget(this.buildArch, 'win32');
1353
- return `src-tauri/target/${target}/${basePath}/bundle/`;
1375
+ if (!target) {
1376
+ throw new Error(`Unsupported architecture: ${this.buildArch} for Windows`);
1377
+ }
1378
+ return path.join(this.getCargoTargetDir(), target, basePath, 'bundle');
1354
1379
  }
1355
1380
  hasArchSpecificTarget() {
1356
1381
  return true;
1357
1382
  }
1358
1383
  getArchSpecificPath() {
1359
1384
  const target = this.getTauriTarget(this.buildArch, 'win32');
1360
- return `src-tauri/target/${target}`;
1385
+ if (!target) {
1386
+ throw new Error(`Unsupported architecture: ${this.buildArch} for Windows`);
1387
+ }
1388
+ return path.join(this.getCargoTargetDir(), target);
1389
+ }
1390
+ getRawBinaryPath(appName) {
1391
+ return `${appName}.exe`;
1392
+ }
1393
+ getBinaryName(appName) {
1394
+ return `pake-${generateIdentifierSafeName(appName)}.exe`;
1361
1395
  }
1362
1396
  }
1363
1397
 
@@ -1407,11 +1441,16 @@ class LinuxBuilder extends BaseBuilder {
1407
1441
  if (targets.length === 0) {
1408
1442
  throw new Error(`No valid Linux target in "${this.options.targets}". Valid targets: ${LINUX_TARGET_TYPES.join(', ')}.`);
1409
1443
  }
1444
+ const useTemporaryDebForZst = needsTemporaryDebForZst(targets);
1410
1445
  for (const target of targets) {
1411
1446
  this.currentBuildType = target;
1412
1447
  if (target === 'zst') {
1413
- await this.buildAndCopy(url, 'deb', false);
1414
- await this.createArchPackageFromDeb();
1448
+ if (useTemporaryDebForZst) {
1449
+ await this.buildAndCopy(url, 'deb', false);
1450
+ }
1451
+ await this.createArchPackageFromDeb({
1452
+ removeSourceDeb: useTemporaryDebForZst,
1453
+ });
1415
1454
  }
1416
1455
  else {
1417
1456
  await this.buildAndCopy(url, target);
@@ -1432,7 +1471,7 @@ class LinuxBuilder extends BaseBuilder {
1432
1471
  }
1433
1472
  }
1434
1473
  }
1435
- async createArchPackageFromDeb() {
1474
+ async createArchPackageFromDeb({ removeSourceDeb, }) {
1436
1475
  const { name = 'pake-app' } = this.options;
1437
1476
  const packageName = generateLinuxPackageName(name);
1438
1477
  const version = tauriConfig.version;
@@ -1493,11 +1532,13 @@ post_remove() {
1493
1532
  }
1494
1533
  `);
1495
1534
  await shellExec(`bsdtar --zstd -cf "${packagePath}" -C "${dataDir}" .PKGINFO .INSTALL usr`);
1496
- await fsExtra.remove(debPath);
1497
1535
  logger.success('โœ” Build success!');
1498
1536
  logger.success('โœ” App installer located in', packagePath);
1499
1537
  }
1500
1538
  finally {
1539
+ if (removeSourceDeb) {
1540
+ await fsExtra.remove(debPath);
1541
+ }
1501
1542
  await fsExtra.remove(workDir);
1502
1543
  }
1503
1544
  }
@@ -1545,7 +1586,10 @@ post_remove() {
1545
1586
  const basePath = this.options.debug ? 'debug' : 'release';
1546
1587
  if (this.buildArch === 'arm64') {
1547
1588
  const target = this.getTauriTarget(this.buildArch, 'linux');
1548
- return `src-tauri/target/${target}/${basePath}/bundle/`;
1589
+ if (!target) {
1590
+ throw new Error(`Unsupported architecture: ${this.buildArch} for Linux`);
1591
+ }
1592
+ return path.join(this.getCargoTargetDir(), target, basePath, 'bundle');
1549
1593
  }
1550
1594
  return super.getBasePath();
1551
1595
  }
@@ -1561,7 +1605,10 @@ post_remove() {
1561
1605
  getArchSpecificPath() {
1562
1606
  if (this.buildArch === 'arm64') {
1563
1607
  const target = this.getTauriTarget(this.buildArch, 'linux');
1564
- return `src-tauri/target/${target}`;
1608
+ if (!target) {
1609
+ throw new Error(`Unsupported architecture: ${this.buildArch} for Linux`);
1610
+ }
1611
+ return path.join(this.getCargoTargetDir(), target);
1565
1612
  }
1566
1613
  return super.getArchSpecificPath();
1567
1614
  }
@@ -2399,6 +2446,20 @@ function normalizeUrl(urlToNormalize) {
2399
2446
  throw new Error(`Your url "${urlWithProtocol}" is invalid: ${err.message}`);
2400
2447
  }
2401
2448
  }
2449
+ // Compiles a comma-separated domain list into a regex source for
2450
+ // internal_url_regex. Each domain is escaped and matched against the URL host
2451
+ // and its subdomains so path or query text cannot accidentally opt a link in.
2452
+ // Returns '' for empty input.
2453
+ function safeDomainsToRegex(domains) {
2454
+ const escaped = domains
2455
+ .split(',')
2456
+ .map((domain) => domain.trim().toLowerCase())
2457
+ .filter(Boolean)
2458
+ .map((domain) => domain.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
2459
+ return escaped.length
2460
+ ? `^https?:\\/\\/(?:[^/?#@]+\\.)*(?:${escaped.join('|')})(?::\\d+)?(?:[/?#]|$)`
2461
+ : '';
2462
+ }
2402
2463
 
2403
2464
  /**
2404
2465
  * Error class used for user-facing CLI errors.
@@ -2479,6 +2540,10 @@ async function handleOptions(options, url) {
2479
2540
  name: resolvedName,
2480
2541
  identifier: resolveIdentifier(url, options.name, options.identifier),
2481
2542
  };
2543
+ // --safe-domain is sugar over --internal-url-regex; an explicit regex wins.
2544
+ if (!options.internalUrlRegex && options.safeDomain) {
2545
+ appOptions.internalUrlRegex = safeDomainsToRegex(options.safeDomain);
2546
+ }
2482
2547
  const iconPath = await handleIcon(appOptions, url);
2483
2548
  appOptions.icon = iconPath || '';
2484
2549
  return appOptions;
@@ -2527,6 +2592,7 @@ const DEFAULT_PAKE_OPTIONS = {
2527
2592
  startToTray: false,
2528
2593
  forceInternalNavigation: false,
2529
2594
  internalUrlRegex: '',
2595
+ safeDomain: '',
2530
2596
  enableFind: false,
2531
2597
  iterativeBuild: false,
2532
2598
  zoom: 100,
@@ -2540,6 +2606,9 @@ const DEFAULT_PAKE_OPTIONS = {
2540
2606
  };
2541
2607
 
2542
2608
  function validateNumberInput(value) {
2609
+ if (value.trim() === '') {
2610
+ throw new InvalidArgumentError('Not a number.');
2611
+ }
2543
2612
  const parsedValue = Number(value);
2544
2613
  if (!Number.isFinite(parsedValue)) {
2545
2614
  throw new InvalidArgumentError('Not a number.');
@@ -2576,6 +2645,7 @@ ${green('|_| \\__,_|_|\\_\\___| can turn any webpage into a desktop app with
2576
2645
  return program$1
2577
2646
  .addHelpText('beforeAll', logo)
2578
2647
  .usage(`[url] [options]`)
2648
+ .helpOption('-h, --help', 'Show all CLI options')
2579
2649
  .showHelpAfterError()
2580
2650
  .argument('[url]', 'The web URL you want to package', validateUrlInput)
2581
2651
  .option('--name <string>', 'Application name')
@@ -2664,12 +2734,9 @@ ${green('|_| \\__,_|_|\\_\\___| can turn any webpage into a desktop app with
2664
2734
  .addOption(new Option('--start-to-tray', 'Start app minimized to tray')
2665
2735
  .default(DEFAULT_PAKE_OPTIONS.startToTray)
2666
2736
  .hideHelp())
2667
- .addOption(new Option('--force-internal-navigation', 'Keep every link inside the Pake window instead of opening external handlers')
2668
- .default(DEFAULT_PAKE_OPTIONS.forceInternalNavigation)
2669
- .hideHelp())
2670
- .addOption(new Option('--internal-url-regex <string>', 'Regex pattern to match URLs that should be considered internal')
2671
- .default(DEFAULT_PAKE_OPTIONS.internalUrlRegex)
2672
- .hideHelp())
2737
+ .addOption(new Option('--force-internal-navigation', 'Keep every link inside the Pake window instead of opening external handlers').default(DEFAULT_PAKE_OPTIONS.forceInternalNavigation))
2738
+ .addOption(new Option('--internal-url-regex <string>', 'Regex pattern to match URLs that should be considered internal').default(DEFAULT_PAKE_OPTIONS.internalUrlRegex))
2739
+ .addOption(new Option('--safe-domain <domains>', 'Comma-separated domains kept inside the app (e.g. SSO/workspace callbacks)').default(DEFAULT_PAKE_OPTIONS.safeDomain))
2673
2740
  .addOption(new Option('--enable-find', 'Enable in-page Find UI with Cmd/Ctrl+F/G shortcuts')
2674
2741
  .default(DEFAULT_PAKE_OPTIONS.enableFind)
2675
2742
  .hideHelp())
@@ -2700,9 +2767,7 @@ ${green('|_| \\__,_|_|\\_\\___| can turn any webpage into a desktop app with
2700
2767
  .addOption(new Option('--iterative-build', 'Turn on rapid build mode (app only, no dmg/deb/msi), good for debugging')
2701
2768
  .default(DEFAULT_PAKE_OPTIONS.iterativeBuild)
2702
2769
  .hideHelp())
2703
- .addOption(new Option('--new-window', 'Allow sites to open new windows (for auth flows, tabs, branches)')
2704
- .default(DEFAULT_PAKE_OPTIONS.newWindow)
2705
- .hideHelp())
2770
+ .addOption(new Option('--new-window', 'Allow sites to open new windows (for auth flows, tabs, branches)').default(DEFAULT_PAKE_OPTIONS.newWindow))
2706
2771
  .addOption(new Option('--install', 'Auto-install app to /Applications (macOS) after build and remove local bundle')
2707
2772
  .default(DEFAULT_PAKE_OPTIONS.install)
2708
2773
  .hideHelp())
@@ -2715,14 +2780,19 @@ ${green('|_| \\__,_|_|\\_\\___| can turn any webpage into a desktop app with
2715
2780
  .version(packageJson.version, '-v, --version')
2716
2781
  .configureHelp({
2717
2782
  sortSubcommands: true,
2783
+ visibleOptions: (command) => {
2784
+ const options = [...command.options];
2785
+ const helpOption = command
2786
+ ._helpOption;
2787
+ if (helpOption) {
2788
+ options.push(helpOption);
2789
+ }
2790
+ return options;
2791
+ },
2718
2792
  optionTerm: (option) => {
2719
- if (option.flags === '-v, --version' || option.flags === '-h, --help')
2720
- return '';
2721
2793
  return option.flags;
2722
2794
  },
2723
2795
  optionDescription: (option) => {
2724
- if (option.flags === '-v, --version' || option.flags === '-h, --help')
2725
- return '';
2726
2796
  return option.description;
2727
2797
  },
2728
2798
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pake-cli",
3
- "version": "3.11.9",
3
+ "version": "3.12.0",
4
4
  "description": "๐Ÿคฑ๐Ÿป Turn any webpage into a desktop app with one command. ๐Ÿคฑ๐Ÿป ไธ€้”ฎๆ‰“ๅŒ…็ฝ‘้กต็”Ÿๆˆ่ฝป้‡ๆกŒ้ขๅบ”็”จใ€‚",
5
5
  "engines": {
6
6
  "node": ">=18.0.0"
@@ -26,6 +26,7 @@
26
26
  "productivity"
27
27
  ],
28
28
  "files": [
29
+ "LICENSE-EXCEPTION",
29
30
  "dist",
30
31
  "src-tauri"
31
32
  ],
@@ -2564,7 +2564,7 @@ dependencies = [
2564
2564
 
2565
2565
  [[package]]
2566
2566
  name = "pake"
2567
- version = "3.11.9"
2567
+ version = "3.12.0"
2568
2568
  dependencies = [
2569
2569
  "objc2",
2570
2570
  "objc2-app-kit",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "pake"
3
- version = "3.11.9"
3
+ version = "3.12.0"
4
4
  description = "๐Ÿคฑ๐Ÿป Turn any webpage into a desktop app with Rust."
5
5
  authors = ["Tw93"]
6
6
  license = "GPL-3.0-or-later"
@@ -27,12 +27,26 @@ function matchesAuthUrl(url, baseUrl = window.location.href) {
27
27
  /\/o\/oauth2/,
28
28
  ];
29
29
 
30
- const isMatch = oauthPatterns.some(
31
- (pattern) =>
32
- pattern.test(hostname) ||
33
- pattern.test(pathname) ||
34
- pattern.test(fullUrl),
35
- );
30
+ // Enterprise SSO. Match identity providers on the host, and SAML/SSO/ADFS on
31
+ // the pathname with endpoint-shaped patterns only, so ordinary pages such as
32
+ // /settings/sso/providers (or a query string carrying an SSO URL) are not
33
+ // misread as authentication.
34
+ const enterpriseHostPatterns = [/(^|\.)okta\.com$/, /(^|\.)onelogin\.com$/];
35
+ const enterprisePathPatterns = [
36
+ /\/saml2?\/(sso|acs|login|metadata|consume|redirect|callback|continue)/,
37
+ /\/sso\/(saml|oidc|oauth|login|authorize|redirect|callback|acs|start|continue|metadata)/,
38
+ /\/adfs\/ls\b/,
39
+ ];
40
+
41
+ const isMatch =
42
+ oauthPatterns.some(
43
+ (pattern) =>
44
+ pattern.test(hostname) ||
45
+ pattern.test(pathname) ||
46
+ pattern.test(fullUrl),
47
+ ) ||
48
+ enterpriseHostPatterns.some((pattern) => pattern.test(hostname)) ||
49
+ enterprisePathPatterns.some((pattern) => pattern.test(pathname));
36
50
 
37
51
  if (isMatch) {
38
52
  console.log("[Pake] OAuth URL detected:", url);
@@ -272,6 +272,33 @@ function shouldBypassPakeLinkHandling(rawHref) {
272
272
  );
273
273
  }
274
274
 
275
+ function shouldNavigateAuthInCurrentWindow() {
276
+ return /macintosh|mac os x/i.test(navigator.userAgent);
277
+ }
278
+
279
+ function canNavigateAuthUrl(url) {
280
+ const normalizedUrl = normalizeAnchorHref(url).toLowerCase();
281
+ return normalizedUrl !== "" && normalizedUrl !== "about:blank";
282
+ }
283
+
284
+ function navigateInCurrentWindow(url) {
285
+ window.location.href = url;
286
+ return window;
287
+ }
288
+
289
+ function openAuthNavigation(originalWindowOpen, url, name, specs) {
290
+ if (shouldNavigateAuthInCurrentWindow() && canNavigateAuthUrl(url)) {
291
+ return navigateInCurrentWindow(url);
292
+ }
293
+
294
+ const authWindow = originalWindowOpen.call(window, url, name, specs);
295
+ if (!authWindow) {
296
+ return navigateInCurrentWindow(url);
297
+ }
298
+
299
+ return authWindow;
300
+ }
301
+
275
302
  document.addEventListener("DOMContentLoaded", () => {
276
303
  const tauri = window.__TAURI__;
277
304
  const appWindow = tauri.window.getCurrentWindow();
@@ -432,24 +459,23 @@ document.addEventListener("DOMContentLoaded", () => {
432
459
  const absoluteUrl = hrefUrl.href;
433
460
  let filename = anchorElement.download || getFilenameFromUrl(absoluteUrl);
434
461
 
435
- // Keep OAuth/authentication flows inside the app when popup support is enabled.
462
+ // Keep OAuth/authentication flows inside the app. Without --new-window,
463
+ // navigate in place so the SSO redirect chain and callback stay in the
464
+ // webview instead of falling through to the system browser.
436
465
  if (window.isAuthLink(absoluteUrl)) {
437
466
  console.log("[Pake] Handling OAuth navigation in-app:", absoluteUrl);
467
+ e.preventDefault();
468
+ e.stopImmediatePropagation();
438
469
 
439
470
  if (window.pakeConfig?.new_window) {
440
- e.preventDefault();
441
- e.stopImmediatePropagation();
442
-
443
- const authWindow = originalWindowOpen.call(
444
- window,
471
+ openAuthNavigation(
472
+ originalWindowOpen,
445
473
  absoluteUrl,
446
474
  "_blank",
447
475
  "width=1200,height=800,scrollbars=yes,resizable=yes",
448
476
  );
449
-
450
- if (!authWindow) {
451
- window.location.href = absoluteUrl;
452
- }
477
+ } else {
478
+ window.location.href = absoluteUrl;
453
479
  }
454
480
 
455
481
  return;
@@ -465,7 +491,15 @@ document.addEventListener("DOMContentLoaded", () => {
465
491
  }
466
492
 
467
493
  if (isInternalUrl(absoluteUrl)) {
468
- // For internal links (based on regex or domain), let the browser handle it naturally
494
+ // With --new-window the Rust on_new_window handler opens an in-app
495
+ // window; without it, deferring to the native handler sends the
496
+ // _blank target to the system browser and strands SSO callbacks.
497
+ // Navigate in place so internal links stay inside the webview.
498
+ if (!window.pakeConfig?.new_window) {
499
+ e.preventDefault();
500
+ e.stopImmediatePropagation();
501
+ window.location.href = absoluteUrl;
502
+ }
469
503
  return;
470
504
  }
471
505
 
@@ -544,9 +578,15 @@ document.addEventListener("DOMContentLoaded", () => {
544
578
  return originalWindowOpen.call(window, url, name, specs);
545
579
  }
546
580
 
547
- // Allow authentication popups to open normally
581
+ // Avoid macOS WebKit auth-popup crashes by navigating auth URLs in-place.
548
582
  if (window.isAuthPopup(url, name)) {
549
- return originalWindowOpen.call(window, url, name, specs);
583
+ try {
584
+ const baseUrl = window.location.origin + window.location.pathname;
585
+ const absoluteUrl = new URL(url, baseUrl).href;
586
+ return openAuthNavigation(originalWindowOpen, absoluteUrl, name, specs);
587
+ } catch (error) {
588
+ return openAuthNavigation(originalWindowOpen, url, name, specs);
589
+ }
550
590
  }
551
591
 
552
592
  try {
@@ -563,6 +603,14 @@ document.addEventListener("DOMContentLoaded", () => {
563
603
  return null;
564
604
  }
565
605
 
606
+ // With --new-window the native handler opens an in-app window; without it,
607
+ // originalWindowOpen would route the internal target to the system browser
608
+ // and strand SSO callbacks, so navigate in place instead.
609
+ if (!window.pakeConfig?.new_window) {
610
+ window.location.href = absoluteUrl;
611
+ return window;
612
+ }
613
+
566
614
  return originalWindowOpen.call(window, absoluteUrl, name, specs);
567
615
  } catch (error) {
568
616
  return originalWindowOpen.call(window, url, name, specs);
@@ -10,6 +10,12 @@ use tauri_plugin_window_state::StateFlags;
10
10
  use std::time::Duration;
11
11
 
12
12
  const WINDOW_SHOW_DELAY: u64 = 50;
13
+ #[cfg(target_os = "linux")]
14
+ const PAKE_LINUX_WEBKIT_SAFE_MODE: &str = "PAKE_LINUX_WEBKIT_SAFE_MODE";
15
+ #[cfg(target_os = "linux")]
16
+ const WEBKIT_DISABLE_DMABUF_RENDERER: &str = "WEBKIT_DISABLE_DMABUF_RENDERER";
17
+ #[cfg(target_os = "linux")]
18
+ const WEBKIT_DISABLE_COMPOSITING_MODE: &str = "WEBKIT_DISABLE_COMPOSITING_MODE";
13
19
 
14
20
  use app::{
15
21
  invoke::{
@@ -21,16 +27,83 @@ use app::{
21
27
  };
22
28
  use util::get_pake_config;
23
29
 
30
+ #[cfg(any(target_os = "linux", test))]
31
+ fn is_disabled_env_value(value: &str) -> bool {
32
+ matches!(
33
+ value.trim().to_ascii_lowercase().as_str(),
34
+ "0" | "false" | "off" | "no" | "native" | "disabled"
35
+ )
36
+ }
37
+
38
+ #[cfg(any(target_os = "linux", test))]
39
+ fn is_non_empty_env_value(value: Option<&str>) -> bool {
40
+ value.map(|value| !value.trim().is_empty()).unwrap_or(false)
41
+ }
42
+
43
+ #[cfg(any(target_os = "linux", test))]
44
+ fn contains_niri(value: &str) -> bool {
45
+ value
46
+ .split([':', ';', ',', ' '])
47
+ .any(|part| part.eq_ignore_ascii_case("niri"))
48
+ }
49
+
50
+ #[cfg(any(target_os = "linux", test))]
51
+ fn should_enable_linux_webkit_safe_mode_from_values(
52
+ safe_mode: Option<&str>,
53
+ niri_socket: Option<&str>,
54
+ desktop_values: &[Option<&str>],
55
+ ) -> bool {
56
+ if let Some(value) = safe_mode.filter(|value| !value.trim().is_empty()) {
57
+ return !is_disabled_env_value(value);
58
+ }
59
+
60
+ let is_niri_session = is_non_empty_env_value(niri_socket)
61
+ || desktop_values
62
+ .iter()
63
+ .flatten()
64
+ .any(|value| contains_niri(value));
65
+
66
+ !is_niri_session
67
+ }
68
+
69
+ #[cfg(target_os = "linux")]
70
+ fn apply_linux_webkit_runtime_flags() {
71
+ let safe_mode = std::env::var(PAKE_LINUX_WEBKIT_SAFE_MODE).ok();
72
+ if safe_mode.as_deref().is_some_and(is_disabled_env_value) {
73
+ std::env::remove_var(WEBKIT_DISABLE_DMABUF_RENDERER);
74
+ std::env::remove_var(WEBKIT_DISABLE_COMPOSITING_MODE);
75
+ return;
76
+ }
77
+
78
+ let desktop_values = [
79
+ std::env::var("XDG_CURRENT_DESKTOP").ok(),
80
+ std::env::var("XDG_SESSION_DESKTOP").ok(),
81
+ std::env::var("DESKTOP_SESSION").ok(),
82
+ ];
83
+ let desktop_refs = desktop_values
84
+ .iter()
85
+ .map(|value| value.as_deref())
86
+ .collect::<Vec<_>>();
87
+
88
+ if !should_enable_linux_webkit_safe_mode_from_values(
89
+ safe_mode.as_deref(),
90
+ std::env::var("NIRI_SOCKET").ok().as_deref(),
91
+ &desktop_refs,
92
+ ) {
93
+ return;
94
+ }
95
+
96
+ if std::env::var(WEBKIT_DISABLE_DMABUF_RENDERER).is_err() {
97
+ std::env::set_var(WEBKIT_DISABLE_DMABUF_RENDERER, "1");
98
+ }
99
+ if std::env::var(WEBKIT_DISABLE_COMPOSITING_MODE).is_err() {
100
+ std::env::set_var(WEBKIT_DISABLE_COMPOSITING_MODE, "1");
101
+ }
102
+ }
103
+
24
104
  pub fn run_app() {
25
105
  #[cfg(target_os = "linux")]
26
- {
27
- if std::env::var("WEBKIT_DISABLE_DMABUF_RENDERER").is_err() {
28
- std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
29
- }
30
- if std::env::var("WEBKIT_DISABLE_COMPOSITING_MODE").is_err() {
31
- std::env::set_var("WEBKIT_DISABLE_COMPOSITING_MODE", "1");
32
- }
33
- }
106
+ apply_linux_webkit_runtime_flags();
34
107
 
35
108
  let (pake_config, tauri_config) = get_pake_config();
36
109
  let tauri_app = tauri::Builder::default();
@@ -202,3 +275,58 @@ pub fn run_app() {
202
275
  pub fn run() {
203
276
  run_app()
204
277
  }
278
+
279
+ #[cfg(test)]
280
+ mod tests {
281
+ use super::*;
282
+
283
+ #[test]
284
+ fn linux_webkit_safe_mode_stays_on_by_default() {
285
+ assert!(should_enable_linux_webkit_safe_mode_from_values(
286
+ None,
287
+ None,
288
+ &[None, None, None]
289
+ ));
290
+ }
291
+
292
+ #[test]
293
+ fn linux_webkit_safe_mode_is_disabled_for_niri_socket() {
294
+ assert!(!should_enable_linux_webkit_safe_mode_from_values(
295
+ None,
296
+ Some("/run/user/501/niri.sock"),
297
+ &[None, None, None]
298
+ ));
299
+ }
300
+
301
+ #[test]
302
+ fn linux_webkit_safe_mode_is_disabled_for_niri_desktop() {
303
+ assert!(!should_enable_linux_webkit_safe_mode_from_values(
304
+ None,
305
+ None,
306
+ &[Some("niri"), None, None]
307
+ ));
308
+ }
309
+
310
+ #[test]
311
+ fn linux_webkit_safe_mode_can_be_forced_on_for_niri() {
312
+ assert!(should_enable_linux_webkit_safe_mode_from_values(
313
+ Some("1"),
314
+ Some("/run/user/501/niri.sock"),
315
+ &[Some("niri"), None, None]
316
+ ));
317
+ }
318
+
319
+ #[test]
320
+ fn linux_webkit_safe_mode_can_be_disabled_explicitly() {
321
+ for value in ["0", "false", "off", "no", "native", "disabled"] {
322
+ assert!(
323
+ !should_enable_linux_webkit_safe_mode_from_values(
324
+ Some(value),
325
+ None,
326
+ &[None, None, None]
327
+ ),
328
+ "expected {value} to disable safe mode"
329
+ );
330
+ }
331
+ }
332
+ }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "productName": "Weekly",
3
3
  "identifier": "com.pake.weekly",
4
- "version": "3.11.9",
4
+ "version": "3.12.0",
5
5
  "app": {
6
6
  "withGlobalTauri": true,
7
7
  "trayIcon": {