Skip to content

Add typed embed SDK and viewer preset - #1615

Open
giswqs wants to merge 18 commits into
mainfrom
fix/issue-1525-embed-sdk
Open

Add typed embed SDK and viewer preset#1615
giswqs wants to merge 18 commits into
mainfrom
fix/issue-1525-embed-sdk

Conversation

@giswqs

@giswqs giswqs commented Aug 1, 2026

Copy link
Copy Markdown
Member

Summary

  • add a dependency-free, publishable @geolibre/embed client with typed commands and events
  • extend the embed protocol to v2 with layer visibility/listing, filters, viewport queries, runtime layer adds, and PNG export while retaining v1 handling
  • add a read-only layout=viewer preset with navigation controls and a visibility-only layer legend
  • publish the SDK from GeoLibre releases and document the new client and protocol

Verification

  • npm run build
  • node --import tsx --test tests/embed-api.test.ts tests/layout-options.test.ts
  • pre-commit run --files <changed files>
  • Playwright verification of the viewer preset in light and dark themes
  • Playwright verification from a real parent iframe host of v2 ready, listLayers, and correlated ack/result messages

Fixes #1525

Summary by CodeRabbit

  • New Features
    • Added a read-only viewer layout with simplified controls, hidden editing panels, and layer visibility management.
    • Added an iframe embedding client for project, view, feature, layer, filter, viewport, and image operations.
    • Added runtime commands for managing layers, applying filters, retrieving viewports, and exporting map images.
  • Compatibility
    • Added protocol version 2 support while maintaining version 1 compatibility.
  • Documentation
    • Updated embedding guidance with viewer mode, client usage, protocol details, validation, and command examples.

Copilot AI review requested due to automatic review settings August 1, 2026 14:42
@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

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

Added a publishable @geolibre/embed SDK, protocol v2 runtime commands with v1 compatibility, a read-only viewer layout, transient embed filters, documentation, tests, and npm publishing automation.

Changes

Embed SDK and viewer experience

