Skip to content

feat(share): point the app at a self-hosted share/collab server at runtime - #1687

Merged
giswqs merged 7 commits into
mainfrom
feat/runtime-share-host
Aug 4, 2026
Merged

feat(share): point the app at a self-hosted share/collab server at runtime#1687
giswqs merged 7 commits into
mainfrom
feat/runtime-share-host

Conversation

@giswqs

@giswqs giswqs commented Aug 3, 2026

Copy link
Copy Markdown
Member

Closes #1684. First step of #1665's self-hosting track.

Problem

The web container could not be pointed at a self-hosted sharing server or collaboration relay.

resolveShareBaseUrl() and resolveCollabBaseUrl() already honored VITE_GEOLIBRE_SHARE_URL / VITE_GEOLIBRE_COLLAB_URL — but both read import.meta.env, so they were build-time only, and neither variable appeared in Dockerfile or docker/entrypoint.sh. Repointing the published image meant forking and rebuilding it.

There was also a worse failure mode. resolveShareBaseUrl() fell back to https://share.geolibre.app whenever the configured value failed to parse or was HTTP on a non-loopback host. A self-hosted deployment that set http://geolibre.lan:8080, or typo'd its own hostname, got a working Share button that uploaded its users' projects to the public hosted service.

What changed

The entrypoint already writes geolibre-runtime-config.js on every boot (for the AI proxy and embed origins), so both hosts now flow through that same channel:

lib/deployment-env.ts New. Reads one VITE_* value, deployment env before build env — the precedence readEmbedOrigins and readDeploymentAssistantEnv already use.
resolveShareHost() New. Returns a status — default / configured / disabled / invalid — plus the base URL and the configured value. resolveShareBaseUrl() is now a thin wrapper returning string | null.
GEOLIBRE_SHARE_URL=off Removes Share and the Project Gallery from the UI entirely.
resolveCollabBaseUrl() Reads the deployment env too, so a prebuilt image can enable collaboration against a self-hosted relay.
docker/entrypoint.sh Carries both vars, validating each and exiting on a malformed value — the same discipline the GEOLIBRE_EMBED_ORIGINS block already applies.
Dockerfile Matching ARG/ENV pairs, so the build-time path is documented rather than accidental.

No more silent fallback. A rejected value resolves to null; the hosted default applies only when nothing is configured. Menu entries disable with a reason, and the gallery throws a new not-configured GalleryErrorCode.

