Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,20 @@ ARG VITE_GEOLIBRE_SHARE_URL=
# Self-hosted collaboration relay (wss://…). Unset leaves collaboration dark.
# Also settable at RUN time (-e GEOLIBRE_COLLAB_URL=…).
ARG VITE_GEOLIBRE_COLLAB_URL=
# Set to 1 to strip every external CDN reference (unpkg.com, cdn.jsdelivr.net,
# …) from the build output, for deployments that may not load third-party
# hosts. Features that depend on CDN-hosted assets are disabled or degraded —
# see docs/self-hosting.md. Build-time only: the flag is baked into the bundle,
# so it cannot be flipped at RUN time the way the embed/share/collab URLs can.
ARG GEOLIBRE_NO_EXTERNAL_CDN=
ENV GEOLIBRE_APP_BASE=${GEOLIBRE_APP_BASE}
ENV VITE_GEE_OAUTH_CLIENT_ID=${VITE_GEE_OAUTH_CLIENT_ID}
ENV VITE_MAPILLARY_ACCESS_TOKEN=${VITE_MAPILLARY_ACCESS_TOKEN}
ENV VITE_WELCOME_DISABLED=${VITE_WELCOME_DISABLED}
ENV VITE_GEOLIBRE_EMBED_ORIGINS=${VITE_GEOLIBRE_EMBED_ORIGINS}
ENV VITE_GEOLIBRE_SHARE_URL=${VITE_GEOLIBRE_SHARE_URL}
ENV VITE_GEOLIBRE_COLLAB_URL=${VITE_GEOLIBRE_COLLAB_URL}
ENV GEOLIBRE_NO_EXTERNAL_CDN=${GEOLIBRE_NO_EXTERNAL_CDN}

