Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,42 +18,55 @@ This also installs the `captchas` dependency. `pos-cli deploy <env>` afterwards.

## Keys

Create a site in the [hCaptcha dashboard](https://dashboard.hcaptcha.com/) and store the
pair in constants:
Create a site in the [hCaptcha dashboard](https://dashboard.hcaptcha.com/). **Best
practice**: store the pair in the abstraction's default constants, so the generic
widget/verify calls need neither `provider:`, `site_key:`, nor `secret:` at any call site:

```bash
pos-cli constants set <env> --name CAPTCHA_HCAPTCHA_SITE_KEY --value "10000000-..."
pos-cli constants set <env> --name CAPTCHA_HCAPTCHA_SECRET --value "0x0000..."
pos-cli constants set <env> --name CAPTCHA_DEFAULT_PROVIDER --value "hcaptcha"
pos-cli constants set <env> --name CAPTCHA_DEFAULT_SITE_KEY --value "10000000-..."
pos-cli constants set <env> --name CAPTCHA_DEFAULT_SECRET --value "0x0000..."
```

The site key is public (rendered client-side); the secret is server-side only — never output
it in HTML. The constant names are a convention: keys are always passed by the caller, so
multiple sitekeys per instance are just more constants.
it in HTML. Keys are always passed by the caller (or resolved from the defaults above), so
multiple sitekeys per instance are just another pair of constants, passed explicitly at that
call site instead of relying on the default.

## Usage

```liquid
{# in your form #}
{% render 'modules/captchas/widget',
provider: 'hcaptcha',
site_key: context.constants.CAPTCHA_HCAPTCHA_SITE_KEY %}
{% render 'modules/captchas/widget' %}

{# in your POST handler #}
{% function result = 'modules/captchas/commands/captcha/verify',
provider: 'hcaptcha',
secret: context.constants.CAPTCHA_HCAPTCHA_SECRET,
expected_sitekey: context.constants.CAPTCHA_HCAPTCHA_SITE_KEY %}
expected_sitekey: context.constants.CAPTCHA_DEFAULT_SITE_KEY %}
{% if result.valid %}...{% endif %}
```

> ⚠️ **Pass `expected_sitekey`.** hCaptcha secrets are **account-wide**: a token solved
> against any widget of your account verifies against your secret, so a token issued for one
> form could be redeemed on another. `expected_sitekey` is forwarded as hCaptcha's `sitekey`
> siteverify param (which hCaptcha itself recommends) so such tokens are rejected. Always set
> it when the account has more than one sitekey.
> it — even with a single default site key — since it costs nothing and future-proofs against
> a second sitekey being added later.

Pass `provider:`, `site_key:`, and/or `secret:` explicitly only when a call site needs to
deviate from the instance default (see [Keys](#keys)):

```liquid
{% render 'modules/captchas/widget',
provider: 'hcaptcha',
site_key: context.constants.CAPTCHA_HCAPTCHA_MKTG_SITE_KEY %}

{% function result = 'modules/captchas/commands/captcha/verify',
provider: 'hcaptcha',
secret: context.constants.CAPTCHA_HCAPTCHA_MKTG_SECRET,
expected_sitekey: context.constants.CAPTCHA_HCAPTCHA_MKTG_SITE_KEY %}
```

Set `CAPTCHA_DEFAULT_PROVIDER=hcaptcha` to omit the `provider:` argument. The token is read
automatically from `h-captcha-response` in `context.params`.
The token is read automatically from `h-captcha-response` in `context.params`.

## Widget options

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,34 +23,46 @@ This also installs the `captchas` dependency. `pos-cli deploy <env>` afterwards.
## Keys

Register a **v2** site in the [reCAPTCHA admin console](https://www.google.com/recaptcha/admin)
(your own Google account) and store the pair in constants:
(your own Google account). **Best practice**: store the pair in the abstraction's default
constants, so the generic widget/verify calls need neither `provider:`, `site_key:`, nor
`secret:` at any call site:

```bash
pos-cli constants set <env> --name CAPTCHA_RECAPTCHA_SITE_KEY --value "6Le..."
pos-cli constants set <env> --name CAPTCHA_RECAPTCHA_SECRET --value "6Le..."
pos-cli constants set <env> --name CAPTCHA_DEFAULT_PROVIDER --value "recaptcha"
pos-cli constants set <env> --name CAPTCHA_DEFAULT_SITE_KEY --value "6Le..."
pos-cli constants set <env> --name CAPTCHA_DEFAULT_SECRET --value "6Le..."
```

The site key is public (rendered client-side); the secret is server-side only — never output
it in HTML. The constant names are a convention: keys are always passed by the caller, so
multiple key pairs per instance are just more constants.
it in HTML. Keys are always passed by the caller (or resolved from the defaults above), so
multiple key pairs per instance are just another pair of constants, passed explicitly at
that call site instead of relying on the default.

## Usage

```liquid
{# in your form #}
{% render 'modules/captchas/widget' %}

{# in your POST handler #}
{% function result = 'modules/captchas/commands/captcha/verify' %}
{% if result.valid %}...{% endif %}
```

Pass `provider:`, `site_key:`, and/or `secret:` explicitly only when a call site needs to
deviate from the instance default (see [Keys](#keys)):

```liquid
{% render 'modules/captchas/widget',
provider: 'recaptcha',
site_key: context.constants.CAPTCHA_RECAPTCHA_SITE_KEY %}
site_key: context.constants.CAPTCHA_RECAPTCHA_MKTG_SITE_KEY %}

{# in your POST handler #}
{% function result = 'modules/captchas/commands/captcha/verify',
provider: 'recaptcha',
secret: context.constants.CAPTCHA_RECAPTCHA_SECRET %}
{% if result.valid %}...{% endif %}
secret: context.constants.CAPTCHA_RECAPTCHA_MKTG_SECRET %}
```

Set `CAPTCHA_DEFAULT_PROVIDER=recaptcha` to omit the `provider:` argument. The token is read
automatically from `g-recaptcha-response` in `context.params`.
The token is read automatically from `g-recaptcha-response` in `context.params`.

## Widget options

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,41 +26,56 @@ This also installs the `captchas` dependency. `pos-cli deploy <env>` afterwards.
## Keys

Register a **v3** site in the [reCAPTCHA admin console](https://www.google.com/recaptcha/admin)
(your own Google account; v3 has **no public test keys** and keys are tied to your domain)
and store the pair in constants:
(your own Google account; v3 has **no public test keys** and keys are tied to your domain).
**Best practice**: store the pair in the abstraction's default constants, so the generic
widget/verify calls need neither `provider:`, `site_key:`, nor `secret:` at any call site:

```bash
pos-cli constants set <env> --name CAPTCHA_RECAPTCHA3_SITE_KEY --value "6Le..."
pos-cli constants set <env> --name CAPTCHA_RECAPTCHA3_SECRET --value "6Le..."
pos-cli constants set <env> --name CAPTCHA_DEFAULT_PROVIDER --value "recaptcha3"
pos-cli constants set <env> --name CAPTCHA_DEFAULT_SITE_KEY --value "6Le..."
pos-cli constants set <env> --name CAPTCHA_DEFAULT_SECRET --value "6Le..."
```

The site key is public (rendered client-side); the secret is server-side only — never output
it in HTML.
it in HTML. Keys are always passed by the caller (or resolved from the defaults above), so a
second key pair on the same instance is just another pair of constants, passed explicitly at
that call site instead of relying on the default.

## Usage

```liquid
{# in your form — invisible: wires the form's submit, no visible widget #}
{% parse_json widget_options %}{ "action": "signup_submit" }{% endparse_json %}
{% render 'modules/captchas/widget', options: widget_options %}

{# in your POST handler #}
{% function result = 'modules/captchas/commands/captcha/verify',
min_score: 0.5,
expected_action: 'signup_submit' %}
{% if result.valid %}...{% endif %}
```

Pass `provider:`, `site_key:`, and/or `secret:` explicitly only when a call site needs to
deviate from the instance default (see [Keys](#keys)):

```liquid
{% render 'modules/captchas/widget',
provider: 'recaptcha3',
site_key: context.constants.CAPTCHA_RECAPTCHA3_SITE_KEY,
site_key: context.constants.CAPTCHA_RECAPTCHA3_MKTG_SITE_KEY,
options: widget_options %}

{# in your POST handler #}
{% function result = 'modules/captchas/commands/captcha/verify',
provider: 'recaptcha3',
secret: context.constants.CAPTCHA_RECAPTCHA3_SECRET,
secret: context.constants.CAPTCHA_RECAPTCHA3_MKTG_SECRET,
min_score: 0.5,
expected_action: 'signup_submit' %}
{% if result.valid %}...{% endif %}
```

v3 is **score-based**: every verification returns a score (`0.0` = likely bot, `1.0` =
likely human) instead of a binary verdict. `verify` passes only when the provider reports
success, `result.score >= min_score` (default `0.5`), and — when `expected_action` is set —
the response action matches. `result.score` and `result.action` are returned for logging or
custom thresholds. Set `CAPTCHA_DEFAULT_PROVIDER=recaptcha3` to omit the `provider:` argument.
custom thresholds.

## Widget options

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,37 +19,49 @@ This also installs the `captchas` dependency. `pos-cli deploy <env>` afterwards.
## Keys

Create a Turnstile widget in the [Cloudflare dashboard](https://dash.cloudflare.com/?to=/:account/turnstile)
(your own account — keys are per-customer, not platform-wide) and store the pair in constants:
(your own account — keys are per-customer, not platform-wide). **Best practice**: store them
in the abstraction's default constants, so the generic widget/verify calls need neither
`provider:`, `site_key:`, nor `secret:` at any call site:

```bash
pos-cli constants set <env> --name CAPTCHA_TURNSTILE_SITE_KEY --value "0x4AAA..."
pos-cli constants set <env> --name CAPTCHA_TURNSTILE_SECRET --value "0x4AAA..."
pos-cli constants set <env> --name CAPTCHA_DEFAULT_PROVIDER --value "turnstile"
pos-cli constants set <env> --name CAPTCHA_DEFAULT_SITE_KEY --value "0x4AAA..."
pos-cli constants set <env> --name CAPTCHA_DEFAULT_SECRET --value "0x4AAA..."
```

The site key is public (rendered client-side); the secret is server-side only — never output
it in HTML. The constant names are a convention: keys are always passed by the caller, so
multiple key pairs per instance (e.g. a separate marketing-site widget) are just more
constants (`CAPTCHA_TURNSTILE_MKTG_SITE_KEY`, ...).
it in HTML. Keys are always passed by the caller (or resolved from the defaults above), so
multiple key pairs per instance (e.g. a separate marketing-site widget) are just another pair
of constants, passed explicitly at that call site instead of relying on the default
(`CAPTCHA_TURNSTILE_MKTG_SITE_KEY`, ...).

## Usage

```liquid
{# in your form #}
{% render 'modules/captchas/widget' %}

{# in your POST handler #}
{% function result = 'modules/captchas/commands/captcha/verify' %}
{% if result.valid %}...{% endif %}
```

Pass `provider:`, `site_key:`, and/or `secret:` explicitly only when a call site needs to
deviate from the instance default (see [Keys](#keys)):

```liquid
{% render 'modules/captchas/widget',
provider: 'turnstile',
site_key: context.constants.CAPTCHA_TURNSTILE_SITE_KEY %}
site_key: context.constants.CAPTCHA_TURNSTILE_MKTG_SITE_KEY %}

{# in your POST handler #}
{% function result = 'modules/captchas/commands/captcha/verify',
provider: 'turnstile',
secret: context.constants.CAPTCHA_TURNSTILE_SECRET %}
{% if result.valid %}...{% endif %}
secret: context.constants.CAPTCHA_TURNSTILE_MKTG_SECRET %}
```

Set `CAPTCHA_DEFAULT_PROVIDER=turnstile` to omit the `provider:` argument. The token is read
automatically from `cf-turnstile-response` in `context.params`; if you rename the widget's
response field via its `response_field_name` option, pass the same value to `verify` as
`response_field_name:`.
The token is read automatically from `cf-turnstile-response` in `context.params`; if you
rename the widget's response field via its `response_field_name` option, pass the same value
to `verify` as `response_field_name:`.

## Widget options

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,19 @@ pos-cli deploy <env>

The module **stores no keys**. You pass the **public site key** to the widget (rendered
client-side) and the **secret key** to `verify` (server-side only), typically read from
**platformOS constants**. Because keys are passed per call, **multiple keys for the same
provider on one instance** is trivial — different forms simply pass different keys:
**platformOS constants**. For the common case of one widget/site per instance — **the best
practice** — set `CAPTCHA_DEFAULT_SITE_KEY` and `CAPTCHA_DEFAULT_SECRET` once (see [Site key
selection](#site-key-selection) and [Secret selection](#secret-selection)) and skip passing
`site_key`/`secret` at every call site. Don't invent a per-provider constant name
(`CAPTCHA_TURNSTILE_SITE_KEY` and friends) unless you actually need one — that naming exists
only for the **multiple keys on one instance** case, where extra forms pass their own key
explicitly instead of relying on the default:

```bash
pos-cli constants set <env> --name CAPTCHA_TURNSTILE_SITE_KEY --value "0x4AAA..."
pos-cli constants set <env> --name CAPTCHA_TURNSTILE_SECRET --value "0x4AAA..."
pos-cli constants set <env> --name CAPTCHA_DEFAULT_SITE_KEY --value "0x4AAA..."
pos-cli constants set <env> --name CAPTCHA_DEFAULT_SECRET --value "0x4AAA..."

# A second widget/site on the same instance — just another pair of constants:
# A second widget/site on the same instance — an extra pair of constants, passed explicitly:
pos-cli constants set <env> --name CAPTCHA_TURNSTILE_MKTG_SITE_KEY --value "0x4AAA..."
pos-cli constants set <env> --name CAPTCHA_TURNSTILE_MKTG_SECRET --value "0x4AAA..."
```
Expand All @@ -60,21 +65,46 @@ Each provider module's README documents its constant-name convention and where t

## Usage

**Best practice**: set `CAPTCHA_DEFAULT_PROVIDER`, `CAPTCHA_DEFAULT_SITE_KEY`, and
`CAPTCHA_DEFAULT_SECRET` once per instance, then invoke the widget and `verify` with none of
`provider`, `site_key`, or `secret` — all three resolve from constants. No per-provider
constant name to invent, and swapping providers later is a constant change, not a template
change:

```bash
pos-cli constants set <env> --name CAPTCHA_DEFAULT_PROVIDER --value "turnstile"
pos-cli constants set <env> --name CAPTCHA_DEFAULT_SITE_KEY --value "0x4AAA..."
pos-cli constants set <env> --name CAPTCHA_DEFAULT_SECRET --value "0x4AAA..."
```

```liquid
{# 1. in your form #}
{% render 'modules/captchas/widget',
provider: 'turnstile',
site_key: context.constants.CAPTCHA_TURNSTILE_SITE_KEY %}
{% render 'modules/captchas/widget' %}

{# 2. in your POST handler #}
{% liquid
function result = 'modules/captchas/commands/captcha/verify', provider: 'turnstile', secret: context.constants.CAPTCHA_TURNSTILE_SECRET
function result = 'modules/captchas/commands/captcha/verify'
if result.valid
# proceed
endif
%}
```

Pass `provider`, `site_key`, and/or `secret` explicitly only when a call site needs to
deviate from the instance default — e.g. a second widget on the same page using a different
provider or key (see [Keys are caller-supplied](#keys-are-caller-supplied)):

```liquid
{% render 'modules/captchas/widget',
provider: 'turnstile',
site_key: context.constants.CAPTCHA_TURNSTILE_MKTG_SITE_KEY %}

{% liquid
function result = 'modules/captchas/commands/captcha/verify',
provider: 'turnstile', secret: context.constants.CAPTCHA_TURNSTILE_MKTG_SECRET
%}
```

Render errors with `| t` — `result.errors` maps fields to arrays of translation keys:

```liquid
Expand All @@ -98,6 +128,29 @@ normalized (trimmed, lowercased).
pos-cli constants set <env> --name CAPTCHA_DEFAULT_PROVIDER --value "turnstile"
```

### Site key selection

`site_key` (widget only) may be passed explicitly. If omitted, it resolves to the
`CAPTCHA_DEFAULT_SITE_KEY` constant. There is no built-in fallback — with neither set, the
widget logs an error and emits an HTML comment instead (see [the widget
reference](#partial-modulescaptchaswidget)). Only whitespace is trimmed — unlike the provider
key, site keys are case-sensitive.

```bash
pos-cli constants set <env> --name CAPTCHA_DEFAULT_SITE_KEY --value "0x4AAA..."
```

### Secret selection

`secret` (verify only) may be passed explicitly. If omitted, it resolves to the
`CAPTCHA_DEFAULT_SECRET` constant. There is no built-in fallback — with neither set, `verify`
fails closed with `secret_missing` (no network call). Only whitespace is trimmed — secrets
are case-sensitive.

```bash
pos-cli constants set <env> --name CAPTCHA_DEFAULT_SECRET --value "0x4AAA..."
```

A provider is **available** when its module (`captchas_<key>`) is installed — detected via
the `provider.name` translation every provider module ships. Optionally,
`CAPTCHA_ENABLED_PROVIDERS` (CSV, e.g. `turnstile,hcaptcha`) pins an allow-list: when set,
Expand All @@ -115,7 +168,7 @@ a logged HTML comment, and verify returns `valid: false` with

| Param | Required | Description |
|---|---|---|
| `site_key` | yes | The provider's public site key. Blank logs an error + emits an HTML comment (the widget is still rendered and fails visibly client-side). |
| `site_key` | no | The provider's public site key; defaults to `CAPTCHA_DEFAULT_SITE_KEY`. A blank result (neither passed nor set) logs an error + emits an HTML comment (the widget is still rendered and fails visibly client-side). |
| `provider` | no | Provider key; defaults to `CAPTCHA_DEFAULT_PROVIDER`. |
| `options` | no | Hash of provider-specific widget options — see the provider module's README. Build it with `parse_json`/`hash_merge`; inline hash literals are nil at runtime. |

Expand All @@ -124,7 +177,7 @@ a logged HTML comment, and verify returns `valid: false` with
| Param | Required | Description |
|---|---|---|
| `provider` | no | Provider key; defaults to `CAPTCHA_DEFAULT_PROVIDER`. |
| `secret` | yes | Provider secret key (server-side; read from a constant by the caller). |
| `secret` | no | Provider secret key (server-side); defaults to `CAPTCHA_DEFAULT_SECRET`. A blank result (neither passed nor set) fails closed with `secret_missing`. |
| `token` | no | The widget token; defaults to the provider's response field in `context.params`. |
| `response_field_name` | no | Form field the widget wrote the token to; overrides the provider default (only needed if you set the widget's `response_field_name` option). |
| `remote_ip` | no | Optional visitor IP forwarded to the provider. |
Expand Down Expand Up @@ -218,7 +271,8 @@ pos-cli deploy <env>
pos-cli test run <env>
```

The suite assumes `CAPTCHA_DEFAULT_PROVIDER` is unset and `CAPTCHA_ENABLED_PROVIDERS` is
unset (or contains `test`) on the test instance. Provider-specific tests — real siteverify
The suite assumes `CAPTCHA_DEFAULT_PROVIDER`, `CAPTCHA_DEFAULT_SITE_KEY`, and
`CAPTCHA_DEFAULT_SECRET` are unset, and `CAPTCHA_ENABLED_PROVIDERS` is unset (or contains
`test`), on the test instance. Provider-specific tests — real siteverify
round-trips against public test keys, score thresholds — live in each provider module's
repo.
Loading
Loading