Skip to content

server-node should serve client/fcaptcha.js by default (single-origin widget delivery) #4

@PanoramicRum

Description

@PanoramicRum

Problem

FCaptcha ships two halves of the integration in one repo:

  • server-node/server.js (and server-python/, server-go/) — exposes the API: /health, /api/verify, /api/score, /api/token/verify, /api/pow/challenge, /api/challenge.
  • client/fcaptcha.js — the 98 KB browser widget that collects behavioural signals, calls /api/challenge, solves the PoW, and returns a token.

The browser needs client/fcaptcha.js. None of the server implementations serve it. So an operator who deploys server-node is missing the widget bundle that every consumer expects to be reachable at the same origin as the API.

Why it's a problem in practice

Consumers (e.g. anti-abuse libraries that integrate FCaptcha) typically expose a single serverUrl to clients, and the browser fetches the widget from <serverUrl>/fcaptcha.js. That implicit contract — "your FCaptcha deployment serves both the API and the widget at one origin" — is what most integrators expect. Today, every operator has to either:

  1. Bundle client/fcaptcha.js into their own frontend (extra build step per integrator).
  2. Host it as a static asset on a separate origin (CDN / nginx) and rewire the integration to point at that.
  3. Patch the server.

I just hit this in PanoramicRum/nimiq-simple-faucet#113 and worked around it with a one-line sed injection in our Dockerfile that adds app.use('/fcaptcha.js', express.static('/app/client/fcaptcha.js')). Verified it works end-to-end (faucet → fcaptcha widget loads inside Nimiq Pay's WebView over LAN).

Suggested fix

Implicit by default, opt-out via env.

  • Server serves /fcaptcha.js from client/fcaptcha.js automatically. Zero config. Most consumers benefit immediately, and the implicit single-origin contract gets honoured.
  • An env var (FCAPTCHA_SERVE_CLIENT=false) lets operators turn it off if they prefer to host the widget on a CDN / edge cache and serve it themselves. Rare case; opt-out keeps it simple.

Three-line change in server-node/server.js:

const path = require('path');
// ...
if (process.env.FCAPTCHA_SERVE_CLIENT !== 'false') {
  app.use('/fcaptcha.js', express.static(path.join(__dirname, '..', 'client', 'fcaptcha.js')));
}

Same idea applies to server-python and server-go for parity.

Docs change

Even with this merged, INSTALLATION.md should explicitly document the layering — both the implicit single-origin default and the CDN-hosting escape hatch. Today the README and INSTALLATION.md don't say anything about how the widget bundle reaches the browser, which is why every integrator hits this.

Happy to send a PR

I can send a PR for server-node with the change above plus the INSTALLATION.md note. Wanted to file this first so you can push back on the design (FCAPTCHA_SERVE_CLIENT name, default value, opt-in vs opt-out) before I write code. Parity changes for server-python and server-go can follow as separate PRs.

Found while integrating FCaptcha into a Nimiq Pay Mini App — full integration notes at PanoramicRum/nimiq-simple-faucet#113.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions