[Foundry] Decide & document connection topology: self-hosted relay vs direct URL, and the SSRF/URL-policy exemption #143

Open
opened 2026-07-15 22:02:32 +00:00 by claude-bot · 0 comments
Contributor

Motivation/Context

Live NPC/token push (see the FoundryVTT live-push investigation, 2026-07-15) needs a hard architectural decision before any of #5's connection settings work can be considered final: how does the Quest Board backend actually reach a GM's Foundry instance, given that Foundry is frequently localhost:30000 on a GM's PC, or on a home LAN/VPN, never reliably reachable by direct inbound HTTP from the backend (#24 already calls this out explicitly)? The investigation evaluated three options: (A) direct backend→Foundry REST module HTTP call, (B) an outbound WebSocket relay — the ThreeHats foundryvtt-rest-api module already named in #24 is architected exactly this way, with the Foundry-side module dialing out to a relay server and the backend talking REST to the relay — and (C) browser-mediated push (rejected: HTTPS→HTTP mixed-content blocking, no server-side retries/idempotency, tab-dependent).

This also surfaces a real code-level collision: every existing admin-configured service URL is normalized through normalize_service_url() in webapp/backend/app/services/url_policy_service.py, called from settings_service.py (e.g. lines 167–171, 187–192) with private-IP targets blocked unless settings.allow_private_service_urls is set. A per-campaign, GM-settable Foundry/relay URL pointing at a LAN address is by design for this feature — so #5's connection-settings shape cannot be finalized until this issue resolves whether/how the Foundry URL is exempted from that SSRF guard.

Approach

