Skip to content

Commit 815c978

Browse files
authored
fix(mas,ios): drop the network.server entitlement with Earth Engine sign-in (#1651)
* fix(mas,ios): drop the network.server entitlement with Earth Engine sign-in App Review rejected GeoLibre Desktop 2.4.0 under guideline 2.4.5: the bundle carried com.apple.security.network.server with no functionality the automated analysis could match to it. The entitlement was real, not stray. Earth Engine sign-in uses Google's OAuth loopback-redirect flow, so earth_engine_oauth.rs binds 127.0.0.1:5173 and accepts the browser's inbound redirect to receive the token. The bind happens lazily inside a Tauri command, which is why nothing static pointed at it. Rather than justify the entitlement in App Review Information, drop the feature from both Apple targets: the module is gated `#[cfg(not(any(feature = "mas", target_os = "ios")))]` and replaced by stub commands that bind nothing, so neither the Mac App Store build nor the iOS app ever opens a listening socket. This was the only inbound listener left in those builds — wait_for_port_free (Jupyter) and spawn_martin_server were already compiled out. Every other target keeps Earth Engine unchanged: Developer ID macOS, Windows, Linux, Android, and the web build (a browser uses Google's popup/redirect flow and binds nothing, so Earth Engine still works in Safari on iOS). - Remove com.apple.security.network.server from the MAS entitlements template. - Fail mas-store.yml if it ever reappears in the signature. - Hide the Processing menu item and command-palette entry behind isEarthEngineAvailable(); authenticateEarthEngine throws early if a restored panel state or an external plugin still reaches it. - Document the rejection and the constraint in docs/mac-app-store.md. * chore: bump the version to 2.4.1 for the App Store resubmission App Store Connect rejects a re-upload at a version already submitted, and 2.4.0 was rejected under guideline 2.4.5 (see the previous commit). Bump every version declaration that tracks the app release. Workers stay at 0.0.0 (deliberately unversioned), and the historical 2.4.0 references in docs/ios.md, docs/mac-app-store.md, ios.yml, publish-embed.yml, and mas-store.yml are left alone — they describe the rejected submission. * chore: sync CITATION.cff with the 2.4.1 version bump CI's "Validate CITATION.cff" job asserts the cff version matches package.json, so the bump left it failing. * Address Claude review feedback - tests/earth-engine-availability.test.ts: correct the "plain Node" comment. It claimed both defaults resolve to false because there is no `navigator`, but Node exposes a global `navigator` (userAgent "Node.js/<major>"). The assertion was always right — that UA matches none of the Apple patterns, and maxTouchPoints is undefined so the `> 1` check is false — only the stated reasoning was wrong. * Address Claude review feedback - maplibre-geoagent.ts: gate the GeoAgent Earth Engine overlay behind isEarthEngineAvailable(). enhanceEarthEngineSignIn injected a "Sign in" button unconditionally, so on the Apple App Store builds it rendered and only failed on click via the defense-in-depth throw -- worse UX than the Processing menu, which hides its entry outright, and it contradicted docs/mac-app-store.md's claim that those builds remove the overlay. Hide the whole .geoagent-earth-engine section instead: every control in it (status line, OAuth client id, project id) is Earth Engine-specific and useless without sign-in. * Address Claude review feedback - Extract the iPadOS "desktop Macintosh UA" heuristic into @geolibre/core's new platform.ts and call it from both copies. is-mobile.ts and earth-engine-auth.ts each encoded `/Macintosh/.test(ua) && touch > 1` independently, so a future correction to an Apple behaviour could land in one and miss the other. Only that clause is shared -- the surrounding UA patterns legitimately differ (is-mobile includes Android, the Earth Engine check is Apple-only), so they stay separate. * Address Claude review feedback - Hoist the Earth Engine gate to a module-level EARTH_ENGINE_AVAILABLE constant in ProcessingMenu.tsx and import it in TopToolbar.tsx. TopToolbar called isEarthEngineAvailable() inside the commands array, which is rebuilt every render, while ProcessingMenu memoized it per instance. Both inputs (the build define and the user agent) are fixed for the session, so one module-level constant matches the IS_MAS_BUILD pattern and removes the inconsistency in both directions. - mas-store.yml: annotate a codesign failure. Capturing its output meant a bad signature or path aborted the step with a bare shell error instead of an ::error:: message, unlike the two entitlement assertions below it.
1 parent dc962f9 commit 815c978

27 files changed

Lines changed: 296 additions & 46 deletions

.github/workflows/mas-store.yml

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,20 @@ jobs:
131131
[[ -d "$app" ]] || { echo "::error::No app bundle at $app"; exit 1; }
132132
# Confirm the sandbox entitlement made it into the signature before
133133
# packaging; an unsandboxed binary is an automatic App Review reject.
134-
codesign -d --entitlements - --xml "$app" | grep -q "com.apple.security.app-sandbox" \
134+
entitlements="$(codesign -d --entitlements - --xml "$app")" \
135+
|| { echo "::error::codesign could not read entitlements from $app"; exit 1; }
136+
grep -q "com.apple.security.app-sandbox" <<<"$entitlements" \
135137
|| { echo "::error::App bundle is missing the app-sandbox entitlement"; exit 1; }
138+
# App Review rejected 2.4.0 (guideline 2.4.5) for shipping
139+
# network.server without matching functionality. The app now makes
140+
# only outgoing connections — the Earth Engine OAuth loopback listener
141+
# is compiled out of the `mas` build — so this entitlement must stay
142+
# gone. If a future feature genuinely needs to accept an inbound
143+
# connection, re-add it here AND justify it in App Review Information.
144+
if grep -q "com.apple.security.network.server" <<<"$entitlements"; then
145+
echo "::error::App bundle carries com.apple.security.network.server; App Review rejects it without an inbound-connection feature (see mas/Entitlements.mas.plist.template)"
146+
exit 1
147+
fi
136148
version="$(node -p "require('./apps/geolibre-desktop/src-tauri/tauri.conf.json').version")"
137149
pkg="GeoLibre.Desktop_${version}_universal_mas.pkg"
138150
xcrun productbuild --sign "$APPLE_MAS_INSTALLER_IDENTITY" \

CITATION.cff

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ authors:
99
email: giswqs@gmail.com
1010
affiliation: "University of Tennessee, Knoxville, USA"
1111
orcid: "https://orcid.org/0000-0001-5437-4073"
12-
version: 2.4.0
13-
date-released: 2026-07-29
12+
version: 2.4.1
13+
date-released: 2026-08-02
1414
doi: "10.5281/zenodo.20785400"
1515
repository-code: "https://github.com/opengeos/GeoLibre"
1616
url: "https://geolibre.app"

apps/geolibre-desktop/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "geolibre-desktop",
3-
"version": "2.4.0",
3+
"version": "2.4.1",
44
"private": true,
55
"type": "module",
66
"scripts": {

apps/geolibre-desktop/src-tauri/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/geolibre-desktop/src-tauri/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "geolibre-desktop"
3-
version = "2.4.0"
3+
version = "2.4.1"
44
description = "GeoLibre Desktop — lightweight cloud-native desktop GIS"
55
authors = ["GeoLibre Contributors"]
66
edition = "2021"

apps/geolibre-desktop/src-tauri/mas/Entitlements.mas.plist.template

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,18 @@
1616
1717
- network.client: map tiles, basemap styles, remote GeoJSON/COG/PMTiles,
1818
collab websocket.
19-
- network.server: the Earth Engine OAuth flow listens on a loopback port for
20-
the redirect callback.
2119
- files.user-selected.read-write: open/save project files and datasets picked
2220
through the file dialogs.
21+
22+
There is deliberately NO network.server entitlement. The app makes only
23+
outgoing connections. The one feature that listened for an inbound connection
24+
was Earth Engine sign-in (Google's OAuth loopback-redirect flow binds
25+
127.0.0.1 to receive the browser redirect); App Review rejected the 2.4.0
26+
submission over it (guideline 2.4.5, submission 76036eca), so the `mas` build
27+
compiles that flow out entirely and hides its UI. If you ever add a feature
28+
that accepts an inbound connection, adding the entitlement back is not enough
29+
— it has to be justified in App Review Information, and mas-store.yml's
30+
"no server entitlement" check will fail until this comment is revisited.
2331
-->
2432
<plist version="1.0">
2533
<dict>
@@ -31,8 +39,6 @@
3139
<string>${APPLE_TEAM_ID}</string>
3240
<key>com.apple.security.network.client</key>
3341
<true/>
34-
<key>com.apple.security.network.server</key>
35-
<true/>
3642
<key>com.apple.security.files.user-selected.read-write</key>
3743
<true/>
3844
</dict>

apps/geolibre-desktop/src-tauri/src/lib.rs

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,32 @@
1+
// Earth Engine sign-in uses Google's OAuth loopback-redirect flow, which binds
2+
// a listener on 127.0.0.1 to accept the browser's redirect. Accepting an
3+
// inbound connection requires the `com.apple.security.network.server`
4+
// entitlement, which App Review rejects for an app that otherwise only makes
5+
// outgoing requests (guideline 2.4.5, submission 76036eca). Both Apple App
6+
// Store targets therefore compile the whole flow out and hide the Earth Engine
7+
// UI, so no Apple build ever binds a listening socket: the `mas` (macOS) build,
8+
// and iOS — which ships through the same review pipeline and has no way to
9+
// justify a listener either. Every other target (Developer ID macOS, Windows,
10+
// Linux, Android, web) keeps it. See docs/mac-app-store.md.
11+
#[cfg(not(any(feature = "mas", target_os = "ios")))]
112
mod earth_engine_oauth;
13+
// App Store stub, mirroring the `native_duckdb` pattern below: the commands stay
14+
// in the handler list (so a stale panel state or an external plugin gets a clear
15+
// message instead of an "unknown command" error) but no socket is ever bound.
16+
#[cfg(any(feature = "mas", target_os = "ios"))]
17+
mod earth_engine_oauth {
18+
const UNAVAILABLE: &str = "Earth Engine sign-in is not available in the App Store build of GeoLibre.";
19+
20+
#[tauri::command]
21+
pub fn start_earth_engine_oauth(_client_id: String) -> Result<serde_json::Value, String> {
22+
Err(UNAVAILABLE.to_string())
23+
}
24+
25+
#[tauri::command]
26+
pub fn poll_earth_engine_oauth(_state_id: String) -> Result<Option<serde_json::Value>, String> {
27+
Err(UNAVAILABLE.to_string())
28+
}
29+
}
230
#[cfg(feature = "native-duckdb")]
331
mod native_duckdb;
432
#[cfg(not(feature = "native-duckdb"))]
@@ -28,9 +56,9 @@ mod native_duckdb {
2856
#[cfg(all(feature = "mas", feature = "native-duckdb"))]
2957
compile_error!("the `mas` (Mac App Store) build must not enable `native-duckdb`: DuckDB loads its spatial extension as unsigned native code at runtime, which App Sandbox and App Store guideline 2.5.2 forbid.");
3058

31-
use earth_engine_oauth::{
32-
poll_earth_engine_oauth, start_earth_engine_oauth, EarthEngineOAuthState,
33-
};
59+
use earth_engine_oauth::{poll_earth_engine_oauth, start_earth_engine_oauth};
60+
#[cfg(not(any(feature = "mas", target_os = "ios")))]
61+
use earth_engine_oauth::EarthEngineOAuthState;
3462
use flate2::read::{GzDecoder, ZlibDecoder};
3563
use rusqlite::{params, Connection, OpenFlags, OptionalExtension};
3664
use serde::{Deserialize, Serialize};
@@ -255,8 +283,13 @@ pub fn run() {
255283
.plugin(tauri_plugin_persisted_scope::init())
256284
.plugin(tauri_plugin_geolocation::init())
257285
.plugin(tauri_plugin_http::init())
258-
.plugin(tauri_plugin_opener::init())
259-
.manage(EarthEngineOAuthState::default());
286+
.plugin(tauri_plugin_opener::init());
287+
288+
// The Earth Engine OAuth loopback listener is compiled out of the Apple App
289+
// Store builds (see the module gate at the top of this file); the stub
290+
// commands are stateless, so the state goes with it.
291+
#[cfg(not(any(feature = "mas", target_os = "ios")))]
292+
let builder = builder.manage(EarthEngineOAuthState::default());
260293

261294
// The Martin/sidecar/Jupyter process managers exist only where the commands
262295
// that spawn those processes do; the MAS build compiles both out together.

apps/geolibre-desktop/src-tauri/tauri.conf.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "https://schema.tauri.app/config/2",
33
"productName": "GeoLibre Desktop",
4-
"version": "2.4.0",
4+
"version": "2.4.1",
55
"identifier": "org.geolibre.desktop",
66
"build": {
77
"beforeDevCommand": "npm run dev",

apps/geolibre-desktop/src/components/layout/TopToolbar.tsx

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ import { HelpMenu } from "./toolbar/HelpMenu";
131131
import { OsmPbfDialogs } from "./toolbar/OsmPbfDialogs";
132132
import { PluginsMenu } from "./toolbar/PluginsMenu";
133133
import { PluginToolbarMenus } from "./toolbar/PluginToolbarMenus";
134-
import { ProcessingMenu } from "./toolbar/ProcessingMenu";
134+
import { EARTH_ENGINE_AVAILABLE, ProcessingMenu } from "./toolbar/ProcessingMenu";
135135
import { ProjectFileDialogs } from "./toolbar/ProjectFileDialogs";
136136
import { ProjectMenu } from "./toolbar/ProjectMenu";
137137
import { googleEarthUrl, googleMapsUrl } from "../../lib/external-map-links";
@@ -1201,12 +1201,21 @@ export function TopToolbar({
12011201
group: t("toolbar.commandGroup.processing"),
12021202
run: handleOpenPlanetaryComputer,
12031203
},
1204-
{
1205-
id: "proc.earth-engine",
1206-
title: t("toolbar.command.earthEngine"),
1207-
group: t("toolbar.commandGroup.processing"),
1208-
run: panels.earthEngine.toggle,
1209-
},
1204+
// Earth Engine sign-in needs the Rust loopback OAuth listener, which the
1205+
// Apple App Store builds (Mac App Store and iOS) compile out so the app
1206+
// claims no `com.apple.security.network.server` entitlement. Shares the
1207+
// ProcessingMenu gate's module-level constant rather than recomputing it in
1208+
// this array, which is rebuilt on every render.
1209+
...(EARTH_ENGINE_AVAILABLE
1210+
? [
1211+
{
1212+
id: "proc.earth-engine",
1213+
title: t("toolbar.command.earthEngine"),
1214+
group: t("toolbar.commandGroup.processing"),
1215+
run: panels.earthEngine.toggle,
1216+
},
1217+
]
1218+
: []),
12101219
// Controls
12111220
...MAP_CONTROL_ITEMS.map((control) => ({
12121221
id: `control.${control.id}`,

apps/geolibre-desktop/src/components/layout/toolbar/ProcessingMenu.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { type NetworkToolKind, useAppStore } from "@geolibre/core";
2+
import { isEarthEngineAvailable } from "@geolibre/plugins";
23
import {
34
Button,
45
DropdownMenu,
@@ -22,6 +23,14 @@ import { isMenuItemVisible } from "../../../lib/ui-profile";
2223
import { WHITEBOX_MENU_CATALOG } from "../../../lib/whitebox-menu-catalog";
2324
import type { ToolbarChrome } from "./constants";
2425

26+
// Earth Engine sign-in needs the Rust loopback OAuth listener, which the Apple
27+
// App Store builds (Mac App Store and iOS) compile out so the app claims no
28+
// `com.apple.security.network.server` entitlement — App Review rejected it
29+
// otherwise. Module scope, like IS_MAS_BUILD: the build flag and user agent it
30+
// reads are fixed for the session, so there is nothing to recompute per render.
31+
// TopToolbar's command-palette gate reads the same constant.
32+
export const EARTH_ENGINE_AVAILABLE = isEarthEngineAvailable();
33+
2534
interface ProcessingMenuProps {
2635
chrome: ToolbarChrome;
2736
earthEnginePanel: ToolbarPanel;
@@ -72,6 +81,7 @@ export function ProcessingMenu({
7281
// the browser via WebAssembly, so unlike the sidecar-backed tools it stays
7382
// available on mobile.
7483
const showWhitebox = show("processing.whitebox");
84+
const showEarthEngine = EARTH_ENGINE_AVAILABLE && show("processing.earthEngine");
7585

7686
// Open the Whitebox toolbox dialog preselected to a specific tool, used by the
7787
// per-category submenus below. Two store writes: queue the tool, then open.
@@ -104,7 +114,7 @@ export function ProcessingMenu({
104114
show("processing.notebook") ||
105115
show("processing.dashboard") ||
106116
show("processing.planetaryComputer") ||
107-
show("processing.earthEngine");
117+
showEarthEngine;
108118

109119
return (
110120
<DropdownMenu>
@@ -530,7 +540,7 @@ export function ProcessingMenu({
530540
{t("toolbar.command.planetaryComputer")}
531541
</DropdownMenuItem>
532542
)}
533-
{show("processing.earthEngine") && (
543+
{showEarthEngine && (
534544
<DropdownMenuItem onSelect={earthEnginePanel.toggle}>
535545
{t("toolbar.command.earthEngine")}
536546
{earthEnginePanel.visible ? " ✓" : ""}

0 commit comments

Comments
 (0)