# The `prebuild` hook of apps/geolibre-desktop runs scripts/build-jupyterlite.mjs,
# which generates the site the Notebook panel embeds. That script is best-effort
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,11 +185,18 @@ export function ObjectDetectionDialog({
const [imageBytes, setImageBytes] = useState<ArrayBuffer | null>(null);
const [imageName, setImageName] = useState("");
// Default to a built-in model so detection works out of the box with no file.
const [modelSource, setModelSource] = useState<"builtin" | "local">("builtin");
const [builtinModelId, setBuiltinModelId] = useState(BUILTIN_DETECTION_MODELS[0].id);
// A build with external CDNs disabled ships no built-ins (the weights are
// CDN-hosted), so fall back to a user-supplied model rather than indexing
// into an empty list.
const [modelSource, setModelSource] = useState<"builtin" | "local">(
BUILTIN_DETECTION_MODELS.length > 0 ? "builtin" : "local",
);
const [builtinModelId, setBuiltinModelId] = useState(BUILTIN_DETECTION_MODELS[0]?.id ?? "");
const [modelBytes, setModelBytes] = useState<ArrayBuffer | null>(null);
const [modelName, setModelName] = useState("");
const [classNames, setClassNames] = useState(BUILTIN_DETECTION_MODELS[0].classNames.join(", "));
const [classNames, setClassNames] = useState(
BUILTIN_DETECTION_MODELS[0]?.classNames.join(", ") ?? "",
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
const [confidence, setConfidence] = useState(0.25);
const [iou, setIou] = useState(0.45);
const [inputSize, setInputSize] = useState(640);
Expand Down Expand Up @@ -539,7 +546,9 @@ export function ObjectDetectionDialog({
if (next === "builtin") selectBuiltinModel(builtinModelId);
}}
>
<option value="builtin">{t("objectDetection.modelSourceBuiltin")}</option>
{BUILTIN_DETECTION_MODELS.length > 0 ? (
<option value="builtin">{t("objectDetection.modelSourceBuiltin")}</option>
) : null}
<option value="local">{t("objectDetection.modelSourceLocal")}</option>
</Select>
</div>
Expand Down
13 changes: 13 additions & 0 deletions apps/geolibre-desktop/src/lib/build-flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,16 @@
*/
export const IS_MAS_BUILD: boolean =
typeof __GEOLIBRE_MAS_BUILD__ !== "undefined" ? __GEOLIBRE_MAS_BUILD__ : false;

/**
* True when the build was configured with `GEOLIBRE_NO_EXTERNAL_CDN=1`, which
* strips every reference to an external CDN origin (unpkg.com,
* cdn.jsdelivr.net, …) so the deployment loads nothing from a third-party host.
* Features that depend on CDN-hosted assets (story map HTML export, the
* built-in detection models, ONNX WASM, the 3D Tiles decoders, Pyodide) are
* disabled or degraded. Guarded the same way as IS_MAS_BUILD so pure helpers
* stay importable in a plain Node test, where the define is absent.
* Injected by vite.config.ts.
*/
export const NO_EXTERNAL_CDN: boolean =
typeof __NO_EXTERNAL_CDN__ !== "undefined" ? __NO_EXTERNAL_CDN__ : false;
35 changes: 19 additions & 16 deletions apps/geolibre-desktop/src/lib/detection-models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
* mirror these files into a GeoLibre-controlled repo so the built-in option does
* not depend on an external account.
*/
import { NO_EXTERNAL_CDN } from "./build-flags";

/** The 80 COCO class names, in model output order. */
export const COCO_CLASSES = [
Expand Down Expand Up @@ -118,22 +119,24 @@ export interface BuiltinDetectionModel {
inputSize: number;
}

export const BUILTIN_DETECTION_MODELS: readonly BuiltinDetectionModel[] = [
{
id: "yolov8n-coco",
label: "YOLOv8n (COCO, 80 classes)",
url: "https://cdn.jsdelivr.net/gh/Hyuto/yolov8-onnxruntime-web@fc4a52c466d15ad4519873a0cef22fbc935b93b6/public/model/yolov8n.onnx",
classNames: COCO_CLASSES,
inputSize: 640,
},
{
id: "yolov5n-coco",
label: "YOLOv5n (COCO, 80 classes)",
url: "https://cdn.jsdelivr.net/gh/Hyuto/yolov5-onnxruntime-web@203637cc45962e40a81b2a7e78f98813f93971db/public/model/yolov5n.onnx",
classNames: COCO_CLASSES,
inputSize: 640,
},
];
export const BUILTIN_DETECTION_MODELS: readonly BuiltinDetectionModel[] = NO_EXTERNAL_CDN
? []
: [
{
id: "yolov8n-coco",
label: "YOLOv8n (COCO, 80 classes)",
url: "https://cdn.jsdelivr.net/gh/Hyuto/yolov8-onnxruntime-web@fc4a52c466d15ad4519873a0cef22fbc935b93b6/public/model/yolov8n.onnx",
classNames: COCO_CLASSES,
inputSize: 640,
},
{
id: "yolov5n-coco",
label: "YOLOv5n (COCO, 80 classes)",
url: "https://cdn.jsdelivr.net/gh/Hyuto/yolov5-onnxruntime-web@203637cc45962e40a81b2a7e78f98813f93971db/public/model/yolov5n.onnx",
classNames: COCO_CLASSES,
inputSize: 640,
},
];

/** Cache bucket holding downloaded model files so a model is fetched once. */
const MODEL_CACHE = "geolibre-detection-models";
Expand Down
23 changes: 21 additions & 2 deletions apps/geolibre-desktop/src/lib/pyodide/pyodide-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@
// As of Pyodide 0.27.x, geopandas/shapely/pyproj (with PROJ data) ship in the
// distribution, so a single loadPackage("geopandas") pulls the whole graph.
import { getRuntimeEnvironment } from "@geolibre/core";
import { NO_EXTERNAL_CDN } from "../build-flags";

export const PYODIDE_VERSION = "0.27.7";

const DEFAULT_INDEX_URL = `https://cdn.jsdelivr.net/pyodide/v${PYODIDE_VERSION}/full/`;
const DEFAULT_INDEX_URL = NO_EXTERNAL_CDN
? ""
: `https://cdn.jsdelivr.net/pyodide/v${PYODIDE_VERSION}/full/`;

/**
* Resolve the Pyodide indexURL (where pyodide.js, the wasm runtime, the
Expand All @@ -24,12 +27,25 @@ const DEFAULT_INDEX_URL = `https://cdn.jsdelivr.net/pyodide/v${PYODIDE_VERSION}/
*
* Returns:
* The indexURL string, guaranteed to end with a slash.
*
* Raises:
* Error: When the build disabled external CDNs and no `VITE_PYODIDE_INDEX_URL`
* mirror was configured, so there is nowhere to load Pyodide from.
*/
export function getPyodideIndexUrl(
env: Record<string, string | undefined> = getRuntimeEnvironment(),
): string {
const override = env.VITE_PYODIDE_INDEX_URL?.trim();
const url = override || DEFAULT_INDEX_URL;
// Without an override the default is empty in a no-external-CDN build.
// Falling through would hand callers "/" — a bogus same-origin path that
// fails later with an opaque 404 — so fail here with the reason instead.
if (!url) {
throw new Error(
Comment thread
giswqs marked this conversation as resolved.
"Pyodide is unavailable in this build (external CDN resources are disabled). " +
"Set VITE_PYODIDE_INDEX_URL to a self-hosted Pyodide mirror to enable it.",
);
}
return url.endsWith("/") ? url : `${url}/`;
}

Expand All @@ -48,5 +64,8 @@ export function getPyodideIndexUrl(
* True when `indexURL` is the default CDN URL.
*/
export function isDefaultPyodideIndexUrl(indexURL: string): boolean {
return indexURL === DEFAULT_INDEX_URL;
// There is no default CDN in a no-external-CDN build, so nothing can match
// it — guard the empty default rather than reporting an empty indexURL as
// "the CDN default" and skipping the mirror's CSP workaround.
return DEFAULT_INDEX_URL !== "" && indexURL === DEFAULT_INDEX_URL;
}
10 changes: 10 additions & 0 deletions apps/geolibre-desktop/src/lib/storymap-export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
type MapProjection,
type StoryMap,
} from "@geolibre/core";
import { NO_EXTERNAL_CDN } from "./build-flags";
import { sanitizeStoryHtml } from "./sanitize-html";
import {
STORY_END_STEP_ID,
Expand Down Expand Up @@ -81,6 +82,15 @@ const BLANK_EXPORT_STYLE: Record<string, unknown> = {
* @returns A complete HTML document as a string.
*/
export function buildStoryMapHtml(options: StoryMapExportOptions): string {
// The exported standalone HTML loads maplibre-gl, scrollama, and the RTL text
// plugin from external CDNs (unpkg.com). When external CDN references are
// stripped from the build, the export cannot produce a functioning page.
if (NO_EXTERNAL_CDN) {
throw new Error(
"Story map HTML export is unavailable in this build (external CDN resources are disabled).",
);
}

const {
storymap,
basemapStyleUrl,
Expand Down
8 changes: 8 additions & 0 deletions apps/geolibre-desktop/src/vite-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ declare const __GEOLIBRE_MAS_BUILD__: boolean;
// vite.config.ts.
declare const __GEOLIBRE_EMBED_BUILD__: boolean;

// True when the build is configured with GEOLIBRE_NO_EXTERNAL_CDN=1 to strip
// all references to external CDN origins (unpkg.com, cdn.jsdelivr.net, etc.).
// Features that depend on externally hosted resources (storymap HTML export,
// built-in object detection models, ONNX WASM, 3D Tiles decoders, Pyodide) are
// disabled or degraded. Intended for deployments that cannot load from untrusted
// CDNs (e.g. Amazon/Harmony). See vite.config.ts.
declare const __NO_EXTERNAL_CDN__: boolean;

// jsDelivr URLs for the PGlite engine and its PostGIS extension, injected by
// vite.config.ts. Only the embed (Jupyter wheel) build reads them, from
// pglite-loader.cdn.ts; web/desktop builds bundle PGlite and never reference
Expand Down
16 changes: 16 additions & 0 deletions apps/geolibre-desktop/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,21 @@ if (!process.env.VITE_GEE_OAUTH_CLIENT_ID) {
// the service worker from the desktop bundle.
const IS_TAURI_BUILD = !!process.env.TAURI_ENV_PLATFORM;

// Strip ALL external CDN references (unpkg.com, cdn.jsdelivr.net, etc.) from the
// build output. When set, features that depend on external CDN-hosted resources
// (storymap HTML export, object detection models, ONNX WASM, 3D Tiles decoders,
// Pyodide, PGlite, CereusDB, GDAL) are either disabled or degraded. Intended for
// deployments that cannot reference untrusted external CDNs (e.g. Harmony/Amazon).
// This implicitly forces GEOLIBRE_PGLITE_CDN=0, GEOLIBRE_CEREUS_CDN=0,
// GEOLIBRE_GDAL_CDN=0, and GEOLIBRE_DUCKDB_WASM_CDN=0.
const NO_EXTERNAL_CDN = process.env.GEOLIBRE_NO_EXTERNAL_CDN === "1";
if (NO_EXTERNAL_CDN) {
process.env.GEOLIBRE_PGLITE_CDN = "0";
process.env.GEOLIBRE_CEREUS_CDN = "0";
process.env.GEOLIBRE_GDAL_CDN = "0";
process.env.GEOLIBRE_DUCKDB_WASM_CDN = "0";
Comment thread
giswqs marked this conversation as resolved.
}

// PGlite + PostGIS is ~25 MB raw and weighs ~22 MB inside the Tauri binary
// (postgis.tar is pre-gzipped, so brotli can't shrink it — it was the entire
// 42 → 63 MB binary regression). By default it is fetched from jsDelivr at
Expand Down Expand Up @@ -908,6 +923,7 @@ export default defineConfig({
__GEOLIBRE_STORE_BUILD__: JSON.stringify(IS_STORE_BUILD),
__GEOLIBRE_MAS_BUILD__: JSON.stringify(IS_MAS_BUILD),
__GEOLIBRE_EMBED_BUILD__: JSON.stringify(IS_EMBED),
__NO_EXTERNAL_CDN__: JSON.stringify(NO_EXTERNAL_CDN),
__PGLITE_CDN_URL__: JSON.stringify(PGLITE_CDN_URL),
__PGLITE_POSTGIS_CDN_URL__: JSON.stringify(PGLITE_POSTGIS_CDN_URL),
__CEREUS_WASM_CDN_URL__: JSON.stringify(CEREUS_WASM_CDN_URL),
Expand Down
2 changes: 2 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ Caching is split to keep the first visit light:

So each of these CDN engines needs the network on **first** use, then works offline. To remove even the first-use dependency, build with `GEOLIBRE_PGLITE_CDN=0` and `GEOLIBRE_CEREUS_CDN=0`, which vendor PGlite/PostGIS and the CereusDB wasm back into the build under `/assets/`, where the same-origin rule covers them (PGlite alone re-adds ~22 MB to the Tauri binary).

To strip **all** external CDN references from the build output — for deployments that cannot load any resource from untrusted CDNs — use `GEOLIBRE_NO_EXTERNAL_CDN=1`. This implies `GEOLIBRE_PGLITE_CDN=0`, `GEOLIBRE_CEREUS_CDN=0`, `GEOLIBRE_GDAL_CDN=0`, and `GEOLIBRE_DUCKDB_WASM_CDN=0`, and additionally disables features whose code references external CDNs: storymap HTML export (which injects `<script>` tags from `unpkg.com`), built-in object detection models (YOLO ONNX weights from `cdn.jsdelivr.net`), the ONNX Runtime WASM backend, 3D Tiles Draco/KTX2 decoder fallback paths, and the default Pyodide index URL. Note: some third-party npm packages (DuckDB-WASM, loaders.gl, maplibre-gl-3d-tiles) contain their own internal CDN URL strings that cannot be removed without forking them; these are data/WASM fetch targets (`connect-src`), not script execution (`script-src`).

Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
**DuckDB-WASM goes the other way**, because it is on the critical path for opening a local vector file and so is bundled by default. `GEOLIBRE_DUCKDB_WASM_CDN=1` moves it to jsDelivr instead — or `npm run lite:build`, which sets that flag and then *asserts* no emitted file exceeds the ceiling, so a regression fails the build rather than the upload. It is pinned to the installed version by duckdb-wasm's own `getJsDelivrBundles()`, so the fetched engine cannot drift from the loader compiled into the bundle. The flag exists for one reason: `duckdb-mvp.wasm` (~40 MB) and `duckdb-eh.wasm` (~35 MB) both exceed the **25 MiB per-asset limit on Cloudflare Pages and Workers static assets**, which rejects the upload outright, and nothing else in the build comes close (the next largest is ~22 MB). So this single flag decides whether the web build can be hosted there at all — it takes the output from ~251 MB to ~176 MB with no file over the ceiling. GitHub Pages allows 100 MB per file and needs none of this. The flag is ignored for both targets that ship no service worker: a Tauri build, which must stay offline-capable, and an embed build (`GEOLIBRE_EMBED=1`), where the engine would otherwise be refetched every notebook session with no runtime cache behind it. Neither has the size ceiling this exists for, since a binary and a wheel are not uploaded to Cloudflare. A CDN-hosted worker script cannot be passed to `new Worker` (it must be same-origin), so that variant wraps it in a same-origin blob that `importScripts` the real one; this needs `worker-src blob:` and the CDN in `script-src`, both already in `docker/nginx.conf`. The nested `importScripts` is matched against `script-src` rather than `worker-src` — verified against that policy in Chromium and Firefox, since Firefox has historically checked worker sub-resources against `worker-src`/`child-src`.

Two engines have no such escape hatch. **Pyodide** is always fetched: `VITE_PYODIDE_INDEX_URL` re-points it at a mirror you host — worth doing when jsDelivr is unreachable or disallowed — but note that neither CacheFirst rule matches such a mirror, since one is scoped to `cdn.jsdelivr.net` and the other to same-origin `/assets/`, so unless the mirror is served from under `/assets/` it falls back to ordinary HTTP caching rather than the service worker. **`gdal3.js`** — which backs the Georeferencer's client-side GeoTIFF/COG export — is never vendored at all: its wasm (~28 MB) and data (~12 MB) only ever come from the CDN, and `GEOLIBRE_GDAL_CDN=0` opts out of that rather than bundling them, leaving the loader with no paths and that export unavailable.
Expand Down
19 changes: 19 additions & 0 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,25 @@ Where to find the output:
- **Web build** — static files in `apps/geolibre-desktop/dist/`. Serve this directory with any static web server (or the Docker image above).
- **Desktop installers** — `apps/geolibre-desktop/src-tauri/target/release/bundle/`, with per-platform subfolders: `deb/`, `rpm/`, and `appimage/` on Linux; `msi/` and `nsis/` on Windows; `dmg/` and `macos/` on macOS. The unbundled executable is in `apps/geolibre-desktop/src-tauri/target/release/`. On Linux, `npm run tauri:build` builds `deb` and `rpm` by default; passing `--bundles` replaces that default selection rather than adding to it, so list every format you want, for example `npm run tauri:build -- --bundles deb,rpm,appimage` for all three.

### Build-time flags

| Variable | Default | Effect |
| --- | --- | --- |
| `GEOLIBRE_PGLITE_CDN` | `1` (CDN) | Set `0` to bundle PGlite/PostGIS into the build (~22 MB) instead of loading from jsDelivr. |
| `GEOLIBRE_CEREUS_CDN` | `1` (CDN) | Set `0` to bundle CereusDB WASM (~40 MB) instead of loading from jsDelivr. |
| `GEOLIBRE_GDAL_CDN` | `1` (CDN) | Set `0` to disable GDAL export (the ~40 MB WASM/data are not bundled, just unavailable). |
| `GEOLIBRE_DUCKDB_WASM_CDN` | `0` (bundled) | Set `1` to move DuckDB-WASM to jsDelivr (required for Cloudflare Pages' 25 MiB per-file limit). |
| `GEOLIBRE_NO_EXTERNAL_CDN` | unset | Set `1` to strip **all** external CDN references from the build. Forces all `*_CDN=0` flags and disables features that embed CDN URLs (storymap HTML export, built-in detection models, ONNX WASM, 3D Tiles decoders, Pyodide). Intended for enterprise deployments that cannot reference untrusted CDNs. |
| `GEOLIBRE_STORE_BUILD` | unset | Set `1` for Microsoft Store MSIX builds (removes in-app updater). |
| `GEOLIBRE_MAS_BUILD` | unset | Set `1` for Mac App Store builds (removes sidecar/server features). |
| `GEOLIBRE_EMBED` | unset | Set `1` for the Jupyter embed wheel build. |

Example — build with no external CDN dependencies:

```bash
GEOLIBRE_NO_EXTERNAL_CDN=1 npx vite build
```

## Optional imagery credentials

The Street View plugin can use Google Street View and Mapillary imagery. The 3D Tiles panel can also load Google Photorealistic 3D Tiles with the same Google Maps key. Create `apps/geolibre-desktop/.env.local` and set one or both provider credentials:
Expand Down
1 change: 1 addition & 0 deletions docs/self-hosting.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ Settings that matter for a private deployment:
| `GEOLIBRE_POSTGIS_HOSTS` | unset unless needed | The sidecar's PostGIS endpoints refuse every destination until this names the allowed databases, so a caller cannot aim them at hosts only the container can reach. |
| `GEOLIBRE_DISABLE_SIDECAR` | `1` if you do not need it | Runs nginx only. |
| `GEOLIBRE_EMBED_ORIGINS` | unset, or the exact host page origin | Off by default, so a framed deployment cannot be driven by whoever frames it. |
| `GEOLIBRE_NO_EXTERNAL_CDN` (build arg) | `1` for restricted deployments | Strips all references to external CDNs (`unpkg.com`, `cdn.jsdelivr.net`) from the build output. Features that depend on CDN-hosted resources (storymap HTML export, built-in object detection models, ONNX WASM, 3D Tiles decoders, Pyodide, PGlite, CereusDB, gdal3.js) are disabled or degraded. Also forces `GEOLIBRE_PGLITE_CDN=0`, `GEOLIBRE_CEREUS_CDN=0`, `GEOLIBRE_GDAL_CDN=0`, and `GEOLIBRE_DUCKDB_WASM_CDN=0`. Intended for deployments that cannot reference untrusted external CDNs (e.g. enterprise environments with strict CSP requirements). |
Comment thread
giswqs marked this conversation as resolved.
Outdated
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
| `VITE_WELCOME_DISABLED=1` (build arg) | optional | Skips the first-launch wizard for every visitor. |

See [Getting Started](getting-started.md#run-with-docker) for the full list.
Expand Down
Loading
Loading