Recommended architecture per the investigation: Option B (relay) as the primary mechanism, with Option A (direct URL) as a degenerate case of the same code pathfoundry/client.py (per #24) just speaks to "a base URL + API key"; whether that URL is a self-hosted relay or a directly reachable Foundry endpoint is a per-campaign configuration detail, not an architectural fork.

For the SSRF collision, the investigation's recommended resolution (the safer of the two options considered) is:

  • Admin sets the relay URL once, stored in app_settings like any other admin-configured service endpoint (no SSRF exposure — it's an admin, not GM, setting).
  • GMs only paste their world's API key — no campaign-level arbitrary URL field at all, which removes campaign-level SSRF entirely.
  • The alternative (exempt a per-campaign Foundry URL from normalize_service_url with its own allowlist policy) should be documented as rejected, with the reasoning, if this issue confirms the admin-relay approach.

Concrete deliverables for this issue:

  1. Validate the foundryvtt-rest-api module's relay mode against the actual production Foundry instance and core version (per #24's explicit sequencing warning — this must happen before adapter code in #7 is written).
  2. Confirm whether the self-hostable relay server should run as a small Docker service alongside the Quest Board stack, or whether the community-hosted relay is acceptable for v1.
  3. Write up the final decision (relay vs direct, admin-URL vs per-campaign-URL, SSRF-policy interaction) as a short design note referenced by #5 and #7.
  4. Confirm the relay's "at least one connected Foundry client" requirement and document it as a caveat for #8 (scheduled member sync, which may run when nobody is in the world) — not a blocker for this issue, but must be recorded.

Dependencies

  • Amends/extends #4 (install/validate module) and #5 (per-campaign connection settings) in v4.0.0 — this issue's decision is a precondition for finalizing #5's settings schema.
  • Informs #6 (health check) and #7 (adapter scaffolding), which should not begin adapter-level work until this is validated (per #24's sequencing rule).

Out of scope

  • Writing the relay server itself (use the existing open-source self-hostable relay, don't build one).
  • Adapter code (npc_to_actor, etc.) — tracked separately in #7/#9.
  • Live scene placement, token push, or any Phase 2.5 functionality — this issue is purely the connectivity/topology decision.

Acceptance criteria

  • The foundryvtt-rest-api module's relay mode has been validated end-to-end against the real production Foundry instance (tested module + core version pair recorded).
  • A written decision exists (in this issue or linked doc) on: relay vs direct URL as the primary mechanism, and admin-set relay URL + GM-pasted API key vs per-campaign arbitrary URL.
  • The SSRF/URL-policy interaction is explicitly resolved and documented — either the Foundry URL never reaches GM-controlled input (preferred), or a specific, justified exemption to normalize_service_url is specified.
  • #5's connection-settings design references this issue's decision rather than re-deciding it.
## Motivation/Context Live NPC/token push (see the FoundryVTT live-push investigation, 2026-07-15) needs a hard architectural decision before any of #5's connection settings work can be considered final: how does the Quest Board backend actually reach a GM's Foundry instance, given that Foundry is frequently `localhost:30000` on a GM's PC, or on a home LAN/VPN, never reliably reachable by direct inbound HTTP from the backend (#24 already calls this out explicitly)? The investigation evaluated three options: (A) direct backend→Foundry REST module HTTP call, (B) an outbound WebSocket relay — the ThreeHats `foundryvtt-rest-api` module already named in #24 is architected exactly this way, with the Foundry-side module dialing *out* to a relay server and the backend talking REST to the relay — and (C) browser-mediated push (rejected: HTTPS→HTTP mixed-content blocking, no server-side retries/idempotency, tab-dependent). This also surfaces a real code-level collision: every existing admin-configured service URL is normalized through `normalize_service_url()` in `webapp/backend/app/services/url_policy_service.py`, called from `settings_service.py` (e.g. lines 167–171, 187–192) with private-IP targets blocked unless `settings.allow_private_service_urls` is set. A per-campaign, GM-settable Foundry/relay URL pointing at a LAN address is *by design* for this feature — so #5's connection-settings shape cannot be finalized until this issue resolves whether/how the Foundry URL is exempted from that SSRF guard. ## Approach Recommended architecture per the investigation: **Option B (relay) as the primary mechanism, with Option A (direct URL) as a degenerate case of the same code path** — `foundry/client.py` (per #24) just speaks to "a base URL + API key"; whether that URL is a self-hosted relay or a directly reachable Foundry endpoint is a per-campaign configuration detail, not an architectural fork. For the SSRF collision, the investigation's recommended resolution (the safer of the two options considered) is: - **Admin sets the relay URL once**, stored in `app_settings` like any other admin-configured service endpoint (no SSRF exposure — it's an admin, not GM, setting). - **GMs only paste their world's API key** — no campaign-level arbitrary URL field at all, which removes campaign-level SSRF entirely. - The alternative (exempt a per-campaign Foundry URL from `normalize_service_url` with its own allowlist policy) should be documented as rejected, with the reasoning, if this issue confirms the admin-relay approach. Concrete deliverables for this issue: 1. Validate the `foundryvtt-rest-api` module's relay mode against the actual production Foundry instance and core version (per #24's explicit sequencing warning — this must happen before adapter code in #7 is written). 2. Confirm whether the self-hostable relay server should run as a small Docker service alongside the Quest Board stack, or whether the community-hosted relay is acceptable for v1. 3. Write up the final decision (relay vs direct, admin-URL vs per-campaign-URL, SSRF-policy interaction) as a short design note referenced by #5 and #7. 4. Confirm the relay's "at least one connected Foundry client" requirement and document it as a caveat for #8 (scheduled member sync, which may run when nobody is in the world) — not a blocker for this issue, but must be recorded. ## Dependencies - Amends/extends #4 (install/validate module) and #5 (per-campaign connection settings) in v4.0.0 — this issue's decision is a precondition for finalizing #5's settings schema. - Informs #6 (health check) and #7 (adapter scaffolding), which should not begin adapter-level work until this is validated (per #24's sequencing rule). ## Out of scope - Writing the relay server itself (use the existing open-source self-hostable relay, don't build one). - Adapter code (`npc_to_actor`, etc.) — tracked separately in #7/#9. - Live scene placement, token push, or any Phase 2.5 functionality — this issue is purely the connectivity/topology decision. ## Acceptance criteria - The `foundryvtt-rest-api` module's relay mode has been validated end-to-end against the real production Foundry instance (tested module + core version pair recorded). - A written decision exists (in this issue or linked doc) on: relay vs direct URL as the primary mechanism, and admin-set relay URL + GM-pasted API key vs per-campaign arbitrary URL. - The SSRF/URL-policy interaction is explicitly resolved and documented — either the Foundry URL never reaches GM-controlled input (preferred), or a specific, justified exemption to `normalize_service_url` is specified. - #5's connection-settings design references this issue's decision rather than re-deciding it.
Sign in to join this conversation.
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
rbrooks/Quest-Board#143
No description provided.