@nage-api/integrations-recaptcha
v1.0.0-beta.4
Published
Typed reCAPTCHA verification for @nage-api — secret in the body, POST, no TLS downgrade
Readme
@nage-api/integrations-recaptcha
Typed reCAPTCHA verification (PLAN.md §8, §12: "fix TLS, reCAPTCHA key/POST").
The legacy integration got three things wrong, and each one leaves a captcha looking like it works while providing no protection:
- it read the secret from
RECAPTCHA_SECRET_KET(§2.4), a variable nothing sets, so no usable secret ever went up the wire. Google answersinvalid-input-secret; code that only checkssuccesssees every verification fail, and code that ignores the response sees every one pass. - it was a GET with the secret in the query string — writing a long-lived credential into every access log and proxy log on the path.
- TLS verification was off across the integration libraries (§5.2), so the answer could come from anyone in the middle.
This package POSTs the secret in a form body, over TLS, to a URL that is not configurable — no deployment can point it somewhere plaintext.
import { ValidationError } from '@nage-api/core';
import { RecaptchaVerifier } from '@nage-api/integrations-recaptcha';
// The reader's validated environment. The name matters: the legacy library read
// `RECAPTCHA_SECRET_KET`, so it always got `undefined` and every verification
// failed the same way a wrong secret does.
declare const env: { readonly RECAPTCHA_SECRET_KEY: string };
// Off the request: the token the widget produced, and the caller's address.
declare const token: string;
declare const remoteIp: string;
const verifier = new RecaptchaVerifier({
secretKey: env.RECAPTCHA_SECRET_KEY,
minimumScore: 0.5,
allowedHostnames: ['example.com'],
});
const result = await verifier.assertHuman({ token, action: 'login', remoteIp });
if (!result.success) throw new ValidationError({ message: 'Captcha failed.' });The decision lives here
verify returns what Google said. assertHuman decides — score threshold,
action match, hostname allow-list — because a caller that only checks success
has a v3 captcha that passes every bot scoring 0.1, and a token minted on the
login page and replayed against the payment page is a real and cheap attack.
A failed captcha is a return value, not an exception: failing is the expected outcome for a bot. Only an unreachable Google raises.
Anything unparseable reads as not verified. A response this code cannot understand must never count as a pass — that is the difference between a captcha and a decoration.
The constructor rejects a key containing markup, which is what a site key pasted out of a browser snippet looks like. It cannot tell a site key from a secret otherwise: both are public-looking strings of the same shape.
Not yet implemented
- reCAPTCHA Enterprise, which has a different endpoint and an API key.
- hCaptcha and Turnstile, which have compatible-enough shapes to sit behind one port later.
