Skip to content

feat: add Cap CAPTCHA - #12

Merged
taobojlen merged 10 commits into
mainfrom
feat/cap-captcha
Jul 24, 2026
Merged

feat: add Cap CAPTCHA#12
taobojlen merged 10 commits into
mainfrom
feat/cap-captcha

Conversation

@taobojlen

@taobojlen taobojlen commented Jul 24, 2026

Copy link
Copy Markdown
Member

No description provided.

@cubic-dev-ai cubic-dev-ai 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.

Review completed against the latest diff

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread cron/bundle_certs.sh Outdated
Comment thread cron/Dockerfile Outdated
Comment thread cron/Dockerfile Outdated
Comment thread README.md
Comment thread README.md
The README and example.env used 'Authorization: Bot $CAP_ADMIN_KEY' to
create a site key, but Cap's Bot scheme authenticates against
Valkey-stored API keys, not the ADMIN_KEY env var — so that curl always
401s. The correct flow: POST /auth/login with the admin_key to obtain a
session token + hash, then POST /server/keys with a Bearer token
(base64 of {token,hash}). Verified live against tiago2/cap:latest.

Also notes that the production 'cap' service has no host port mapping,
so the curl must run via a temporary ports override or 'docker compose
exec' on the compose network (http://cap:3000).
The final whole-branch review found a production-breaking defect: the
hosting compose hardcoded CAP_INSTANCE_URL=http://cap:3000, but that
value is rendered into the browser's <cap-widget data-cap-api-endpoint>.
http://cap:3000 is Docker-network-internal (unresolvable from a browser)
and would be blocked as mixed content on an https://APP_DOMAIN page.
With Cap 'enabled' the widget would never solve, fail-closing every
register/login/reset POST for real users.

Fix: make the compose use ${CAP_INSTANCE_URL} (not the hardcoded
internal URL), document that it MUST be a browser-reachable HTTPS URL,
and add it to example.env with guidance. The cap service stays
internal-only; the self-hoster puts it behind their own ingress
(e.g. a Caddy route) and points CAP_INSTANCE_URL at the public URL.
Add a cap.{APP_DOMAIN} site block reverse-proxying to the internal cap:3000
service, so the browser-rendered <cap-widget> can reach Cap over HTTPS (the
existing internal-only http://cap:3000 is unresolvable + mixed-content blocked
on https pages).

Default CAP_INSTANCE_URL to https://cap. so the stack works
out-of-the-box once a site key is set, while remaining overridable in .env
for self-hosters who want a path-based or different-domain setup.
cap.{APP_DOMAIN} baked the 'cap.' prefix in, so there was no way to serve
Cap on a different subdomain (e.g. cap.shroud.email when
APP_DOMAIN=app.shroud.email). Introduce CAP_DOMAIN as the single knob:

- Caddyfile: cap.{} -> {$CAP_DOMAIN:disabled.localhost}. The
  :disabled.localhost default-token keeps the file valid when CAP_DOMAIN is
  unset (a bare {$CAP_DOMAIN} block is invalid Caddyfile and would break the
  whole stack for self-hosters who don't use Cap); Caddy issues no cert and
  routes no real traffic to the inert block.
- compose: default CAP_DOMAIN to cap. on both web and caddy,
  and default CAP_INSTANCE_URL to https://. Nested-default
  resolution keeps web + caddy in sync; explicit overrides still win.
- example.env: document CAP_DOMAIN and the now-derived CAP_INSTANCE_URL.
- Caddyfile.bunny: mirror the cap block from Caddyfile so self-hosters using
  Bunny DNS-01 get the Cap route too (with dns bunny issuer, matching the
  APP_DOMAIN block).
- docker-compose: stop defaulting CAP_DOMAIN to cap.${APP_DOMAIN}. It was
  the only thing making the CAP_INSTANCE_URL default complex
  (${CAP_INSTANCE_URL:-https://${CAP_DOMAIN:-cap.${APP_DOMAIN}}}). CAP_DOMAIN
  is now a required knob to enable Cap; when unset, Caddy leaves the route
  inert via the :disabled.localhost fallback and the app stays disabled.
  CAP_INSTANCE_URL simplifies to
  ${CAP_INSTANCE_URL:-https://${CAP_DOMAIN}}.
- caddy service keeps CAP_DOMAIN=${CAP_DOMAIN} (no default) so Caddy sees
  the operator's value or falls back.
- example.env: document CAP_DOMAIN as required, note both TLS paths.
@taobojlen taobojlen changed the title feat: add Cap CAPTCHA + hardened Haraka TLS to self-hosting stack feat: add Cap CAPTCHA Jul 24, 2026
@taobojlen
taobojlen merged commit 240d1c4 into main Jul 24, 2026
3 checks passed
@taobojlen
taobojlen deleted the feat/cap-captcha branch July 24, 2026 17:16
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.

1 participant