Layer / File(s) Summary
Publishable embed SDK
.github/workflows/publish-embed.yml, packages/embed/*, apps/geolibre-desktop/package.json, apps/geolibre-desktop/vite.config.ts, tests/embed-client.test.ts
Added typed iframe connection APIs, origin checks, readiness handling, package configuration, desktop wiring, connection tests, and release publishing.
Protocol v2 runtime commands
apps/geolibre-desktop/src/lib/embed-api.ts, apps/geolibre-desktop/src/hooks/useEmbedApi.ts, tests/embed-api.test.ts
Added version negotiation, command results, layer visibility and listing, filters, viewport retrieval, runtime layer creation, image export, validation, and v1 compatibility tests.
Read-only viewer layout
apps/geolibre-desktop/src/hooks/useLayoutOptions.ts, apps/geolibre-desktop/src/components/layout/*, apps/geolibre-desktop/src/components/panels/ViewerLayerPanel.tsx, tests/layout-options.test.ts
Added layout=viewer, restricted authoring and plugin UI, blocked file drops, deactivated the geometry editor, and added layer visibility controls.
Transient embed filtering
packages/core/src/types.ts, packages/core/src/project.ts, packages/map/src/layer-sync.ts
Added embedFilter support to rendering and derived geometry checks, then removed it before project persistence.
Embedding documentation
docs/user-guide/embedding.md
Documented viewer layouts, the typed SDK, protocol v2 commands, acknowledgement results, validation errors, and v1 compatibility.

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

Sequence Diagram(s)

sequenceDiagram
  participant HostPage
  participant GeoLibreEmbedClient
  participant useEmbedApi
  participant LayerStore
  participant MapCanvas
  HostPage->>GeoLibreEmbedClient: connect to iframe
  GeoLibreEmbedClient->>useEmbedApi: send versioned command
  useEmbedApi->>LayerStore: update or list layers
  useEmbedApi->>MapCanvas: read viewport or export image
  LayerStore-->>useEmbedApi: return command result
  MapCanvas-->>useEmbedApi: return viewport or image
  useEmbedApi-->>GeoLibreEmbedClient: send acknowledgement with result
  GeoLibreEmbedClient-->>HostPage: resolve request or dispatch event
Loading

Possibly related PRs

Poem

A rabbit checks each layer’s view,
Sends typed messages, clean and true.
Viewer tools stay out of sight,
V2 answers land just right.
Filters fade before saves begin—
The embed burrow ships within.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 62.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 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 changes: a typed embed SDK and a viewer preset.
Linked Issues check ✅ Passed The changes implement the typed SDK, v2 protocol features with v1 support, and the read-only viewer preset described in issue [#1525].
Out of Scope Changes check ✅ Passed The workflow, package setup, implementation, documentation, and tests directly support the objectives in issue [#1525].
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/issue-1525-embed-sdk

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 1, 2026

Copy link
Copy Markdown
Contributor

🔍 GitHub Pages PR preview

Item Value
Site https://opengeos.org/pages-preview/GeoLibre/pr-1615/
Demo app https://opengeos.org/pages-preview/GeoLibre/pr-1615/demo/
Commit 5b3099e

@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

🔍 Cloudflare PR preview

Item Value
Site https://e85a7d46.geolibre-preview.pages.dev
Demo app https://e85a7d46.geolibre-preview.pages.dev/demo/
Commit 5b3099e

Comment thread apps/geolibre-desktop/src/hooks/useEmbedApi.ts Outdated
Comment thread apps/geolibre-desktop/src/hooks/useEmbedApi.ts Outdated
Comment thread apps/geolibre-desktop/src/hooks/useEmbedApi.ts Outdated
Comment thread apps/geolibre-desktop/src/lib/embed-api.ts
@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Code review

Bugs

  • useEmbedApi.ts:369-374 — after the initial double ready burst, hostVersion resets to 2 and only changes again when an inbound request is received. A v1 host that only listens for events (never sends a command) gets all subsequent events (selectionChanged, viewChanged, projectLoaded, toolCompleted) emitted as v: 2, which a strict message.v !== 1 filter drops — contradicting the PR's own backward-compatibility claim in docs/user-guide/embedding.md. Confidence: medium-high.
  • useEmbedApi.ts:259hostVersion is a shared mutable variable rewritten on every inbound message rather than learned once (unlike hostOrigin). Because ack fires asynchronously after runCommand resolves, two rapid requests declaring different v values can cause an earlier request's ack to be sent with the later request's version, mismatching the envelope the caller expects. Confidence: medium.
  • useEmbedApi.ts:225-237addLayer's spec.type is validated only as a non-empty string, then force-cast to GeoLibreLayer["type"] and pushed into the store unchecked. layer-sync.ts renders layers via non-exhaustive if checks with no fallback, so an unrecognized type is silently added to the store (and shows up in listLayers) but never rendered — while the addLayer ack reports success. Confidence: medium.

Security

  • No issues found. The embed API's trust boundary (origin allowlist, isFetchableUrl scheme restriction, structured-clone-safe payloads) is unchanged in shape; new commands (setFilter, addLayer, etc.) operate within the same already-trusted host-origin model as existing commands like loadProject.

Performance

  • No notable issues; the new commands are simple store reads/writes and the PNG export reuses the existing captureMapImage helper.

Quality

  • embed-api.ts:20SUPPORTED_EMBED_API_VERSIONS is exported but unused; parseEmbedRequest hardcodes the version check instead of consulting it, so the two can silently drift. Confidence: low.

CLAUDE.md

  • No violations found — new user-facing strings use t()/existing i18n keys, RTL-safe logical Tailwind classes are used in ViewerLayerPanel.tsx, and no node_modules or generated-catalog files were hand-edited.

- Preserve listen-only v1 hosts and bind acknowledgements to request versions.
- Reject unsupported runtime layer types before adding them.
- Use the shared supported-version list during parsing.

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

This PR introduces a publishable, dependency-free @geolibre/embed SDK for GeoLibre’s iframe postMessage API, bumps the embed protocol to v2 (while aiming to keep v1 compatibility), and adds a new read-only layout=viewer preset for embedded viewing experiences.

Changes:

  • Add @geolibre/embed workspace package exporting typed commands/events and a connect() client.
  • Extend the app-side embed protocol parsing/handling to v2 (layer visibility/listing, filters, viewport query, runtime layer add, PNG export) and update docs/tests accordingly.
  • Add a viewer layout preset (read-only chrome, visibility-only legend) and supporting UI wiring.

Reviewed changes

Copilot reviewed 18 out of 19 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
tests/layout-options.test.ts Adds coverage for the new layout=viewer preset option mapping.
tests/embed-api.test.ts Extends protocol parsing tests to cover v2 commands and version handling.
packages/map/src/layer-sync.ts Applies the new transient embedFilter to MapLibre feature filters during sync.
packages/embed/tsconfig.json Adds TS config for the new @geolibre/embed package.
packages/embed/tsconfig.build.json Adds build TS config to emit JS + .d.ts into dist/.
packages/embed/src/index.ts Implements the typed SDK surface (connect, commands, event typing).
packages/embed/package.json Defines the publishable @geolibre/embed package metadata and build scripts.
packages/core/src/types.ts Adds embedFilter?: unknown[] to GeoLibreLayer for embed-driven filtering.
packages/core/src/project.ts Strips embedFilter on save so it remains transient/non-persistent.
package-lock.json Wires @geolibre/embed into the workspace lockfile.
docs/user-guide/embedding.md Documents layout=viewer, protocol v2, and recommends the new SDK usage.
apps/geolibre-desktop/src/lib/embed-api.ts Updates protocol constants/types and parsing to accept v1 + v2 envelopes and new commands.
apps/geolibre-desktop/src/hooks/useLayoutOptions.ts Introduces viewer layout option behavior (read-only chrome flags).
apps/geolibre-desktop/src/hooks/useEmbedApi.ts Implements v2 command execution, ack.result, export image, runtime add layer, and v1/v2 event versioning.
apps/geolibre-desktop/src/components/panels/ViewerLayerPanel.tsx Adds a minimal, read-only visibility legend for viewer mode.
apps/geolibre-desktop/src/components/layout/TopToolbar.tsx Hides authoring/management menus when viewer is enabled.
apps/geolibre-desktop/src/components/layout/DesktopShell.tsx Wires viewer mode into shell layout (panel suppression + viewer legend).
apps/geolibre-desktop/package.json Ensures the embed SDK is built before desktop dev/build steps.
.github/workflows/publish-embed.yml Adds a release-triggered workflow to publish @geolibre/embed to npm.
Suppressed comments (1)

apps/geolibre-desktop/src/hooks/useEmbedApi.ts:374

  • After emitting both v2 and v1 ready, hostVersion is set back to 2. A v1 host that only listens for events (and never sends a command) will then receive all subsequent events (projectLoaded, viewChanged, etc.) as v2 and ignore them, breaking existing v1 integrations.
        return;
      }
      // Within the throttle window: schedule the trailing edge so the host still
      // gets the final position of a drag.
      if (trailingTimer !== null) return;

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

Comment thread apps/geolibre-desktop/src/hooks/useEmbedApi.ts Outdated
Comment thread apps/geolibre-desktop/src/hooks/useEmbedApi.ts Outdated
Comment thread apps/geolibre-desktop/src/components/panels/ViewerLayerPanel.tsx Outdated
Comment thread packages/embed/src/index.ts
Comment thread packages/embed/src/index.ts
- Honor addLayer insertion targets and expose keyboard focus in viewer layers.
- Align nullable viewport bounds with runtime events.
- Cover client origin checks, correlation, disconnects, and timeouts.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 11

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/components/layout/TopToolbar.tsx (1)

1500-1740: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Gate all viewer mutation paths.

Viewer mode hides menus and panels, but it does not restrict the underlying actions. The full command list still reaches useGlobalShortcuts and CommandPalette. The Controls menu also remains available. A viewer can open the command palette or use shortcuts to create, load, save, process, or add project data. The shell also keeps drag-and-drop import handlers active.

  • apps/geolibre-desktop/src/components/layout/TopToolbar.tsx#L1500-L1740: create a viewer allowlist for commands, shortcuts, and Controls actions. Keep only supported viewer actions such as navigation, basemap changes, place search, identify, and visibility controls.
  • apps/geolibre-desktop/src/components/layout/DesktopShell.tsx#L1955-L1963: disable file drag-and-drop imports when layoutOptions.viewer is true.
🤖 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/components/layout/TopToolbar.tsx` around lines 1500
- 1740, Gate all viewer mutation paths: in
apps/geolibre-desktop/src/components/layout/TopToolbar.tsx lines 1500-1740,
define and apply a viewer allowlist for the command list, useGlobalShortcuts,
CommandPalette, and ControlsMenu so viewers retain only navigation, basemap,
place search, identify, and visibility actions;
apps/geolibre-desktop/src/components/layout/DesktopShell.tsx lines 1955-1963
must disable drag-and-drop file imports when layoutOptions.viewer is true.
🤖 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 @.github/workflows/publish-embed.yml:
- Line 15: Update the actions/checkout@v6 step in the publish workflow to set
persist-credentials to false, ensuring checkout does not leave Git credentials
available to subsequent npm ci install scripts while preserving the existing npm
publish authentication.
- Around line 7-9: Move id-token: write from the workflow-level permissions
block into the publish job’s permissions block, while keeping contents: read at
the workflow level. Ensure only the publish job requests OIDC token access.

In `@apps/geolibre-desktop/src/components/layout/DesktopShell.tsx`:
- Around line 692-698: Move the geo-editor deactivation logic from the
standalone viewer effect into the project-restoration flow after
restoreProjectState completes, and retain the layoutOptions.viewer guard there.
Ensure the restoration path includes viewer mode so restored plugin state cannot
activate maplibre-gl-geo-editor in the read-only preset.

In `@apps/geolibre-desktop/src/components/panels/ViewerLayerPanel.tsx`:
- Around line 19-35: Add a visible focus-within ring or outline to the label
wrapping each checkbox in the layer control, while preserving the existing
styling and keyboard behavior. Update the label’s className in the layer
rendering block so focus on the sr-only input makes the active control visually
identifiable.

In `@apps/geolibre-desktop/src/lib/embed-api.ts`:
- Line 20: Update the version validation in parseEmbedRequest to check
membership in SUPPORTED_EMBED_API_VERSIONS rather than comparing against
hardcoded 1 and EMBED_API_VERSION values. Preserve rejection of unsupported
versions while keeping the check automatically aligned with future additions to
the supported-version constant.
- Around line 350-368: Extend the validation in the addLayer case to verify
optional spec fields before the unchecked AddLayerSpec cast: visible must be
boolean when present, opacity must be a finite number when present, style and
metadata must be records when present, and beforeId must be a string when
present. Also restrict type to the supported GeoLibre layer type values, while
preserving acceptance of omitted optional fields and the existing failure
response.

In `@docs/user-guide/embedding.md`:
- Around line 196-199: Update the message handler around the event.origin and
message.source checks to also require event.source === frame.contentWindow
before processing the payload, ensuring only messages from the intended iframe
are accepted.
- Around line 93-94: Update the connect call in the embedding example to narrow
document.querySelector("iframe") to a non-null HTMLIFrameElement before passing
it to connect, preserving the existing origin configuration and ensuring the
TypeScript example type-checks.

In `@packages/embed/src/index.ts`:
- Around line 110-117: Update send() to apply a per-request timeout after
registering each request in pending. When the timeout expires, remove that
requestId from pending and reject its promise with an appropriate timeout error;
ensure successful acknowledgements and disconnect cleanup cancel or otherwise
prevent the timeout from acting afterward.

In `@packages/map/src/layer-sync.ts`:
- Around line 179-181: Update the layer synchronization flow around the
embedFilter handling to apply filters before cluster inputs are created: use a
pre-filtered feature collection for the clustered GeoJSON/source path, and
filter the points supplied to Supercluster.load() on the tiled path. Keep
render-layer filtering for display, but ensure filtered features never enter
either clustering input.

In `@tests/embed-api.test.ts`:
- Around line 129-184: Add failure-path assertions to the “parseEmbedRequest: v2
commands” tests for setLayerVisibility, setFilter, and addLayer. Verify parsing
rejects a non-boolean visible value, an expression that is neither an array nor
null, and an addLayer spec with a missing or incorrectly typed optional field,
matching the validation behavior in parseEmbedRequest.

---

Outside diff comments:
In `@apps/geolibre-desktop/src/components/layout/TopToolbar.tsx`:
- Around line 1500-1740: Gate all viewer mutation paths: in
apps/geolibre-desktop/src/components/layout/TopToolbar.tsx lines 1500-1740,
define and apply a viewer allowlist for the command list, useGlobalShortcuts,
CommandPalette, and ControlsMenu so viewers retain only navigation, basemap,
place search, identify, and visibility actions;
apps/geolibre-desktop/src/components/layout/DesktopShell.tsx lines 1955-1963
must disable drag-and-drop file imports when layoutOptions.viewer is true.
🪄 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: 74f9539a-97be-4cf6-9782-fbbbbedbdebe

📥 Commits

Reviewing files that changed from the base of the PR and between c93327f and 2fe7fe4.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (18)
  • .github/workflows/publish-embed.yml
  • apps/geolibre-desktop/package.json
  • apps/geolibre-desktop/src/components/layout/DesktopShell.tsx
  • apps/geolibre-desktop/src/components/layout/TopToolbar.tsx
  • apps/geolibre-desktop/src/components/panels/ViewerLayerPanel.tsx
  • apps/geolibre-desktop/src/hooks/useEmbedApi.ts
  • apps/geolibre-desktop/src/hooks/useLayoutOptions.ts
  • apps/geolibre-desktop/src/lib/embed-api.ts
  • docs/user-guide/embedding.md
  • packages/core/src/project.ts
  • packages/core/src/types.ts
  • packages/embed/package.json
  • packages/embed/src/index.ts
  • packages/embed/tsconfig.build.json
  • packages/embed/tsconfig.json
  • packages/map/src/layer-sync.ts
  • tests/embed-api.test.ts
  • tests/layout-options.test.ts

Comment thread .github/workflows/publish-embed.yml Outdated
Comment thread .github/workflows/publish-embed.yml Outdated
Comment thread apps/geolibre-desktop/src/components/layout/DesktopShell.tsx Outdated
Comment thread apps/geolibre-desktop/src/components/panels/ViewerLayerPanel.tsx Outdated
Comment thread apps/geolibre-desktop/src/lib/embed-api.ts
Comment thread docs/user-guide/embedding.md
Comment thread docs/user-guide/embedding.md
Comment thread packages/embed/src/index.ts
Comment thread packages/map/src/layer-sync.ts
Comment thread tests/embed-api.test.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 `@tests/embed-client.test.ts`:
- Around line 42-52: Extend the test around connect and the ready event to add
negative cases with a different event.source and a different data.source before
the valid event. Assert after each case that connect remains unsettled, then
keep the exact-origin event and final receive("ready", {}) as the successful
resolution path.
🪄 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: f7eb1866-e648-4880-bc5d-a68bf0900ec8

📥 Commits

Reviewing files that changed from the base of the PR and between 2fe7fe4 and 4a23aec.

📒 Files selected for processing (5)
  • apps/geolibre-desktop/src/components/panels/ViewerLayerPanel.tsx
  • apps/geolibre-desktop/src/hooks/useEmbedApi.ts
  • apps/geolibre-desktop/src/lib/embed-api.ts
  • packages/embed/src/index.ts
  • tests/embed-client.test.ts

Comment thread tests/embed-client.test.ts
Comment thread apps/geolibre-desktop/src/hooks/useLayoutOptions.ts Outdated
Comment thread .github/workflows/publish-embed.yml Outdated
Comment thread apps/geolibre-desktop/package.json
Comment thread apps/geolibre-desktop/src/hooks/useEmbedApi.ts Outdated
Comment thread apps/geolibre-desktop/src/components/panels/ViewerLayerPanel.tsx
@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Code review

Bugs

  • useLayoutOptions.ts:73layerPanelVisible isn't forced true for layout=viewer the way stylePanelVisible/attributePanelVisible are. If a browser's persisted desktop setting has the Layers panel toggled off, a layout=viewer embed loads with no Layers panel and no way to restore it, since the only toggle (SettingsDialog) is hidden in viewer mode. Confidence: medium-high.

Security

  • No issues found. The new addLayer/setFilter/exportImage commands are correctly gated behind the existing origin allowlist (GEOLIBRE_EMBED_ORIGINS), and MapLibre filter expressions are inert data, not executed code. addLayer's loose validation of source/style/geojson is a correctness/robustness concern rather than a security one, given the documented trust model treats allowlisted origins as trusted.

Performance

  • No meaningful issues found; the dual-version broadcast in emit() (v1+v2) is a small, bounded fan-out only while the host hasn't spoken yet.

Quality

  • .github/workflows/publish-embed.yml — publishes @geolibre/embed on every GitHub release with no version-diff guard or path filter; will fail noisily on releases that don't bump the embed package's version. Confidence: medium.
  • apps/geolibre-desktop/package.json predev/prebuild — @geolibre/embed is built once via tsc before vite starts (unlike sibling @geolibre/* packages, which resolve straight to src/), so edits to packages/embed/src/index.ts aren't picked up by Vite's watcher during npm run dev. Confidence: medium.
  • useEmbedApi.ts — the new runCommand branches (addLayer, setFilter, setLayerVisibility, listLayers, getViewport, exportImage) have no direct unit tests; only parseEmbedRequest's envelope parsing is covered. addLayer also doesn't validate that source/geojson match the declared type. Confidence: low-medium.
  • ViewerLayerPanel.tsx — renders layers as a flat list, ignoring groupId/layer groups that LayerPanel honors; may be intentional for the read-only preset. Confidence: low.
  • packages/map/src/layer-sync.ts — the withFeatureFilters/externalFeatureFilterExtras docstrings weren't updated to mention the new embedFilter alongside timeFilter and the rule-based filter (not commented inline, minor).

CLAUDE.md

  • No violations found: new user-facing strings reuse an existing i18n key (sharedRail.layers), no node_modules edits, no untracked new external tile/map hosts, and the packages/embed addition doesn't touch any of the documented "mirrored constant" contracts (MAX_VECTOR_PMTILES_ZOOM, MAX_VECTOR_BYTES, MAP_PANEL_SELECTOR, propertySpecFor, Whitebox distance params).

- Harden release permissions and skip already-published SDK versions.
- Validate optional layer fields and add command response timeouts.
- Keep viewer layers deterministic and SDK source hot-reloadable.
- Expand protocol filtering and timeout tests.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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/components/layout/TopToolbar.tsx (1)

1500-1740: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Block authoring commands in viewer mode.

These guards hide menu triggers only. commands still contains project.*, add.*, proc.*, and plugin commands. useGlobalShortcuts registers them, and Help can open the mounted CommandPalette. A viewer user can therefore open authoring flows through keyboard shortcuts or the palette.

Use an explicit viewer-safe command allowlist for both useGlobalShortcuts and CommandPalette. As per PR objectives, layout=viewer must hide authoring UI.

🤖 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/components/layout/TopToolbar.tsx` around lines 1500
- 1740, Viewer mode currently hides menu triggers but still exposes authoring
commands through useGlobalShortcuts and CommandPalette. Add and apply an
explicit viewer-safe command allowlist to both command consumers, excluding
project, add-data, processing, and plugin authoring commands while preserving
view, help, and other viewer-safe actions; ensure layout=viewer cannot invoke
authoring flows via shortcuts or the palette.
♻️ Duplicate comments (1)
packages/map/src/layer-sync.ts (1)

179-181: ⚠️ Potential issue | 🟠 Major

Apply embedFilter before building clustered data.

withFeatureFilters changes only render-layer filters. The clustered GeoJSON source still receives the unfiltered layer.geojson!, so MapLibre builds clusters from features that embedFilter later hides. Cluster bubbles and point_count can therefore include excluded features. Pre-filter the clustered GeoJSON data and the tiled/Supercluster input, while retaining the render filter for unclustered features.

#!/bin/bash
set -euo pipefail

rg -n -C 10 \
  'embedFilter|cluster: true|setData\(layer\.geojson|supercluster|point_count' \
  packages/map/src
🤖 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 `@packages/map/src/layer-sync.ts` around lines 179 - 181, Apply
layer.embedFilter to the feature collection before constructing clustered
GeoJSON or passing data to Supercluster, so excluded features cannot affect
clusters or point_count. Update the clustering/data-preparation flow near the
existing embedFilter handling, while retaining the render-layer filter used for
unclustered features and leaving non-clustered behavior unchanged.
🤖 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/hooks/useLayoutOptions.ts`:
- Around line 73-78: The layer panel visibility logic in useLayoutOptions must
force the panel visible in viewer mode unless panelsHidden is true, overriding
persisted layerPanelVisible settings; update tests/layout-options.test.ts lines
91-102 to cover viewer mode with layerPanelVisible false and assert the panel
remains enabled.

---

Outside diff comments:
In `@apps/geolibre-desktop/src/components/layout/TopToolbar.tsx`:
- Around line 1500-1740: Viewer mode currently hides menu triggers but still
exposes authoring commands through useGlobalShortcuts and CommandPalette. Add
and apply an explicit viewer-safe command allowlist to both command consumers,
excluding project, add-data, processing, and plugin authoring commands while
preserving view, help, and other viewer-safe actions; ensure layout=viewer
cannot invoke authoring flows via shortcuts or the palette.

---

Duplicate comments:
In `@packages/map/src/layer-sync.ts`:
- Around line 179-181: Apply layer.embedFilter to the feature collection before
constructing clustered GeoJSON or passing data to Supercluster, so excluded
features cannot affect clusters or point_count. Update the
clustering/data-preparation flow near the existing embedFilter handling, while
retaining the render-layer filter used for unclustered features and leaving
non-clustered behavior unchanged.
🪄 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: 6d2fab38-080f-4662-a6b4-aa5824ba7bb4

📥 Commits

Reviewing files that changed from the base of the PR and between c93327f and 4a23aec.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (19)
  • .github/workflows/publish-embed.yml
  • apps/geolibre-desktop/package.json
  • apps/geolibre-desktop/src/components/layout/DesktopShell.tsx
  • apps/geolibre-desktop/src/components/layout/TopToolbar.tsx
  • apps/geolibre-desktop/src/components/panels/ViewerLayerPanel.tsx
  • apps/geolibre-desktop/src/hooks/useEmbedApi.ts
  • apps/geolibre-desktop/src/hooks/useLayoutOptions.ts
  • apps/geolibre-desktop/src/lib/embed-api.ts
  • docs/user-guide/embedding.md
  • packages/core/src/project.ts
  • packages/core/src/types.ts
  • packages/embed/package.json
  • packages/embed/src/index.ts
  • packages/embed/tsconfig.build.json
  • packages/embed/tsconfig.json
  • packages/map/src/layer-sync.ts
  • tests/embed-api.test.ts
  • tests/embed-client.test.ts
  • tests/layout-options.test.ts

Comment thread apps/geolibre-desktop/src/hooks/useLayoutOptions.ts Outdated
Comment thread apps/geolibre-desktop/src/components/layout/TopToolbar.tsx
Comment thread packages/embed/src/index.ts Outdated
Comment thread apps/geolibre-desktop/package.json Outdated
Comment thread packages/map/src/layer-sync.ts
@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Code review

Bugs

  • layout=viewer's "read-only" contract is not actually enforced: useGlobalShortcuts and the always-mounted CommandPalette in TopToolbar.tsx are never gated on viewer, so their full commands registry (New Project, Open File, Save/Save As, Add Data, Processing tools, Manage Plugins) stays reachable via Ctrl/Cmd+N/O/S and Ctrl/Cmd+K even though the corresponding menu buttons are hidden. High confidence.
  • connect() in packages/embed/src/index.ts is typed to always return a Promise, but a malformed origin option makes validOrigin() throw synchronously before any promise is created, inconsistent with the adjacent !target branch which does Promise.reject(...). Medium confidence.

Security

  • None beyond the read-only-bypass bug above, which itself has a security-relevant flavor (an "authoring hidden" embed preset intended for untrusted third-party pages still permits full project mutation from local keyboard input).

Performance

  • None found.

Quality

  • apps/geolibre-desktop/package.json pins @geolibre/embed to ^2.4.0 while every other internal @geolibre/* workspace dependency uses "*", an unnecessary inconsistency. Low confidence.
  • withFeatureFilters's docstring in packages/map/src/layer-sync.ts wasn't updated to mention the new embedFilter source it now combines. Low confidence.

CLAUDE.md

  • No violations found: the MAX_VECTOR_PMTILES_ZOOM/MAX_VECTOR_BYTES/menu-catalog/lockfile rules aren't implicated by this diff, and the new UI strings correctly use t() and existing i18n keys (sharedRail.layers), with no physical ml-/left- Tailwind utilities introduced.

The embedFilter plumbing itself (types, project.ts save-stripping, all three layer-sync.ts filter-application sites) is consistent and complete — no gaps found there. The @geolibre/embed client's timeout/disconnect/correlation logic and the parseEmbedRequest v1/v2 version handling both check out correctly against their test coverage.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 (1)
docs/user-guide/embedding.md (1)

172-179: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Broaden the documented addLayer source requirements.

addLayer currently accepts a non-empty source.url, non-empty source.data as a URL string, non-empty source.tiles[], or metadata.originalUrl for any layer type, plus inline object GeoJSON for geojson/deckgl-viz. The current text omits source.data and metadata.originalUrl, so host integrations can reject valid re-fetchable source shapes.

🤖 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 `@docs/user-guide/embedding.md` around lines 172 - 179, Update the addLayer
source requirements in the documentation to include non-empty source.data URL
strings and metadata.originalUrl for all layer types, while retaining the
existing source.url, non-empty source.tiles, and inline GeoJSON rules for
geojson/deckgl-viz layers.
🤖 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 `@docs/user-guide/embedding.md`:
- Around line 172-179: Update the addLayer source requirements in the
documentation to include non-empty source.data URL strings and
metadata.originalUrl for all layer types, while retaining the existing
source.url, non-empty source.tiles, and inline GeoJSON rules for
geojson/deckgl-viz layers.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 19cca261-e2fe-4877-b62e-a60bc067ac7b

📥 Commits

Reviewing files that changed from the base of the PR and between 330dc43 and e883860.

📒 Files selected for processing (6)
  • apps/geolibre-desktop/src/components/layout/DesktopShell.tsx
  • apps/geolibre-desktop/src/components/layout/TopToolbar.tsx
  • apps/geolibre-desktop/src/hooks/useGlobalShortcuts.ts
  • apps/geolibre-desktop/src/lib/embed-api.ts
  • docs/user-guide/embedding.md
  • tests/embed-api.test.ts

Comment thread apps/geolibre-desktop/src/components/layout/DesktopShell.tsx Outdated
@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Already covered by the inline comment posted above. That completes the review.

Code review

Bugs

  • apps/geolibre-desktop/src/components/layout/DesktopShell.tsx (lines 699‑713): enforceViewerPlugins deactivates only the geo-editor plugin (GEO_EDITOR_PLUGIN_ID). restoreProjectState re-activates any plugin listed in a loaded project's activePluginIds, and the built-in Annotations plugin (maplibre-gl-annotations) has the same "paints an editing surface onto the map" problem — its toolbar (text/pin/sticky-note/arrow/rectangle/ellipse/freehand) is added via app.addMapControl, which bypasses the pluginContentEl.replaceChildren() gating used for the side-panel dock. A project saved with Annotations (or any other map-control-painting plugin) active will restore fully functional under layout=viewer, contradicting the "an embed cannot be steered into authoring by … a project file" guarantee documented for this feature. Confidence: medium — verified via code paths (project restore → plugin activation → addMapControl), not via a live run. Posted inline with detail.

Security

  • Same finding as above also has a security angle: the layout=viewer preset is documented/marketed as read-only, and any authoring-capable plugin not explicitly deactivated undermines that boundary for whoever relies on the read-only guarantee (e.g., embedding a project from an untrusted/third-party author). Confidence: medium.
  • The rest of the new embed-API surface (setLayerVisibility, setFilter, getViewport, addLayer, exportImage in apps/geolibre-desktop/src/lib/embed-api.ts) looks carefully hardened: origin allow-listing happens before any parsing, setFilter expressions are compiled through the real MapLibre style-spec validator before being stored (so a bad filter fails the ack instead of silently no-oping), and addLayer both requires a renderable source (hasRenderableEmbedSource) and blocks javascript:/vbscript:/data:/file:/blob: schemes on every URL-bearing field, including after stripping tab/newline/CR the way a browser's URL parser would. No issues found here (confidence: high, given test coverage matches the implementation).

Performance

  • Nothing notable. setFilter's JSON.stringify→JSON.parse round trip through validateMapExpression is slightly redundant but negligible and deliberately reuses the same validator the Expression Builder uses.

Quality

  • ViewerLayerPanel.tsx's group/indent logic (walking groupIdparentId with cycle protection, then computing header indentation from depth - headers.length + index) is non-obvious but checks out correctly under manual tracing of nested/sibling group cases.
  • The rest of the viewer-preset wiring (useLayoutOptions.ts, TopToolbar.tsx, ControlsMenu.tsx, HelpMenu.tsx, drag-and-drop gating) is consistent and well-covered by the added tests.

CLAUDE.md

  • i18n: the new ViewerLayerPanel.tsx uses t() for its one user-facing string and reuses an existing key; no hardcoded UI strings introduced.
  • RTL: uses logical Tailwind utilities (border-e) and marginInlineStart rather than physical border-r/margin-left, consistent with the RTL requirement.
  • No violations found regarding the other CLAUDE.md mirrored-constant rules (none of the touched dependencies are the ones tracked there).

- The viewer preset's plugin guard now covers the Annotations plugin as well
  as the geo-editor. Annotations' activate() adds a full drawing toolbar via
  addMapControl, which lives on the map rather than in the React chrome the
  preset hides, so a project that saved it active handed a read-only embed a
  drawing toolbar on restore.
- The two ids live in a new packages/plugins/src/viewer-plugins.ts as
  VIEWER_BLOCKED_PLUGIN_IDS, next to the plugins they name, so whoever adds
  the next authoring plugin is already in that package. Kept as a blocklist,
  not an allowlist: the viewer's job is to show the project as saved, so the
  display plugins a project relies on (layer control, basemaps, time slider,
  components) must keep working.
- Exported ANNOTATIONS_PLUGIN_ID alongside the plugin, mirroring
  GEO_EDITOR_PLUGIN_ID, and added tests pinning the list to the ids the
  plugins actually register under — a rename would otherwise silently stop
  blocking them.
Comment thread apps/geolibre-desktop/src/hooks/useEmbedApi.ts
Comment thread apps/geolibre-desktop/src/components/layout/DesktopShell.tsx
Comment thread packages/map/src/layer-sync.ts
@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

All three inline comments posted. Now the final summary.

Code review

Bugs

  • apps/geolibre-desktop/src/hooks/useEmbedApi.ts:220setFilter stores the raw, structured-cloned command.expression, but filterExpressionProblem (embed-api.ts) validates a JSON round-tripped copy of it. Values that don't survive JSON.stringify/parse identically (undefined in the array, Infinity/NaN) would validate against a sanitized stand-in, get acked ok: true, and then have the actual unvalidated value pushed into layer.embedFilter. Medium-low confidence; narrow edge case.
  • apps/geolibre-desktop/src/components/layout/DesktopShell.tsx:1151enforceViewerPlugins() re-asserts the viewer plugin blocklist synchronously right after restoreProjectState, but the async handleUrlParameters call below it (which can activate plugins) isn't followed by another enforcement pass. Not exploitable today since neither blocked plugin (maplibre-gl-geo-editor, maplibre-gl-annotations) implements handleUrlParameters, but it's a latent gap in the "every activation path must be covered" invariant the code itself calls out. Low confidence.

Security

  • None found. The addLayer/setFilter embed-API validation (scheme blocklist, renderable-source allowlist, style-spec compile check) is thorough and well-tested; the viewer preset's plugin/menu/shortcut/drag-drop lockdown looks consistently wired through every activation path I could trace (restoreProjectState, manual toggles, drag-and-drop, keyboard shortcuts).

Performance

  • None found.

Quality

  • packages/map/src/layer-sync.ts:245 — stale doc comment on applyExternalNativeFeatureFilters still says it reads only timeFilter and the rule filter, not updated to mention the new embedFilter. Trivial, high confidence.

CLAUDE.md

  • No violations found. New user-facing strings reuse existing i18n keys (ViewerLayerPanel.tsx uses t("sharedRail.layers")), RTL-safe Tailwind logical classes are used in the new panel (border-e, marginInlineStart), and the new GitHub Actions workflow matches the repo's existing actions/checkout@v7/actions/setup-node@v7 conventions.

I traced the full protocol v1→v2 compatibility path, the addLayer/setFilter validation logic against hasRestorableLayerSource/validateMapExpression, the viewer preset's plugin-blocking and UI-hiding wiring across DesktopShell.tsx/TopToolbar.tsx/ControlsMenu.tsx/HelpMenu.tsx/useGlobalShortcuts.ts/useLayoutOptions.ts, the new @geolibre/embed client, and the store/layer-sync changes for embedFilter.

- setFilter now stores the expression the style spec compiled, not the array
  that arrived. validateMapExpression sees a JSON round-trip, which rewrites
  `undefined`/`NaN`/`Infinity` — all of which survive a structured clone — as
  `null`, so a host sending `["==", ["get","x"], undefined]` was acked ok on
  a sanitized copy while layer-sync applied the unchecked original.
- The viewer plugin guard re-runs after handleUrlParameters settles. It
  activates plugins asynchronously, so it can land after the synchronous pass
  following restoreProjectState. No blocked plugin registers a URL handler
  today, but covering every activation path is the point of the guard.
- Mention embedFilter in applyExternalNativeFeatureFilters' docstring and its
  `@param layer`, matching the other two updated in this branch.
Comment thread apps/geolibre-desktop/src/hooks/useEmbedApi.ts Outdated
@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Code review

I reviewed the PR across the new @geolibre/embed client package, the extended embed postMessage protocol (v2), the read-only layout=viewer preset (menus, shortcuts, drag/drop, plugin blocking), and the associated addLayer/setFilter input validation. This is an unusually well-defended change: URL-scheme filtering (javascript:, data:, file:, blob:, with newline/tab normalization matching browser URL parsing), MapLibre-expression compilation before storing a host-set filter, an allowlist of inline-feature layer types, viewer-plugin blocking re-asserted after every project restore/URL-parameter pass, and extensive, purpose-built tests for each of these paths (tests/embed-api.test.ts, tests/embed-client.test.ts, tests/viewer-plugins.test.ts, tests/layout-options.test.ts).

I traced the plugin-manager activation/deactivation ordering (enforceViewerPlugins after both restoreProjectState and the async handleUrlParameters pass), the JSX restructuring in DesktopShell.tsx around the viewer/non-viewer panel branches, the ViewerLayerPanel group-header indentation math against the store's documented "group members stay contiguous" invariant (which is in fact enforced in store.ts), and the shortcut-filtering claim in TopToolbar.tsx ("every shortcut-carrying command is view.* or project.*") against the actual command list — all held up.

Bugs: None found.

Security: None found. The addLayer/setFilter validation, origin/version handling in both the app-side hook and the new @geolibre/embed client, and the viewer-plugin blocklist all correctly close the gaps their own code comments call out, and are backed by tests for the specific bypasses they claim to prevent.

Performance: None found.

Quality:

  • Low-medium confidence — the new v1/v2 dual-broadcast and hostVersion-pinning logic in useEmbedApi.ts's emit/handleMessage has no direct automated test (only the pure helpers in lib/embed-api.ts are unit-tested; this hook-level behavior is covered only by manual Playwright verification per the PR description). Flagged inline.

CLAUDE.md: No violations found — i18n via t() is used for the new ViewerLayerPanel strings, no node_modules edits, and the changes don't touch any of the mirrored-constant surfaces (MAX_VECTOR_PMTILES_ZOOM, MAX_VECTOR_BYTES, MAP_PANEL_SELECTOR, propertySpecFor, Whitebox distance params) that this repo's guidelines call out for extra scrutiny.

- Extract the embed API's envelope-targeting and version-pinning decisions
  out of the useEmbedApi closure into pure helpers in lib/embed-api.ts —
  embedEventTargets, embedEventVersions, embedRequestVersion — and cover them
  in tests/embed-api.test.ts: the broadcast-to-every-origin-until-the-host-
  speaks case, the scope-to-one-origin-after case, the wildcard collapse, the
  dual v2+v1 broadcast that keeps a listen-only v1 host working, the pin to
  the first request's version, and an ack answering in its own version
  whatever the host was pinned to. Previously only manual verification
  covered this branch.
Comment thread packages/plugins/src/viewer-plugins.ts
Comment thread packages/core/src/project.ts
@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Code review

Bugs

  • (low-medium confidence) VIEWER_BLOCKED_PLUGIN_IDS enforcement (enforceViewerPlugins() in DesktopShell.tsx, called right after pluginManager.restoreProjectState) silently assumes every blocked plugin's activate() mounts its map control synchronously. PluginManager explicitly supports async activation (watchAsyncActivation), so a future plugin added to the blocklist that activates asynchronously could have its control appear after enforceViewerPlugins() already ran, leaving an authoring control live in a read-only viewer embed. Not a live bug today (both maplibre-gl-geo-editor and maplibre-gl-annotations activate synchronously), but the invariant is undocumented and untested. See inline comment on packages/plugins/src/viewer-plugins.ts.

Security

  • No issues found. The embed protocol's input validation (apps/geolibre-desktop/src/lib/embed-api.ts) is careful and well-tested: addLayer sources are checked against a scheme blocklist (javascript:, data:, file:, blob:, with tab/newline/CR stripped first to prevent scheme-splitting bypasses), layer specs are required to carry an actually-renderable source, setFilter expressions are compiled through the real MapLibre style-spec validator before being accepted, and listLayers/addLayer only expose/accept a narrow, deliberately chosen field set. Origin handling (isEmbedOriginAllowed, allowlist parsing) looks sound and is unchanged in substance from the pre-PR version, just refactored into pure, unit-tested functions.

Performance

  • No issues found in the changed code.

Quality

  • (low confidence, minor) prepareLayerForSave now strips the transient embedFilter field before persisting a project, mirroring the existing timeFilter handling — but there's no test verifying this (unlike other transient-state stripping, which is at least exercised indirectly elsewhere). A small regression test would guard against a runtime host-set filter silently leaking into a saved/shared .geolibre.json. See inline comment on packages/core/src/project.ts.
  • The rest of the change (the @geolibre/embed client package, the v2 protocol additions, the layout=viewer preset wiring through DesktopShell/TopToolbar/ControlsMenu/HelpMenu, and ViewerLayerPanel) is thorough, consistently documented, and backed by extensive unit tests (tests/embed-api.test.ts, tests/embed-client.test.ts, tests/layout-options.test.ts, tests/viewer-plugins.test.ts). I traced the shortcut-narrowing logic in TopToolbar (shortcutCommands filters to view.* when viewer is set) against the actual command list and confirmed every command carrying a shortcut is indeed project.* or view.*, matching the code comment's claim.

CLAUDE.md

  • No violations found. New user-facing strings use t()/react-i18next as required; no ml-/left- physical Tailwind utilities were introduced in the new ViewerLayerPanel (it uses marginInlineStart, border-e); the PR doesn't touch any of the drift-prone mirrored constants (MAX_VECTOR_PMTILES_ZOOM, MAX_VECTOR_BYTES, MAP_PANEL_SELECTOR, propertySpecFor, NON_DISTANCE_NAMES) or the Whitebox menu catalog.

- Document the synchronous-activation invariant VIEWER_BLOCKED_PLUGIN_IDS
  depends on. PluginManager marks a plugin active before an async activate()
  resolves (watchAsyncActivation), and the viewer guard deactivates in that
  same tick — so an async blocked plugin would be torn down before its
  control existed and then mount anyway. Adding such a plugin means teaching
  the guard to await the pending activation, not just adding the id. A test
  asserts neither listed plugin declares activate as async.
- Add a regression test for the transient per-feature filters being stripped
  on save. A leaked embedFilter would bake one host's runtime setFilter call
  into the shared .geolibre.json, where nothing in the UI would explain the
  filtered map to the next person who opened it.
Comment thread packages/plugins/src/viewer-plugins.ts
@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Code review

Bugs

  • None found beyond the security item below — the embed protocol, filter merging, and layout logic are unusually well tested and cross-checked against their own edge cases (undefined/NaN/Infinity serialization, throttled view events, RTL-safe indentation, etc.).

Security

  • High confidence: VIEWER_BLOCKED_PLUGIN_IDS (packages/plugins/src/viewer-plugins.ts:42) only lists the geometry editor and Annotations, but at least two other built-in plugins mount an addMapControl widget that writes directly to the project store — maplibre-gl-geoagent (an AI chat agent that calls addLayer/removeLayer via geoagent-layer-sync.ts) and maplibre-duckdb (SQL Workspace, which calls store.addLayer(layer) when adding a query result as a layer). A project saved with either active would let a ?layout=viewer embed be driven into full authoring through the on-map widget, contradicting the "read-only viewer" guarantee this file and docs/user-guide/embedding.md describe. Both plugins also activate asynchronously (dynamic imports), which is precisely the loophole the file's own doc comment warns about — even adding their ids wouldn't be sufficient without also teaching enforceViewerPlugins to await pending activations. tests/viewer-plugins.test.ts currently asserts the (incomplete) 2-entry list, so this isn't caught by CI. Posted inline with details.
  • Everything else in the trust boundary — origin allowlisting, exact-origin postMessage targeting on both client and app sides, the javascript:/data:/file:/blob: scheme blocklist on addLayer sources, and setFilter's compile-before-store validation — is careful and correctly reasoned.

Performance

  • Nothing notable; view-change events are throttled, filter recomputation is keyed to avoid redundant setFilter calls, and the viewer layer panel's grouping is memoized.

Quality

  • No significant issues. Naming, docstrings, and test coverage are thorough and consistent with the rest of the codebase's documentation style.

CLAUDE.md

  • No violations found: no edits to generated catalogs, no MapLibre control CSS in node_modules, translated strings reuse an existing i18next key (sharedRail.layers) rather than hardcoding text, and the new package follows the monorepo/npm-workspaces conventions (dev alias to source, prepack build, provenance-enabled publish workflow).

giswqs added 2 commits August 1, 2026 13:16
The embedding guide described the v2 protocol but not the client that is
now the recommended way to speak it, and the rest of the docs still
described the pre-v2 world.

- embedding.md: a "The typed client" section with the install, `connect`
  options, the method table and what each resolves with, the rejection
  and timeout semantics, and when to call `disconnect()`.
- sharing-embedding.md: `layout=viewer` in the parameter list with a note
  on when to reach for it over `maponly`, and a step that drives the
  embed from the host page with the client.
- features.md: the viewer preset, the commands v2 added (layer toggle and
  listing, filters, viewport, runtime layer adds, PNG export), and the
  published client.
- index.md: a `layout=viewer` example beside the `maponly` one.
- architecture.md and CLAUDE.md: `@geolibre/embed` in the packages list,
  noting it is the one package published to npm.
- Add GeoAgent to VIEWER_BLOCKED_PLUGIN_IDS. Its control mounts via
  addMapControl and geoagent-layer-sync.ts calls addLayer/updateLayer/
  removeLayer on the store, so a project saved with it active handed a
  layout=viewer embed an AI agent that can add and remove layers. My earlier
  sweep cleared it by grepping `app.` usage inside maplibre-geoagent.ts,
  which misses store writes made from a companion module — the docstring now
  says to follow what the control can do, not which file calls a mutator.
- GeoAgent mounts behind a dynamic import, so the guard now awaits
  PluginManager.pendingActivation(id) before deactivating; deactivating
  during the pending window tore down nothing and let the mount land after.
  pendingActivation is a new public accessor over the existing private
  activationResults map.
- Move the three blocked plugin ids into packages/plugins/src/plugin-ids.ts
  and have each plugin import its own id from there. maplibre-geoagent.ts
  pulls in maplibre-gl-earth-engine, which touches `window` at module load,
  so reading the id from the plugin module made viewer-plugins.ts (and its
  tests) browser-only. Same binding either side, so the ids cannot drift.

SQL Workspace (maplibre-duckdb) was also raised but does not apply: it is not
a registered plugin, so it cannot appear in projectPlugins.activePluginIds or
be activated by restoreProjectState. It opens from the Add Data menu and the
command palette, both already gated in viewer mode.
Comment on lines +402 to +414
function hasRenderableEmbedSource(spec: Record<string, unknown>): boolean {
const source = isRecord(spec.source) ? spec.source : {};
if (
hasRestorableLayerSource({ source, metadata: isRecord(spec.metadata) ? spec.metadata : {} })
) {
return true;
}
if (typeof spec.type !== "string" || !INLINE_FEATURE_LAYER_TYPES.includes(spec.type)) {
return false;
}
// Inline GeoJSON is an object, on the spec's `geojson` or on `source.data`;
// the `hasRestorableLayerSource` check above only counts a string there.
return isRecord(spec.geojson) || isRecord(source.data);

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.

Bug: hasRenderableEmbedSource (via hasRestorableLayerSource) treats any layer type with a generic source.url/data/tiles/metadata.originalUrl as renderable, but layer-sync.ts's syncLayer only actually draws geojson, raster/wms/wmts/xyz, vector-tiles, mbtiles, video, image, or a genuine "external native" layer — the last category (pmtiles, cog, zarr, 3d-tiles, arcgis, lidar, gaussian-splat, deckgl-viz) is only dispatched when getExternalNativeLayerIds(layer) finds registration metadata (e.g. metadata.sourceKind === "pmtiles-url" + metadata.externalNativeLayer === true, checked by isPMTilesExternalLayer) that only each type's own control sets up via registerExternalNativeLayer — a bare addLayer/buildEmbedLayer call never populates it.

Concretely: tests/embed-api.test.ts ("accepts inline features on the spec or on the source for a data-backed type") asserts that addLayerSpec({ type: "pmtiles", source: { url: "pmtiles://https://x/y.pmtiles" } }) and addLayerSpec({ type: "cog", source: { url: "https://x/y.tif" } }) are accepted by parseEmbedRequest. Both will be pushed into the store and appear in listLayers(), but syncLayer has no case for pmtiles/cog outside the external-native path, and this spec never sets the metadata that path requires — so nothing is drawn on the map. This is exactly the "acked success, renders nothing" failure this function's own doc comment says it exists to prevent; it just doesn't cover control-painted types.

Confidence: high (traced through layer-sync.ts's syncLayer/isExternalNativeLayer/isPMTilesExternalLayer).

* - `deckgl-viz`: `createDeckVizStoreLayer` keeps its features in `geojson`
* with a source holding only `{type, data: rows}`.
*/
const INLINE_FEATURE_LAYER_TYPES = ["geojson", "deckgl-viz"];

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.

Bug: video layers can never be added through the addLayer embed command, even with a fully valid spec. syncVideoLayer in layer-sync.ts reads a video layer's media reference from source.urls (a plural array) plus source.coordinates — never source.url. hasRestorableLayerSource (used by hasRenderableEmbedSource two lines below) only checks source.url (singular)/data/tiles/metadata.originalUrl, and video isn't in INLINE_FEATURE_LAYER_TYPES either. So hasRenderableEmbedSource always returns false for type: "video", and any such spec is rejected with addLayer: a "video" layer needs a source with a url or tiles — regardless of how well-formed source.urls/source.coordinates are.

"video" is a valid LAYER_TYPES entry and passes the type check earlier in parseEmbedRequest, so this looks like an oversight rather than an intentional restriction (unlike the "control-painted" types, which are excluded on purpose per the comment above).

Confidence: medium-high.

Comment on lines +188 to +189
const requireLayer = (layerId: string) =>
requireEmbedLayer(useAppStore.getState().layers, layerId);

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.

Quality (minor): requireLayer is introduced here and used for setLayerVisibility/setFilter, but applyHighlight (above, unchanged) still does its own inline state.layers.find(...) + throw new Error(\No layer with id "${target.layerId}"`)instead of calling this helper — even thoughrequireEmbedLayer's doc comment in embed-api.tssays it's "shared bysetLayerVisibility, setFilter, and highlightFeature." Not a functional bug (the thrown message is identical either way), but worth switching applyHighlightto userequireLayer(target.layerId)` too so the three paths can't drift apart later.

Confidence: low-medium.

@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

All three inline comments posted successfully.

Code review

Bugs

  • hasRenderableEmbedSource/hasRestorableLayerSource in apps/geolibre-desktop/src/lib/embed-api.ts accept addLayer specs for "control-painted" layer types (pmtiles, cog, zarr, 3d-tiles, arcgis, lidar, gaussian-splat, deckgl-viz) whenever a generic source.url/data/tiles is present, but layer-sync.ts's syncLayer only renders these via an "external native" registration path that requires control-specific metadata a bare addLayer call never sets. The PR's own tests assert pmtiles/cog specs are accepted, but they will be added to the store and listed by listLayers() while never actually drawing on the map — the exact "acked success, renders nothing" failure this validation was built to prevent. Confidence: high.
  • type: "video" can never be added via addLayer: syncVideoLayer reads source.urls (plural) + source.coordinates, but hasRestorableLayerSource only recognizes source.url (singular), and video isn't in INLINE_FEATURE_LAYER_TYPES. Any well-formed video spec is rejected as lacking a renderable source. Confidence: medium-high.

Security

  • No issues found. The addLayer/setFilter validation (URL scheme blocklist with tab/newline/CR normalization, MapLibre style-spec compile check, layer-id collision and type checks) is thorough and well-tested; the origin-allowlist and version-pinning logic in both the app-side handler and the new @geolibre/embed client checks out.

Performance

  • No issues found; the new code paths (filter compilation, layer summaries, plugin-guard loop) are all small, bounded operations off the render-critical path.

Quality

  • applyHighlight in useEmbedApi.ts duplicates the "no layer with id" lookup/throw instead of reusing the new requireEmbedLayer/requireLayer helper, despite that helper's doc comment claiming it's shared across setLayerVisibility, setFilter, and highlightFeature. Not a functional bug today (identical error message), but a drift risk. Confidence: low-medium.

CLAUDE.md

  • No violations found. The PR correctly adds @geolibre/embed to the package list description (already reflected in the diff to CLAUDE.md itself), and doesn't touch any of the other mirrored-constant/generated-catalog areas the guidelines call out.

giswqs added 2 commits August 1, 2026 13:30
Drop NODE_AUTH_TOKEN/secrets.NPM_TOKEN in favour of the OIDC id-token,
matching how opengeos/geolibre-rust publishes geolibre-wasm. The job
already requested `id-token: write` and passed `--provenance`, so the
missing pieces were the npm CLI version (Trusted Publishing needs
>= 11.5.1, pinned rather than `npm@latest`, which briefly shipped a
build with no bundled `sigstore`) and dropping the token env.

npm has no PyPI-style pending publisher, so the trusted-publisher
connection cannot be made until the package exists: 2.4.0 goes up from a
maintainer's machine once, then the version gate here skips it and every
later release publishes through this workflow.

Also give the package the metadata npm renders on its page: a README
with install, usage, the API tables, and the allowlist requirement, plus
license, author, repository (with `directory`), homepage, bugs, and
keywords. `npm pack --dry-run` confirms the README ships in the tarball.
GeoAgent joined the geometry editor and Annotations in the viewer's
blocked list, so "map-drawing plugins" no longer describes the set.
Comment on lines +222 to +227
case "addLayer": {
const state = useAppStore.getState();
const layer = buildEmbedLayer(command.spec, state.layers);
state.addLayer(layer, command.spec.beforeId);
return layer.id;
}

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.

Bug (medium-high confidence): addLayer accepts specs for "control-painted" layer kinds (COG/raster, 3D Tiles, ArcGIS, LiDAR, Add-Vector-Layer-native, Overture, web-service, …) and acks ok: true with the new id, but it only calls state.addLayer(...) — it never runs the plugin restore pass those kinds need to actually appear on the map.

Those layer kinds are only rendered by their owning control (see raster-layer-sync.ts, maplibre-3d-tiles.ts, arcgis-i3s-tiles.ts, etc., which all stamp metadata.customLayerType + metadata.externalNativeLayer: true when the control creates the layer, and layer-sync.ts's applyExternalNativePaintBridge only forwards opacity/visibility to a bridge that must already be registered). parseEmbedRequest's hasRenderableEmbedSource only checks that a URL/tiles/inline-geojson is present — it doesn't gate on whether the type needs a control to render it, and the test at tests/embed-api.test.ts (addLayerSpec({ type: "cog", source: { url: "https://x/y.tif" } })) confirms a bare COG spec is accepted as valid.

This is the exact problem restoreLibraryLayer (apps/geolibre-desktop/src/lib/restore-library-layer.ts) was written to solve for the Layer Library's runtime "re-add" flow — re-adding a control-painted layer from My Data calls the matching RESTORE_BY_SOURCE_KIND pass so it actually renders. addLayer here doesn't do the equivalent, so a host that builds a spec with those metadata flags (e.g. by round-tripping an exported project layer) gets a silently inert layer: listLayers() reports it as visible: true, but nothing is painted.

Worth calling restoreLibraryLayer(layer, app) (or the matching RESTORE_BY_SOURCE_KIND pass) after state.addLayer(...) when the built layer is a control-painted kind, mirroring the Layer Library flow.

@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Code review

Bugs

  • addLayer's embed command never runs the plugin restore pass that "control-painted" layer kinds (COG/raster, 3D Tiles, ArcGIS, LiDAR, Add-Vector-Layer-native, etc.) need to actually render — state.addLayer(...) alone puts the layer in the store and acks ok: true, but the map stays blank for these kinds since only their owning control paints them (confirmed against restore-library-layer.ts, which solves the identical problem for the Layer Library's runtime re-add flow, and against the addLayerSpec({ type: "cog", ... }) test case, which shows such a spec passes validation). Medium-high confidence. Posted inline on apps/geolibre-desktop/src/hooks/useEmbedApi.ts.

Security

  • No new issues found. The postMessage origin allowlist (isEmbedOriginAllowed, embedEventTargets), the addLayer/loadProject URL-scheme blocking (BLOCKED_SOURCE_URL_SCHEME, isFetchableUrl, including the tab/newline normalization), the setFilter compile-before-store check, and the viewer preset's plugin blocklist (with its async-activation race handled via pendingActivation) are all careful and consistent with their documented threat models. Low confidence open question (not raised inline, no concrete counter-example found): VIEWER_BLOCKED_PLUGIN_IDS is a hand-maintained blocklist of three plugin ids whose on-map control writes to the project — I checked several other interactive plugins (directions, route animation, terrain measure) and none appeared to write persisted layer data outside VIEWER_BLOCKED_PLUGIN_IDS, but the list's completeness ultimately rests on manual review each time a new plugin is added, which the code comments already flag as the tradeoff.

Performance

  • Nothing notable; the throttled viewChanged emission, memoized ViewerLayerPanel grouping, and origin/version pinning logic are all reasonable.

Quality

  • No significant issues. Naming, plugin-id sharing via plugin-ids.ts (avoiding pulling maplibre-gl-earth-engine into the viewer guard's dependency graph), and the extensive doc comments are all well done.

CLAUDE.md

  • The @geolibre/embed package entry was added to the packages list as required; no other CLAUDE.md-covered conventions (i18n t() usage, RTL logical utilities, CSP allowlisting) appear to need updates for this change.

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.

@geolibre/embed SDK, a richer embed protocol, and a viewer embed preset

2 participants