The hostname is no longer hardcoded in the UI. 11 catalogue keys across all 16 locales now take a {{shareHost}} interpolation (the hostname isn't translated, so this was a mechanical substitution), and the account-settings link in SettingsDialog / ShareProjectDialog derives from the resolved host. A self-hosted instance previously read "Sign in to share.geolibre.app" while linking somewhere else.

TLS policy (decision on item 6 of the issue)

Kept as-is and now documented: HTTPS/WSS required, plaintext only on loopback. These URLs carry a Bearer token. The change is that a value failing this now fails the container boot with an error naming the variable, instead of quietly using the public service. Self-hosters put the server behind a TLS-terminating proxy. I did not add the GEOLIBRE_SHARE_ALLOW_INSECURE escape hatch the issue floated — happy to if you'd rather have it.

Verification

Beyond the unit tests, I drove the built web app under vite preview with the runtime config set four ways (clearing the service worker between each, since it precaches the config):

Config Result
unset Unchanged. Share enabled, copy names share.geolibre.app
off Share and Gallery absent from the Project menu
http://internal.corp Both present, aria-disabled=true, title "Unavailable: this deployment's sharing server address is not valid."
https://maps.example.org Share enabled; copy reads "Sign in to maps.example.org"; "Get API token" opens https://maps.example.org/settings

Also exercised the entrypoint's validator across 12 inputs — https, off/OFF, loopback http, wss, loopback ws all accepted; plaintext LAN, unparseable, embedded credentials, and wrong-scheme all exit with a message naming the variable.

Gate: npm run build, npm run test:frontend (4959 pass / 0 fail), npm run test:worker, npm run lint (0 errors; the two TopToolbar warnings are pre-existing on main, verified by stashing).

Notes for review

  • resolveShareBaseUrl() changing from string to string | null is the deliberate core of this. Four call sites handle null; the tests that asserted the old fallback now assert refusal.
  • Desktop is unchanged in reach: installNativeShareFetch skips the override when null, but the Tauri http:default capability scope still pins the share host, so self-hosting stays a web/Docker capability. Called out in share-fetch.ts and the docs.
  • pre-commit run could not install its node hook env locally (npm error Unknown cli flag: --ignore-prepublish — a toolchain issue on my machine, not this branch), so I ran the hook equivalents directly: oxfmt@0.59.0 over every changed file, plus eslint and the build via the npm scripts. pre-commit.ci will run the rest on the PR.
  • Non-English catalogues got the placeholder substituted but are otherwise untouched; no translation drifted.

Summary by CodeRabbit

  • New Features

    • Configure project sharing and collaboration services per deployment at build time or runtime.
    • Support custom self-hosted endpoints with secure URL validation.
    • Sharing and Project Gallery controls now reflect service availability.
  • Bug Fixes

    • Prevented requests and uploads when services are unavailable or invalid.
    • Updated translated messages to display the configured sharing host.
  • Documentation

    • Added self-hosting guidance for sharing and collaboration configuration.

…ntime

The web container could not be pointed at a self-hosted sharing server or
collaboration relay. `resolveShareBaseUrl()` and `resolveCollabBaseUrl()` already
honored `VITE_GEOLIBRE_SHARE_URL` / `VITE_GEOLIBRE_COLLAB_URL`, but both read
`import.meta.env` (build time only) and neither variable reached `Dockerfile` or
`docker/entrypoint.sh` — so repointing a published image meant forking it.

The entrypoint already writes `geolibre-runtime-config.js` on every boot for the
AI proxy and embed origins, so this wires both hosts through that same channel:

- `lib/deployment-env.ts` reads one `VITE_*` value, deployment env before build
  env, matching the precedence `readEmbedOrigins`/`readDeploymentAssistantEnv`
  already use.
- `resolveShareHost()` returns a status (`default` / `configured` / `disabled` /
  `invalid`) plus the base URL. `GEOLIBRE_SHARE_URL=off` removes Share and the
  Project Gallery entirely.
- `resolveCollabBaseUrl()` reads the deployment env too.
- The entrypoint validates both and exits on a malformed value, as the
  `GEOLIBRE_EMBED_ORIGINS` block already does.

A rejected value no longer falls back to `share.geolibre.app`. Previously a
self-hosted deployment with a typo'd or plaintext host silently uploaded its
users' projects to the public hosted service; `resolveShareBaseUrl()` now returns
null, the menu entries disable with a reason, and the gallery reports a new
`not-configured` error instead.

The hostname in UI copy is also no longer hardcoded: 11 keys across all 16
catalogues take a `{{shareHost}}` interpolation, and the account-settings link in
SettingsDialog/ShareProjectDialog derives from the resolved host, so a
self-hosted instance no longer names or links to share.geolibre.app.

Verified in the built web app against a preview server with the runtime config
set four ways: unset (unchanged — Share enabled, copy names share.geolibre.app),
`off` (Share and Gallery absent), an invalid host (both present but disabled with
the reason), and `https://maps.example.org` (copy and token link both name the
self-hosted host). Also exercised the entrypoint's validation across 12 inputs.

Desktop note: the Tauri `http:default` capability scope still pins the share
host, so self-hosting remains a web/Docker capability for now.

Refs #1684, #1665
Copilot AI review requested due to automatic review settings August 3, 2026 20:19
@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The change adds runtime and build-time configuration for self-hosted sharing and collaboration services. It validates service URLs, resolves deployment-specific hosts, updates sharing behavior and UI, and replaces fixed sharing-host text across supported locales.

Changes

Self-hosted service configuration

Layer / File(s) Summary
Deployment environment and Docker wiring
Dockerfile, docker/entrypoint.sh, apps/geolibre-desktop/src/lib/deployment-env.ts, apps/geolibre-desktop/src/lib/collab-client.ts, tests/*, docs/*
Docker and runtime configuration support sharing and collaboration URLs. Runtime values override build-time values. URL validation enforces HTTPS/WSS, with loopback HTTP/WS exceptions.
Share host resolution and request guards
apps/geolibre-desktop/src/lib/share-geolibre.ts, apps/geolibre-desktop/src/lib/share-gallery.ts, apps/geolibre-desktop/src/lib/share-fetch.ts, apps/geolibre-desktop/src/hooks/useProjectFileActions.ts, tests/share-*
Share hosts expose default, configured, disabled, and invalid states. Gallery requests and uploads reject unavailable hosts without network requests.
Sharing availability and host-specific UI
apps/geolibre-desktop/src/components/layout/*, apps/geolibre-desktop/src/components/layout/toolbar/*, apps/geolibre-desktop/src/i18n/locales/*
Menus and dialogs use the resolved host, hide or disable sharing actions when required, conditionally render settings links, and interpolate the configured host in translations.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

Possibly related PRs

  • opengeos/GeoLibre#1451 — Both changes modify self-hosting documentation and deployment configuration topics.

Sequence Diagram(s)

sequenceDiagram
  participant Admin
  participant Docker
  participant Frontend
  participant ShareService
  Admin->>Docker: configure GEOLIBRE_SHARE_URL
  Docker->>Docker: validate and write runtime configuration
  Frontend->>Frontend: resolve share host status
  Frontend->>ShareService: upload or fetch using configured host
  ShareService-->>Frontend: return sharing response
Loading

Poem

A rabbit sets a sharing host,
Runtime paths replace the fixed domain.
Docker checks each URL with care,
Menus hide when none is there.
The relay hops through a trusted door.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: runtime configuration for self-hosted sharing and collaboration servers.
Docstring Coverage ✅ Passed Docstring coverage is 81.82% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/runtime-share-host

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

🔍 Cloudflare PR preview

Item Value
Site https://46d47ac9.geolibre-preview.pages.dev
Demo app https://46d47ac9.geolibre-preview.pages.dev/demo/
Commit 2e8ecb4

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements runtime (container-startup) configuration for GeoLibre’s Share and Collaboration hosts in the Docker/web deployment, eliminating the prior silent fallback to the public hosted share service on misconfiguration and enabling GEOLIBRE_SHARE_URL=off to remove sharing UI entirely.

Changes:

  • Adds a shared deployment-env reader (window.__GEOLIBRE_DEPLOYMENT_ENV__ precedence over import.meta.env) and wires Share/Collab URL resolution through it.
  • Reworks share host resolution to return a status (default/configured/disabled/invalid) and makes callers handle string | null for share base URLs.
  • Updates Docker entrypoint + docs + UI + tests to validate, surface, and explain disabled/invalid configurations; updates i18n strings to interpolate {{shareHost}}.

Reviewed changes

Copilot reviewed 35 out of 35 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/share-geolibre.test.ts Updates share URL tests for “refuse instead of fallback”, adds resolveShareHost coverage and deployment-env precedence assertions.
tests/share-gallery.test.ts Adds tests ensuring gallery throws not-configured when sharing is disabled/invalid in deployment env.
tests/deployment-env.test.ts New unit tests for deployment-env precedence and blank-value handling.
tests/collab-protocol.test.ts Adds tests for collab URL deployment-env precedence and validation.
docs/getting-started.md Documents GEOLIBRE_SHARE_URL / GEOLIBRE_COLLAB_URL runtime configuration and TLS policy.
docs/collaboration.md Documents Docker runtime configuration path for the collab relay and failure behavior.
Dockerfile Adds documented ARG/ENV for VITE_GEOLIBRE_SHARE_URL and VITE_GEOLIBRE_COLLAB_URL.
docker/entrypoint.sh Validates share/collab URLs at boot, writes them into runtime config, and logs chosen endpoints.
apps/geolibre-desktop/src/lib/share-geolibre.ts Introduces resolveShareHost + status, makes resolveShareBaseUrl return `string
apps/geolibre-desktop/src/lib/share-gallery.ts Throws GalleryError("not-configured") when share base URL is unavailable, preventing unintended hosted listing usage.
apps/geolibre-desktop/src/lib/share-fetch.ts Skips installing native fetch override when share base URL resolves to null.
apps/geolibre-desktop/src/lib/deployment-env.ts New helper to read deployment env from window and prefer it over build-time env.
apps/geolibre-desktop/src/lib/collab-client.ts Reads collab base URL from deployment env first; keeps validation and returns null on invalid/unset.
apps/geolibre-desktop/src/hooks/useProjectFileActions.ts Avoids attaching share token auth when no share base URL is configured.
apps/geolibre-desktop/src/components/layout/TopToolbar.tsx Resolves share host once per render and propagates share availability/status to menus/commands.
apps/geolibre-desktop/src/components/layout/toolbar/ProjectMenu.tsx Hides Share/Gallery when disabled; disables with reason when invalid.
apps/geolibre-desktop/src/components/layout/ShareProjectDialog.tsx Derives account settings link from resolved share base URL and interpolates shareHost in copy.
apps/geolibre-desktop/src/components/layout/SettingsDialog.tsx Derives token help link from resolved share host and interpolates shareHost in copy.
apps/geolibre-desktop/src/components/layout/ProjectGalleryDialog.tsx Interpolates shareHost in gallery copy/error messages and maps not-configured to a dedicated string.
apps/geolibre-desktop/src/i18n/locales/en.json Switches Share/Gallery copy to {{shareHost}} interpolation; adds new error/tooltip strings.
apps/geolibre-desktop/src/i18n/locales/ar.json Switches Share/Gallery copy to {{shareHost}} interpolation.
apps/geolibre-desktop/src/i18n/locales/de.json Switches Share/Gallery copy to {{shareHost}} interpolation.
apps/geolibre-desktop/src/i18n/locales/es.json Switches Share/Gallery copy to {{shareHost}} interpolation.
apps/geolibre-desktop/src/i18n/locales/fr.json Switches Share/Gallery copy to {{shareHost}} interpolation.
apps/geolibre-desktop/src/i18n/locales/hi.json Switches Share/Gallery copy to {{shareHost}} interpolation.
apps/geolibre-desktop/src/i18n/locales/id.json Switches Share/Gallery copy to {{shareHost}} interpolation.
apps/geolibre-desktop/src/i18n/locales/it.json Switches Share/Gallery copy to {{shareHost}} interpolation.
apps/geolibre-desktop/src/i18n/locales/ja.json Switches Share/Gallery copy to {{shareHost}} interpolation.
apps/geolibre-desktop/src/i18n/locales/ka.json Switches Share/Gallery copy to {{shareHost}} interpolation.
apps/geolibre-desktop/src/i18n/locales/ko.json Switches Share/Gallery copy to {{shareHost}} interpolation.
apps/geolibre-desktop/src/i18n/locales/nl.json Switches Share/Gallery copy to {{shareHost}} interpolation.
apps/geolibre-desktop/src/i18n/locales/pt.json Switches Share/Gallery copy to {{shareHost}} interpolation.
apps/geolibre-desktop/src/i18n/locales/ru.json Switches Share/Gallery copy to {{shareHost}} interpolation.
apps/geolibre-desktop/src/i18n/locales/tr.json Switches Share/Gallery copy to {{shareHost}} interpolation.
apps/geolibre-desktop/src/i18n/locales/zh.json Switches Share/Gallery copy to {{shareHost}} interpolation.
Suppressed comments (1)

apps/geolibre-desktop/src/lib/collab-client.ts:66

  • resolveCollabBaseUrl accepts ws(s):// URLs with embedded credentials (userinfo). Even if uncommon for WebSocket URLs, it should be rejected for consistency with the container entrypoint validator and to avoid accidentally enabling Basic Auth / leaking secrets via configuration.
  const value =
    configured !== undefined ? configured : readDeploymentEnvValue(COLLAB_URL_ENV, deploymentEnv);
  if (typeof value !== "string" || !value.trim()) return null;
  const trimmed = value.trim().replace(/\/+$/, "");
  try {
    const url = new URL(trimmed);
    if (
      url.protocol === "wss:" ||
      (url.protocol === "ws:" &&
        (url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]"))
    ) {
      return trimmed;
    }
  } catch {
    // Invalid URL; treat as unconfigured.
  }
  return null;

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread apps/geolibre-desktop/src/lib/share-geolibre.ts
Comment thread apps/geolibre-desktop/src/i18n/locales/en.json
Comment thread apps/geolibre-desktop/src/i18n/locales/en.json
Comment thread docker/entrypoint.sh
Comment thread apps/geolibre-desktop/src/lib/share-geolibre.ts
Comment thread apps/geolibre-desktop/src/lib/share-geolibre.ts
// deployment env does not change while the app is running) and passed down so
// the menu, the command palette, and the dialogs agree.
const shareHost = resolveShareHost();
const shareAvailable = shareHost.baseUrl != null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor UX inconsistency: when shareHostStatus === "invalid", ProjectMenu keeps the Share/Gallery items visible but disabled with a reason (shareBrokenReason) so the misconfiguration is discoverable. Here, shareAvailable (baseUrl != null) is false for both disabled and invalid, so the command-palette entry is silently omitted in the invalid case too — a user relying on the command palette gets no hint that sharing is broken vs. simply off. Not a functional bug, just a discoverability gap between the two entry points. Confidence: low.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not fixing this one — leaving the thread open for a maintainer call.

The gap is real, but the Command type in lib/commands.ts has no disabled state: it is { id, title, group, keywords?, shortcut?, icon?, run }. Surfacing a broken-config hint in the palette means adding one and teaching both the palette renderer and the global shortcut handler to honour it, which is a lot of surface for a state an operator hits once at deploy time — and one the container now refuses to boot into at all, since the entrypoint exits on a malformed GEOLIBRE_SHARE_URL. The only way to reach invalid in practice is a bad build-time var or a hand-edited runtime config.

The menu keeps the discoverable version (visible reason + aria-describedby, tightened in 8a879a4), so the hint exists in the UI. Happy to add disabled to Command if you'd rather have parity.

Comment thread apps/geolibre-desktop/src/i18n/locales/en.json
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

All five inline comments posted successfully.

Code review

Bugs

  • docker/entrypoint.sh (service_url, L178-188): the embedded-credentials check runs only in the non-loopback branch; a loopback value like http://user:pass@localhost returns early on the earlier return value and skips the check entirely, despite the function's own docstring saying credentials aren't allowed — inconsistent with the GEOLIBRE_AI_PROXY_URL validator just above it, which checks unconditionally. Confidence: medium.

Security

  • apps/geolibre-desktop/src/lib/share-geolibre.ts (isSafeShareUrl, L126-138): the client-side URL validator checks protocol/hostname but never rejects embedded credentials, unlike the new server-side entrypoint.sh validator. Not a regression (pre-PR code had the same gap), but the PR tightens this exact class of check on the runtime path while leaving the build-time (VITE_GEOLIBRE_SHARE_URL) path unguarded — worth mirroring for consistency. Confidence: low.

Quality

  • apps/geolibre-desktop/src/lib/share-geolibre.ts (shareHostLabel, L185-202): falls back to share.geolibre.app's hostname whenever resolveShareBaseUrl() is null, which includes the deliberate disabled ("off") status, not just invalid/unset. Settings still reads "Sign in to share.geolibre.app" on a deployment that explicitly turned sharing off everywhere else in the UI. Confidence: medium.
  • apps/geolibre-desktop/src/components/layout/TopToolbar.tsx (L862): the command palette hides the Share entry entirely for both disabled and invalid share-host status, while ProjectMenu shows it visible-but-disabled-with-reason for invalid — a minor discoverability inconsistency between the two entry points. Confidence: low.
  • apps/geolibre-desktop/src/i18n/locales/en.json (L1193): the new gallery.errorNotConfigured key was added only to en.json, not the other 16 locale catalogues (unlike the mechanical {{shareHost}} substitution applied everywhere). Likely fine via fallbackLng, flagged in case it was meant to ship translated. Confidence: low.

CLAUDE.md

  • No violations found: new UI strings correctly use t()/Trans with the {{shareHost}} interpolation, RTL-safe logical Tailwind classes weren't touched, and the uv.lock/whitebox-catalog/mirror-constant rules aren't implicated by this change.

Overall the core logic (resolveShareHost, readDeploymentEnvValue, the entrypoint validator, and the four call sites now handling null) is solid and well-tested — the "no silent fallback to the public host" behavior the PR sets out to fix is implemented correctly in the paths I traced.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/geolibre-desktop/src/lib/share-geolibre.ts (1)

255-264: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use the resolved share host in these error messages, not the hardcoded hosted-service name.

Line 219 in this same function was reworded to drop a hardcoded host reference, but Line 263 and Line 274 still hardcode share.geolibre.app. For a self-hosted deployment, a network failure or malformed response reports the wrong server name to the user, which contradicts the goal of dynamically naming the configured host throughout the UI. shareHostLabel() in this file already derives the correct host string; reuse it (or base) here.

🛠️ Proposed fix to name the actual configured host
   } catch (error) {
     if (error instanceof DOMException) {
       // Caller-initiated abort (dialog closed): propagate so the UI ignores it.
       if (error.name === "AbortError") throw error;
       if (error.name === "TimeoutError") {
         throw new Error("Upload timed out. Please try again.");
       }
     }
-    throw new Error("Could not reach share.geolibre.app. Check your internet connection.");
+    throw new Error(`Could not reach ${shareHostLabel()}. Check your internet connection.`);
   }

   if (!response.ok) {
     const { message, code } = await uploadErrorInfo(response);
     throw new ShareUploadError(message, code);
   }

   const payload = (await response.json().catch(() => ({}))) as ShareProjectResponse;
   const project = payload.project;
   if (!project?.projectUrl || !project.rawJsonUrl) {
-    throw new Error("share.geolibre.app returned an unexpected response.");
+    throw new Error(`${shareHostLabel()} returned an unexpected response.`);
   }

Also applies to: 271-275

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/geolibre-desktop/src/lib/share-geolibre.ts` around lines 255 - 264,
Update the error messages in the catch handling around the share upload
function, including the malformed-response path near lines 271–275, to use the
resolved host from shareHostLabel() or base instead of the hardcoded
share.geolibre.app name. Preserve the existing timeout and abort behavior while
ensuring network and response errors identify the configured server.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/geolibre-desktop/src/components/layout/SettingsDialog.tsx`:
- Around line 382-388: Hide the share-token guidance and input in
apps/geolibre-desktop/src/components/layout/SettingsDialog.tsx at lines 382-388
when shareBaseUrl is null, or show an unavailable-state message without using
shareHostLabel(). In
apps/geolibre-desktop/src/components/layout/ShareProjectDialog.tsx at lines
62-66, guard the dialog with the same unavailable state and do not render setup
or upload guidance based on shareHostLabel() when the resolved share URL is
null.

In `@apps/geolibre-desktop/src/components/layout/toolbar/ProjectMenu.tsx`:
- Around line 156-165: Make the invalid-host reason accessible for both the
Gallery item at
apps/geolibre-desktop/src/components/layout/toolbar/ProjectMenu.tsx:156-165 and
the Share item at
apps/geolibre-desktop/src/components/layout/toolbar/ProjectMenu.tsx:279-280 by
wrapping each disabled DropdownMenuItem in an enabled tooltip trigger or adding
an equivalent accessible explanatory element; do not rely on the native title
attribute.
- Around line 102-106: Update the showSaveGroup calculation to count the Share
item only when shareHidden is false, so a group containing only disabled sharing
is not rendered without items. Preserve the existing group visibility behavior
for the other save-related items and use the shareHidden symbol defined near
shareBroken.

In `@apps/geolibre-desktop/src/hooks/useProjectFileActions.ts`:
- Around line 521-525: In the project-loading flow around fetchProjectFromUrl
and loadProject, derive a single usesShareAuth flag from the effective
shareBaseUrl, auth token, and the same-origin validation used by
shareAuthorizedFetch. Use this flag both to select shareAuthorizedFetch versus
the unauthenticated fetch and to determine the path stored by loadProject,
replacing the independent options.authToken check so recent-project persistence
matches the actual authentication decision.

In `@apps/geolibre-desktop/src/lib/share-fetch.ts`:
- Around line 57-73: Update installNativeShareFetch to derive the native-fetch
host guard from the configured URL.origin rather than only URL.host, preserving
HTTPS origin boundaries when deciding whether to intercept requests. Add
coverage for an HTTPS base URL receiving an HTTP request and verify that request
remains on the browser fetch path.

---

Outside diff comments:
In `@apps/geolibre-desktop/src/lib/share-geolibre.ts`:
- Around line 255-264: Update the error messages in the catch handling around
the share upload function, including the malformed-response path near lines
271–275, to use the resolved host from shareHostLabel() or base instead of the
hardcoded share.geolibre.app name. Preserve the existing timeout and abort
behavior while ensuring network and response errors identify the configured
server.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: d960b90d-ed10-49ac-a004-bc26925f09fd

📥 Commits

Reviewing files that changed from the base of the PR and between da72fe7 and c1bf0dd.

📒 Files selected for processing (35)
  • Dockerfile
  • apps/geolibre-desktop/src/components/layout/ProjectGalleryDialog.tsx
  • apps/geolibre-desktop/src/components/layout/SettingsDialog.tsx
  • apps/geolibre-desktop/src/components/layout/ShareProjectDialog.tsx
  • apps/geolibre-desktop/src/components/layout/TopToolbar.tsx
  • apps/geolibre-desktop/src/components/layout/toolbar/ProjectMenu.tsx
  • apps/geolibre-desktop/src/hooks/useProjectFileActions.ts
  • apps/geolibre-desktop/src/i18n/locales/ar.json
  • apps/geolibre-desktop/src/i18n/locales/de.json
  • apps/geolibre-desktop/src/i18n/locales/en.json
  • apps/geolibre-desktop/src/i18n/locales/es.json
  • apps/geolibre-desktop/src/i18n/locales/fr.json
  • apps/geolibre-desktop/src/i18n/locales/hi.json
  • apps/geolibre-desktop/src/i18n/locales/id.json
  • apps/geolibre-desktop/src/i18n/locales/it.json
  • apps/geolibre-desktop/src/i18n/locales/ja.json
  • apps/geolibre-desktop/src/i18n/locales/ka.json
  • apps/geolibre-desktop/src/i18n/locales/ko.json
  • apps/geolibre-desktop/src/i18n/locales/nl.json
  • apps/geolibre-desktop/src/i18n/locales/pt.json
  • apps/geolibre-desktop/src/i18n/locales/ru.json
  • apps/geolibre-desktop/src/i18n/locales/tr.json
  • apps/geolibre-desktop/src/i18n/locales/zh.json
  • apps/geolibre-desktop/src/lib/collab-client.ts
  • apps/geolibre-desktop/src/lib/deployment-env.ts
  • apps/geolibre-desktop/src/lib/share-fetch.ts
  • apps/geolibre-desktop/src/lib/share-gallery.ts
  • apps/geolibre-desktop/src/lib/share-geolibre.ts
  • docker/entrypoint.sh
  • docs/collaboration.md
  • docs/getting-started.md
  • tests/collab-protocol.test.ts
  • tests/deployment-env.test.ts
  • tests/share-gallery.test.ts
  • tests/share-geolibre.test.ts

Comment thread apps/geolibre-desktop/src/components/layout/SettingsDialog.tsx
Comment thread apps/geolibre-desktop/src/components/layout/toolbar/ProjectMenu.tsx Outdated
Comment thread apps/geolibre-desktop/src/components/layout/toolbar/ProjectMenu.tsx
Comment thread apps/geolibre-desktop/src/hooks/useProjectFileActions.ts Outdated
Comment thread apps/geolibre-desktop/src/lib/share-fetch.ts Outdated
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

🔍 GitHub Pages PR preview

Item Value
Site Deploy failed. See the job log.
Demo app Unavailable
Commit 2e8ecb4

Comment thread apps/geolibre-desktop/src/lib/share-geolibre.ts
Comment thread docker/entrypoint.sh
Comment thread apps/geolibre-desktop/src/i18n/locales/en.json
Comment thread docker/entrypoint.sh Outdated
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

All four findings posted. Final summary below.

Code review

Bugs

  • Two error messages in uploadProjectToShare (apps/geolibre-desktop/src/lib/share-geolibre.ts, network-failure and invalid-response paths) still hardcode share.geolibre.app, contradicting this PR's own goal of removing the hardcoded host from user-facing copy — self-hosted deployments will see the wrong hostname in these two errors. Confidence: high.

Security

  • service_url() in docker/entrypoint.sh returns early on the loopback branch, before the "must not embed credentials" check runs — so a value like http://user:pass@localhost/ bypasses that protection and gets echoed unmasked into container startup logs. Operator-controlled input, so impact is limited, but it's a real gap against the stated intent. Confidence: medium.

Performance

  • None found.

Quality

  • Two newly added i18n keys (gallery.errorNotConfigured, toolbar.item.shareHostUnavailable) were added only to en.json, not the other 15 locale catalogs touched by this PR (which only got the mechanical {{shareHost}} substitution). Not broken — fallbackLng covers it — but non-English users will see these two strings in English. Confidence: medium.
  • Cosmetic: the boot-log case statement in docker/entrypoint.sh only recognizes off|OFF|Off, while the Python validator and the JS resolveShareHost both treat any casing as disabled — a value like oFF logs the raw URL text instead of the "disabled" message. Log text only, no functional effect. Confidence: low.

CLAUDE.md

  • No violations found; i18n keys use t()/Trans, RTL-safe classes weren't touched, and the affected locale files follow the documented mechanical-substitution pattern (aside from the two new-key gaps noted above).

- share-geolibre.ts / collab-client.ts: reject URLs with embedded credentials
  in `isSafeShareUrl` and `resolveCollabBaseUrl`, so the client matches
  `service_url()` in the entrypoint. A `https://user:pass@host` base would send
  Basic Auth alongside the Bearer token (Copilot, Claude).
- docker/entrypoint.sh: move the credential check ahead of the loopback
  early-return, which previously let `http://user:pass@localhost` through despite
  the docstring — and it is echoed to the boot log a few lines down (Claude, x2).
- docker/entrypoint.sh: make the disabled-sharing boot log case-insensitive
  (`[oO][fF][fF]`) to match the Python validator and `resolveShareHost`, both of
  which lowercase before comparing (Claude).
- share-geolibre.ts: the network-failure and unexpected-response upload errors
  still named share.geolibre.app; they now name the resolved host via a new
  `hostOf` helper that `shareHostLabel` also uses (Claude).
- share-fetch.ts: scope the CORS-exempt native fetch by `URL.origin` rather than
  `URL.host`, so a plaintext request to a host configured over HTTPS is not routed
  through it. `requestOrigin` is exported and unit-tested (CodeRabbit).
- useProjectFileActions.ts: derive one `shareAuth` value for both the fetch choice
  and whether the URL is remembered as recent. A token set on a deployment with no
  share host previously loaded unauthenticated yet was still not remembered
  (CodeRabbit).
- ProjectMenu.tsx: count Share in `showSaveGroup` only when it is not hidden, so
  a profile showing only Share cannot leave an orphaned separator (CodeRabbit).
- ProjectMenu.tsx: a disabled DropdownMenuItem carries `pointer-events-none`, so
  the native `title` holding the invalid-host reason could never be hovered. The
  reason is now a rendered line, referenced by `aria-describedby` with a distinct
  id per item (CodeRabbit).
- SettingsDialog.tsx: hide the share-token description, input, and storage note
  when no share host is usable, replacing them with
  `settings.env.tokenUnavailable`. A deployment with `GEOLIBRE_SHARE_URL=off` was
  still telling users to get a token from the public hosted service
  (Claude, CodeRabbit).
- ShareProjectDialog.tsx: guard the dialog on the same state defensively, so a
  future caller cannot render host-bearing setup guidance with no host
  (CodeRabbit).
- Locales: add `gallery.errorNotConfigured`, `toolbar.item.shareHostUnavailable`,
  and the new `settings.env.tokenUnavailable` to all 15 non-English catalogues,
  translated and inserted in en.json key order (Copilot x2, Claude x2).

Verified: the entrypoint validator now rejects credentials on the loopback path
too (9 inputs re-checked), the boot log treats any casing of "off" as disabled,
and in the built app an invalid host renders the reason as visible text with
`aria-describedby` wired up while the Settings token input is replaced by the
unavailable message. 5026 frontend tests pass; lint has 0 errors.
Comment thread apps/geolibre-desktop/src/components/layout/SettingsDialog.tsx
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Code review

Reviewed the full diff: the new deployment-env.ts/resolveShareHost/resolveCollabBaseUrl validation logic, the entrypoint's service_url() Python validator, the Dockerfile ARG/ENV wiring, all UI gating (ProjectMenu, TopToolbar, ShareProjectDialog, SettingsDialog, ProjectGalleryDialog), share-fetch.ts's origin-based (not host-based) matching, and the mechanical {{shareHost}} i18n substitution across all 16 locale files (verified all three new keys — gallery.errorNotConfigured, settings.env.tokenUnavailable, toolbar.item.shareHostUnavailable — are present and consistent in every locale).

Bugs: None found. The client-side (isSafeShareUrl/resolveCollabBaseUrl) and entrypoint (service_url()) validators agree on accepted schemes/loopback hosts/credential rejection; the IPv6 loopback bracket handling differs correctly between Python's urlsplit (strips brackets) and JS's URL.hostname (keeps them), matching each platform's own representation. The null-propagation from resolveShareHostresolveShareBaseUrl → UI gating (menu, command palette, dialogs, native fetch install) is consistent and covered by new tests.

Security: None found. Credentials-in-URL are rejected on both the client and the entrypoint for both share and collab URLs; HTTPS/WSS-except-loopback is enforced consistently; the "no silent fallback to the public host" behavior (the PR's core stated goal) is correctly implemented and tested (resolveShareHost, requireShareBase, uploadProjectToShare).

Performance: No issues; all changes are cheap, render-time string/URL parsing.

Quality:

  • Low-medium confidence: SettingsDialog.tsx's shareTokenUsable/tokenUnavailable copy collapses the disabled and invalid ShareHostStatus states into one generic "no server configured" message, losing the more specific "sharing server address is not valid" wording that ProjectMenu.tsx already shows elsewhere for the invalid case — flagged inline.
  • Very low confidence / not flagged inline: in the same file, the <tokenLink> Trans component's <span /> fallback is unreachable dead code, since that branch only renders when shareTokenUsable is true, which guarantees shareSettingsUrl is non-null.

CLAUDE.md: No violations — new user-facing strings use t()/Trans, all locale catalogues were updated together, and the Docker/entrypoint conventions (validate-and-fail-boot, matching existing GEOLIBRE_EMBED_ORIGINS/GEOLIBRE_AI_* patterns) were followed.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
docker/entrypoint.sh (2)

196-224: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Reject whitespace-only deployment values instead of treating them as unset.

share_url and collab_url are trimmed before their presence checks. For GEOLIBRE_SHARE_URL=" ", no runtime key is emitted, so a build-time value can become active again. This can route sharing to the public service instead of rejecting the deployment value. The shell log also checks the untrimmed value, so " off " is disabled by Python but reported as a configured server.

Track environment-variable presence separately. Reject non-empty values that become empty after trimming. Use the normalized value for the startup message.

Suggested fix
- share_url = os.environ.get("GEOLIBRE_SHARE_URL", "").strip()
- if share_url:
+ raw_share_url = os.environ.get("GEOLIBRE_SHARE_URL")
+ share_url = raw_share_url.strip() if raw_share_url is not None else ""
+ if raw_share_url not in (None, "") and not share_url:
+     raise SystemExit("ERROR: GEOLIBRE_SHARE_URL must not be whitespace-only.")
+ if share_url:
...
- collab_url = os.environ.get("GEOLIBRE_COLLAB_URL", "").strip()
- if collab_url:
+ raw_collab_url = os.environ.get("GEOLIBRE_COLLAB_URL")
+ collab_url = raw_collab_url.strip() if raw_collab_url is not None else ""
+ if raw_collab_url not in (None, "") and not collab_url:
+     raise SystemExit("ERROR: GEOLIBRE_COLLAB_URL must not be whitespace-only.")
+ if collab_url:
...
+share_url_log=$(printf '%s' "${GEOLIBRE_SHARE_URL:-}" |
+  sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
-  case "$GEOLIBRE_SHARE_URL" in
+  case "$share_url_log" in
...
-    *) echo "Project sharing server: $GEOLIBRE_SHARE_URL" ;;
+    *) echo "Project sharing server: $share_url_log" ;;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docker/entrypoint.sh` around lines 196 - 224, Update the Python
deployment-config logic for share_url and collab_url to distinguish an unset
variable from a present-but-whitespace-only value, rejecting the latter instead
of omitting its runtime key. Preserve normalized trimmed values for valid
configuration, and update the GEOLIBRE_SHARE_URL startup logging to use the same
trimmed value so values such as “ off ” are reported as disabled consistently.

169-190: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Validate the service URL hostname and explicit port before accepting it.

urlsplit() accepts https://:443 with no hostname and https://collab.example:bad with an invalid port. These values can pass service_url() and reach configuration as a malformed service URL, or disable self-hosted features when the app rejects them.

Suggested fix
-    parsed = urlsplit(value)
+    try:
+        parsed = urlsplit(value)
+        hostname = parsed.hostname
+        parsed.port  # Force validation of an explicit port.
+    except ValueError:
+        raise SystemExit(f"ERROR: {name} must be a valid URL, not {value!r}.")
     if parsed.username or parsed.password:
         raise SystemExit(f"ERROR: {name} must not embed credentials.")
-    if parsed.scheme in loopback_schemes and parsed.hostname in loopback_hosts:
+    if parsed.scheme in loopback_schemes and hostname in loopback_hosts:
         return value
-    if parsed.scheme not in schemes or not parsed.netloc:
+    if parsed.scheme not in schemes or not parsed.netloc or not hostname:
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docker/entrypoint.sh` around lines 169 - 190, Update service_url to validate
parsed.hostname and the explicit port before accepting either the loopback
shortcut or the general URL path. Reject URLs with a missing hostname or an
invalid/out-of-range port, while preserving the existing credential, scheme,
netloc, and loopback validation behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@docker/entrypoint.sh`:
- Around line 196-224: Update the Python deployment-config logic for share_url
and collab_url to distinguish an unset variable from a
present-but-whitespace-only value, rejecting the latter instead of omitting its
runtime key. Preserve normalized trimmed values for valid configuration, and
update the GEOLIBRE_SHARE_URL startup logging to use the same trimmed value so
values such as “ off ” are reported as disabled consistently.
- Around line 169-190: Update service_url to validate parsed.hostname and the
explicit port before accepting either the loopback shortcut or the general URL
path. Reject URLs with a missing hostname or an invalid/out-of-range port, while
preserving the existing credential, scheme, netloc, and loopback validation
behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 20bcd4c5-a9f4-4755-88f0-cca636f84bbb

📥 Commits

Reviewing files that changed from the base of the PR and between 52049fb and 8a879a4.

📒 Files selected for processing (27)
  • apps/geolibre-desktop/src/components/layout/SettingsDialog.tsx
  • apps/geolibre-desktop/src/components/layout/ShareProjectDialog.tsx
  • apps/geolibre-desktop/src/components/layout/toolbar/ProjectMenu.tsx
  • apps/geolibre-desktop/src/hooks/useProjectFileActions.ts
  • apps/geolibre-desktop/src/i18n/locales/ar.json
  • apps/geolibre-desktop/src/i18n/locales/de.json
  • apps/geolibre-desktop/src/i18n/locales/en.json
  • apps/geolibre-desktop/src/i18n/locales/es.json
  • apps/geolibre-desktop/src/i18n/locales/fr.json
  • apps/geolibre-desktop/src/i18n/locales/hi.json
  • apps/geolibre-desktop/src/i18n/locales/id.json
  • apps/geolibre-desktop/src/i18n/locales/it.json
  • apps/geolibre-desktop/src/i18n/locales/ja.json
  • apps/geolibre-desktop/src/i18n/locales/ka.json
  • apps/geolibre-desktop/src/i18n/locales/ko.json
  • apps/geolibre-desktop/src/i18n/locales/nl.json
  • apps/geolibre-desktop/src/i18n/locales/pt.json
  • apps/geolibre-desktop/src/i18n/locales/ru.json
  • apps/geolibre-desktop/src/i18n/locales/tr.json
  • apps/geolibre-desktop/src/i18n/locales/zh.json
  • apps/geolibre-desktop/src/lib/collab-client.ts
  • apps/geolibre-desktop/src/lib/share-fetch.ts
  • apps/geolibre-desktop/src/lib/share-geolibre.ts
  • docker/entrypoint.sh
  • tests/collab-protocol.test.ts
  • tests/share-fetch.test.ts
  • tests/share-geolibre.test.ts

- SettingsDialog.tsx: distinguish the two unusable share-host states. `disabled`
  keeps "no project sharing server configured"; `invalid` now gets a new
  `settings.env.tokenHostInvalid` string saying the configured address is not
  valid, so an operator who typo'd GEOLIBRE_SHARE_URL is not told they forgot to
  set it. Switched the component to `resolveShareHost()` so it has the status, and
  translated the new key into all 15 non-English catalogues.

Chose a settings-namespaced key over reusing `toolbar.item.shareHostUnavailable`
so a later edit to the menu string cannot silently change the Settings copy.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/geolibre-desktop/src/components/layout/SettingsDialog.tsx`:
- Around line 386-389: Align the settings link with the displayed host by
updating resolveShareHost to reject safe base URLs containing a path, preserving
only host-root URLs for shareBaseUrl and shareHostLabel; alternatively, make the
displayed host use the resolved settings URL rather than appending /settings to
a path-bearing base URL.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: d9edea98-7445-42f1-86d0-5643f2e5fcfe

📥 Commits

Reviewing files that changed from the base of the PR and between 8a879a4 and 6b2b465.

📒 Files selected for processing (17)
  • apps/geolibre-desktop/src/components/layout/SettingsDialog.tsx
  • apps/geolibre-desktop/src/i18n/locales/ar.json
  • apps/geolibre-desktop/src/i18n/locales/de.json
  • apps/geolibre-desktop/src/i18n/locales/en.json
  • apps/geolibre-desktop/src/i18n/locales/es.json
  • apps/geolibre-desktop/src/i18n/locales/fr.json
  • apps/geolibre-desktop/src/i18n/locales/hi.json
  • apps/geolibre-desktop/src/i18n/locales/id.json
  • apps/geolibre-desktop/src/i18n/locales/it.json
  • apps/geolibre-desktop/src/i18n/locales/ja.json
  • apps/geolibre-desktop/src/i18n/locales/ka.json
  • apps/geolibre-desktop/src/i18n/locales/ko.json
  • apps/geolibre-desktop/src/i18n/locales/nl.json
  • apps/geolibre-desktop/src/i18n/locales/pt.json
  • apps/geolibre-desktop/src/i18n/locales/ru.json
  • apps/geolibre-desktop/src/i18n/locales/tr.json
  • apps/geolibre-desktop/src/i18n/locales/zh.json

Comment thread apps/geolibre-desktop/src/components/layout/SettingsDialog.tsx
Comment thread apps/geolibre-desktop/src/components/layout/ShareProjectDialog.tsx Outdated
Comment thread docker/entrypoint.sh Outdated
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Code review

Bugs: None found. The null-instead-of-fallback redesign of resolveShareBaseUrl/resolveShareHost is applied consistently across all four call sites (ShareProjectDialog, SettingsDialog, ProjectGalleryDialog, useProjectFileActions), the gallery library (requireShareBase used in both fetchSharedProjects and fetchMyProjects), and the TopToolbar/ProjectMenu gating (shareHidden/shareBroken/shareAvailable). Traced every reachability path (menu items, command palette, dialog guards) and couldn't find a way to actually reach the "unreachable" defensive branches in ShareProjectDialog/ProjectGalleryDialog in the disabled/invalid states — the UI correctly hides or disables the entry points first. Confidence: high.

Security: None found. isSafeShareUrl (client) and service_url() (entrypoint) apply matching rules — HTTPS/WSS anywhere, plaintext only on exact-matched loopback hostnames (no prefix-check bypass), credentials rejected regardless of scheme, checked before the loopback shortcut so the guarantee holds universally. The origin (not just host) comparison fix in share-fetch.ts's requestOrigin correctly closes the scheme-conflation gap called out in its own comment. The "no silent fallback to the public host" behavior (the PR's core fix) is well-covered by tests. Confidence: high.

Performance: No concerns; this is UI/config plumbing with no hot paths touched.

Quality:

  • ShareProjectDialog.tsx:216,302 — the settingsUrl && (...) checks are dead code: settingsUrl is already guaranteed non-null by the early-return guard at line 180. Harmless, flagged inline. Confidence: high (it's correct as written, just redundant).
  • docker/entrypoint.sh:218-225 — the boot-log echo uses the raw, unstripped $GEOLIBRE_SHARE_URL/$GEOLIBRE_COLLAB_URL while the Python block validates a .strip()-ed copy, so a value with stray whitespace could produce a log line that reads inconsistently with what was actually written to the runtime config (e.g., " off " not matching the case pattern). Purely cosmetic, doesn't affect the applied config. Flagged inline. Confidence: low.
  • The 16-locale mechanical substitution (share.geolibre.app{{shareHost}}) was verified complete and consistent across all locale files for all four new/changed keys (errorNotConfigured, tokenUnavailable, tokenHostInvalid, shareHostUnavailable).

CLAUDE.md adherence: Consistent with repo conventions — i18n strings use t()/Trans with the locale catalogs as source of truth, no direct MapLibre mutation involved, and the change doesn't touch any of the mirrored-constant tripwires called out in CLAUDE.md. No violations found.

- share-geolibre.ts: `hostOf` now keeps a base URL's path, so a server hosted
  under a subpath (`https://example.test/geolibre`) is named the way the links
  built from the same base resolve. Previously the copy read `example.test` beside
  a link to `https://example.test/geolibre/settings` (CodeRabbit). Rejecting paths
  outright was the other option offered, but that would refuse a legitimate
  subpath deployment.
- ShareProjectDialog.tsx: drop the two `settingsUrl &&` checks, dead since the
  early guard added in 8a879a4 makes it non-null past that point (Claude).
- docker/entrypoint.sh: trim the logged values the way the Python block trims the
  validated ones, so `" off "` logs as disabled instead of printing a "sharing
  server" line that disagrees with the config actually written (Claude).

Added `shareHostLabel` tests covering the subpath case (with and without a
trailing slash) and the unusable-host fallback. Re-checked the boot log across
`off`/`" off "`/`oFF`/a padded URL, and `sh -n`/`bash -n` on the entrypoint.
Comment thread docker/entrypoint.sh
Comment thread apps/geolibre-desktop/src/components/layout/SettingsDialog.tsx Outdated
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Code review

Bugs

  • docker/entrypoint.sh validates GEOLIBRE_COLLAB_URL and writes it to the runtime config, but docker/nginx.conf's CSP connect-src only allowlists wss://collab.geolibre.app (no bare wss: scheme, unlike the bare https: that makes GEOLIBRE_SHARE_URL work for any host). A container started with a custom wss:// relay — exactly the example shown in the new docs/getting-started.md section — passes validation and appears available in the UI, then has its actual WebSocket connection silently blocked by CSP. The desktop build has the same limitation but it's documented in docs/collaboration.md; the equivalent gap for this PR's new Docker runtime path isn't. Medium-high confidence, verified directly against the CSP header and entrypoint script.

Security

  • None found. The URL validation (HTTPS-only except loopback, credential rejection, exact hostname matching to avoid localhost.evil.com-style bypasses) is consistently mirrored between the client TypeScript and the Python entrypoint validator, and origin (not just host) is now used to scope the Tauri native-fetch override, closing a scheme-confusion gap.

Performance

  • No issues of note; resolveShareHost()/shareHostLabel() are cheap URL parses recomputed per render, not worth memoizing.

Quality

  • SettingsDialog.tsx: the <span /> fallback for the tokenLink component is unreachable dead code, since the branch it's in only renders when a usable shareSettingsUrl is guaranteed to exist (low confidence — cosmetic only, flagged inline).
  • ShareProjectDialog's "not configured" fallback reuses gallery.errorNotConfigured copy verbatim rather than share-specific wording; harmless but slightly generic (low confidence, not flagged inline).
  • Hook ordering, gating logic (shareHidden/shareBroken), and the menu/command-palette/dialog consistency for the new ShareHostStatus states all check out correctly.

CLAUDE.md

  • No violations found: no edits to node_modules, translation catalogs got only the documented mechanical {{shareHost}} substitution, and Tauri/desktop CSP concerns are correctly called out as out of scope in the PR body and docs.

- docker/nginx.conf + entrypoint.sh: allow a self-hosted collaboration relay in
  the CSP. `connect-src` has a bare `https:` (so any share host works) but no bare
  `wss:` — only `wss://collab.geolibre.app` was listed. A container started with
  `-e GEOLIBRE_COLLAB_URL=wss://relay.example.org` (the example this PR added to
  the docs) passed validation, advertised collaboration in the UI, and then had its
  WebSocket blocked by the browser. The entrypoint now substitutes that relay's
  origin into a `__GEOLIBRE_COLLAB_CONNECT_SRC__` placeholder while rendering the
  nginx template, alongside the existing sidecar-token substitution.
- SettingsDialog.tsx: drop the unreachable `<span />` fallback for `tokenLink`;
  the branch requires `shareBaseUrl`, which `shareSettingsUrl` is derived from.
- docs: note that the CSP is handled automatically for the web/Docker path, in
  contrast to the desktop build's manual `connect-src` edit.

Verified the render step over four inputs: unset, a wss relay, a relay with a port
and path (origin only, path stripped — CSP source expressions take no path), and a
loopback ws relay. With nothing configured the emitted `connect-src` is
byte-identical to the one on `main`.
Comment thread docker/entrypoint.sh Outdated
Comment thread docker/entrypoint.sh
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Code review

Security

  • docker/entrypoint.sh (medium confidence): service_url() (used to validate GEOLIBRE_COLLAB_URL) only checks the URL's scheme and that netloc is non-empty — it never restricts which characters netloc may contain. GEOLIBRE_SHARE_URL is safe because it only ever flows into json.dump(), but the validated GEOLIBRE_COLLAB_URL value is also substituted unescaped into a double-quoted CSP string in nginx.conf.template (__GEOLIBRE_COLLAB_CONNECT_SRC__). Since urlsplit()'s netloc capture stops only at /, ?, or #, a value such as wss://x"; add_header X-Pwned "1 passes validation untouched and can break out of the quoted add_header Content-Security-Policy "..." directive, injecting arbitrary nginx config at boot. This is a real gap relative to this file's own established standard — the sibling GEOLIBRE_TRUSTED_PROXIES validator explicitly parses with ipaddress.ip_network() "so nothing else can be smuggled into the generated directives," and GEOLIBRE_AI_PROXY_URL rejects path/query/fragment. Left two inline comments with a suggested character-class check for service_url().

Bugs

  • None found. The core resolveShareHost/resolveCollabBaseUrl logic, the null-propagation through all four resolveShareBaseUrl() call sites, the gallery's not-configured error path, and the JS/Python validator parity (loopback hosts, credential rejection, "off" handling) all check out and are well covered by the added tests.

Performance

  • No issues; the new URL parsing/resolution is cheap and called at most once per render.

Quality

  • No issues; the i18n substitution is mechanical and complete (all 16 locales carry the 4 new/changed keys), and the new UI gating (menu hidden vs. disabled-with-reason vs. command-palette hidden) is internally consistent and documented.

CLAUDE.md

  • No violations noted — translatable strings use t()/Trans with {{shareHost}} interpolation as required, and the Docker/entrypoint conventions (validate-and-fail-loud) are followed, aside from the character-set gap noted above.

Harden the value that now reaches the nginx CSP.

- docker/entrypoint.sh: `service_url()` rejects a netloc containing anything
  outside `[A-Za-z0-9.\-:\[\]]`. `urlsplit()` puts everything up to the next `/`,
  `?` or `#` into netloc — quotes and semicolons included — and 2153097 started
  substituting that unescaped into the double-quoted `add_header
  Content-Security-Policy` value, so `wss://x"; add_header X-Pwned "1` could have
  injected nginx directives at boot.
- The check runs *before* the loopback early-return, which is load-bearing:
  `urlsplit('ws://localhost:8080"; …')` reports hostname `localhost`, so the
  payload would otherwise have matched the loopback allowlist and returned early.
  (Same ordering lesson as the credentials check.)
- The substitution site re-validates the composed origin with `re.fullmatch` and
  exits rather than emitting anything that is not a plain origin, so a future edit
  loosening the validator cannot reach the config.
- Added the missing `import re` to the nginx-render block.

Verified: four injection shapes (quote-escape, the loopback-shaped variant, a bare
`;`, and an embedded newline) are rejected by both the validator and the render
step, with no `X-Pwned` header reaching the output; `wss://relay.example.org`,
`:8443`, `ws://127.0.0.1:8787`, and `wss://[::1]:8443` still pass. With nothing
configured the emitted `connect-src` is still byte-identical to `main`.
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Code review

Reviewed the runtime share/collab host resolution (share-geolibre.ts, collab-client.ts, deployment-env.ts), the Docker entrypoint's service_url() validator and CSP substitution, the UI gating in TopToolbar/ProjectMenu/SettingsDialog/ShareProjectDialog/ProjectGalleryDialog, the CORS-exempt native fetch scoping in share-fetch.ts, and all 16 locale catalogs plus the test suite.

Bugs: None found. Traced every resolveShareBaseUrl() call site (4 of them) and confirmed each handles the new null return correctly; verified the "disabled"/"invalid" share-host states correctly gate both the Project menu item and the Gallery submenu item (Radix disabled prevents onSelect, so the dialogs' defensive null-checks are truly unreachable, not silently broken); confirmed the entrypoint's Python service_url() validator orders the credentials/charset checks before the loopback shortcut as the comments claim, and that the CSP placeholder substitution in docker/nginx.conf produces byte-identical output when unset.

Security: No issues. The credential-stripping, hostname-exact-match (vs. prefix) checks, and the netloc character-allowlist that guards against breaking out of the double-quoted CSP add_header value are all correctly implemented and covered by tests on both the TS and Python sides. requestOrigin()'s switch from host- to origin-based matching correctly closes the scheme-downgrade gap it documents (http:// vs https:// on the same host no longer both route through the CORS-exempt native fetch). Confidence: high.

Performance: No concerns. resolveShareHost()/resolveShareBaseUrl() are cheap (a window property read plus a URL parse) and are recomputed per render in a few components without memoization, but this is consistent with the existing collaboration.enabled pattern in the same files and not worth optimizing. Confidence: high.

Quality: Minor, low-confidence observation — the nginx CSP's connect-src retains a hardcoded wss://collab.geolibre.app allowance even when a deployment configures its own GEOLIBRE_COLLAB_URL, so a self-hosted, non-public deployment's CSP still trusts the public collab relay's origin unnecessarily. This isn't a vulnerability introduced by the PR (the app itself won't connect there), just slightly more permissive than strictly needed for a "self-hosted" story. Confidence: low — likely intentional to keep the public collab option always available and not worth blocking on.

CLAUDE.md: No violations. New user-facing strings use t()/Trans with {{shareHost}} interpolation and were mechanically propagated to all locale files (verified all 16 contain the new keys shareHostUnavailable, errorNotConfigured, tokenUnavailable, tokenHostInvalid); no RTL-unsafe physical Tailwind classes were introduced in the touched components.

@giswqs
giswqs merged commit 285b7e7 into main Aug 4, 2026
24 checks passed
@giswqs
giswqs deleted the feat/runtime-share-host branch August 4, 2026 01:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Point the web container at a self-hosted share and collaboration server at runtime, and never silently fall back to the public host

2 participants