UNPKG

7.53 kBPlain TextView Raw
1import * as https from "https";
2
3/**
4 *
5 * A no dependencies, fully typed library to verify hCaptcha tokens
6 * submitted by users when solving CAPTCHA challenges.
7 *
8 * @remarks
9 *
10 * Note: this is an unofficial library; we are not affiliated with hCaptcha.com
11 *
12 * @example
13 *
14 * Verify a token submitted by a user:
15 *
16 * ```typescript
17 * import { verifyHcaptchaToken } from 'verify-hcaptcha';
18 *
19 * (async () => {
20 * const result = await verifyHcaptchaToken({
21 * token: "USER-SUBMITTED-RESPONSE-TOKEN",
22 * secretKey: "YOUR-SECRET-KEY",
23 * siteKey: "YOUR-SITE-KEY",
24 * });
25 *
26 * if (result.success) {
27 * console.log("User is human");
28 * } else {
29 * console.log("User is robot");
30 * }
31 * })();
32 * ```
33 *
34 * @example
35 *
36 * Verify a token submitted by a user and get the raw response from hCaptcha:
37 *
38 * ```typescript
39 * import { rawVerifyHcaptchaToken } from 'verify-hcaptcha';
40 *
41 * (async () => {
42 * const result = await rawVerifyHcaptchaToken({
43 * token: "USER-SUBMITTED-RESPONSE-TOKEN",
44 * secretKey: "YOUR-SECRET-KEY",
45 * siteKey: "YOUR-SITE-KEY",
46 * });
47 *
48 * if (result.success) {
49 * console.log("User is human");
50 * } else {
51 * console.log("User is robot");
52 * }
53 * })();
54 * ```
55 *
56 * @packageDocumentation
57 */
58
59/**
60 * `HcaptchaResponse` represents the response to the verification challenge
61 * performed by calling {@link verifyHcaptchaToken}.
62 *
63 * @see {@link https://docs.hcaptcha.com/#verify-the-user-response-server-side}
64 */
65export interface HcaptchaResponse {
66 /**
67 * True if the token is valid and meets the specified security criteria
68 * (for example, if the site key is associated to the secret key)
69 */
70 readonly success: boolean;
71
72 /**
73 * Optional: UTC timestamp of the challenge in ISO 8601 format
74 * (for example, `2021-10-02T18:12:10.149Z`)
75 */
76 readonly challengeTimestamp?: string;
77
78 /** Optional: hostname of the website where the challenge was solved */
79 readonly hostname?: string;
80
81 /** Optional: true if the response will be credited */
82 readonly credit?: boolean;
83
84 /**
85 * Optional: list of error codes
86 *
87 * @see {@link HcaptchaError}
88 */
89 readonly errorCodes?: HcaptchaError[];
90
91 /** Enterprise-only feature: score for malicious activity */
92 readonly score?: number;
93
94 /** Enterprise-only feature: list of reasons for the malicious activity score */
95 readonly scoreReasons?: string[];
96}
97
98/**
99 * `HcaptchaError` collects the errors explaining why a verification challenge failed.
100 *
101 * @see {@link HcaptchaResponse}
102 * @see {@link https://docs.hcaptcha.com/#siteverify-error-codes-table}
103 */
104export type HcaptchaError =
105 /** Secret key is missing */
106 | "missing-input-secret"
107 /** Secret key is invalid */
108 | "invalid-input-secret"
109 /** User response token is missing */
110 | "missing-input-response"
111 /** User response token is invalid */
112 | "invalid-input-response"
113 /** Site key is invalid */
114 | "invalid-sitekey"
115 /** Remote user IP is invalid */
116 | "invalid-remoteip"
117 /** Request is invalid */
118 | "bad-request"
119 /** User response token is invalid or has already been checked */
120 | "invalid-or-already-seen-response"
121 /** Must use the test site key when using a test verification token */
122 | "not-using-dummy-passcode"
123 /** Must use the test secret key when using a test verification token */
124 | "not-using-dummy-secret"
125 /** The site key is not associated to the secret key */
126 | "sitekey-secret-mismatch";
127
128/**
129 * `RawHcaptchaResponse` represents the raw response to the verification challenge
130 * obtained by directly calling the hCaptcha API endpoint
131 * with {@link rawVerifyHcaptchaToken}.
132 *
133 * @see {@link https://docs.hcaptcha.com/#verify-the-user-response-server-side}
134 */
135export interface RawHcaptchaResponse {
136 readonly success: boolean;
137 readonly challenge_ts?: string;
138 readonly hostname?: string;
139 readonly credit?: boolean;
140 readonly "error-codes"?: string[];
141 readonly score?: number;
142 readonly score_reason?: string[];
143}
144
145/**
146 * `verifyHcaptchaToken` verifies with the hCaptcha API that the response token
147 * obtained from a captcha challenge is valid.
148 *
149 * @param token - required: the token obtained from a user with a captcha challenge
150 * @param secretKey - required: the secret key for your account
151 * @param siteKey - optional but recommended: the site key for the website hosting the captcha challenge
152 * @param remoteIp - optional: the IP address of the user submitting the challenge
153 *
154 * @returns a {@link HcaptchaResponse} with the verification result
155 */
156export async function verifyHcaptchaToken({
157 token,
158 secretKey,
159 siteKey,
160 remoteIp,
161}: {
162 token: string;
163 secretKey: string;
164 siteKey?: string;
165 remoteIp?: string;
166}): Promise<HcaptchaResponse> {
167 const {
168 success,
169 challenge_ts: challengeTimestamp,
170 hostname,
171 credit,
172 "error-codes": rawErrorCodes,
173 score,
174 score_reason: scoreReasons,
175 } = await rawVerifyHcaptchaToken({
176 token,
177 secretKey,
178 siteKey,
179 remoteIp,
180 });
181
182 return {
183 success,
184 challengeTimestamp,
185 hostname,
186 credit,
187 errorCodes: rawErrorCodes as HcaptchaError[] | undefined,
188 score,
189 scoreReasons,
190 };
191}
192
193/**
194 * `rawVerifyHcaptchaToken` verifies with the hCaptcha API that the response token
195 * obtained from a captcha challenge is valid and returns the raw hCaptcha response.
196 *
197 * @param token - required: the token obtained from a user with a captcha challenge
198 * @param secretKey - required: the secret key for your account
199 * @param siteKey - optional but recommended: the site key for the website hosting the captcha challenge
200 * @param remoteIp - optional: the IP address of the user submitting the challenge
201 *
202 * @returns a {@link RawHcaptchaResponse} with the verification result
203 */
204export async function rawVerifyHcaptchaToken({
205 token,
206 secretKey,
207 siteKey,
208 remoteIp,
209}: {
210 token: string;
211 secretKey: string;
212 siteKey?: string;
213 remoteIp?: string;
214}): Promise<RawHcaptchaResponse> {
215 const form = buildForm({ token, secretKey, siteKey, remoteIp });
216 const data = await postToHcaptcha({ form });
217 return data;
218}
219
220function buildForm({
221 token,
222 secretKey,
223 siteKey,
224 remoteIp,
225}: {
226 token?: string;
227 secretKey?: string;
228 siteKey?: string;
229 remoteIp?: string;
230}): string {
231 const form = new URLSearchParams();
232 if (token) {
233 form.append("response", token);
234 }
235
236 if (secretKey) {
237 form.append("secret", secretKey);
238 }
239
240 if (siteKey) {
241 form.append("sitekey", siteKey);
242 }
243
244 if (remoteIp) {
245 form.append("remoteip", remoteIp);
246 }
247
248 return form.toString();
249}
250
251function postToHcaptcha({
252 form,
253}: {
254 form: string;
255}): Promise<RawHcaptchaResponse> {
256 const options: https.RequestOptions = {
257 host: "hcaptcha.com",
258 path: "/siteverify",
259 method: "POST",
260 headers: {
261 "Content-Type": "application/x-www-form-urlencoded",
262 "Content-Length": Buffer.byteLength(form),
263 },
264 };
265
266 return new Promise((resolve, reject) => {
267 // See https://nodejs.org/api/http.html#http_class_http_clientrequest
268 const req = https.request(options, (res) => {
269 const chunks: string[] = [];
270 res.setEncoding("utf-8");
271 res
272 .on("data", (data) => {
273 chunks.push(data);
274 })
275 .on("end", () => {
276 const data = JSON.parse(chunks.join("")) as RawHcaptchaResponse;
277 resolve(data);
278 });
279 });
280
281 req.on("error", (err) => {
282 reject(err);
283 });
284
285 req.write(form);
286 req.end();
287 });
288}