@friendlycaptcha/server-sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/.github/workflows/ci.yml +47 -0
  2. package/.github/workflows/publish.yml +18 -0
  3. package/.prettierrc +3 -0
  4. package/CHANGELOG.md +5 -0
  5. package/LICENSE +21 -0
  6. package/README.md +47 -0
  7. package/dist/api/errors.d.ts +54 -0
  8. package/dist/api/errors.js +64 -0
  9. package/dist/api/errors.js.map +1 -0
  10. package/dist/api/index.d.ts +1 -0
  11. package/dist/api/index.js +2 -0
  12. package/dist/api/index.js.map +1 -0
  13. package/dist/api/types.d.ts +59 -0
  14. package/dist/api/types.js +2 -0
  15. package/dist/api/types.js.map +1 -0
  16. package/dist/client/client.d.ts +53 -0
  17. package/dist/client/client.js +100 -0
  18. package/dist/client/client.js.map +1 -0
  19. package/dist/client/errors.d.ts +37 -0
  20. package/dist/client/errors.js +34 -0
  21. package/dist/client/errors.js.map +1 -0
  22. package/dist/client/index.d.ts +3 -0
  23. package/dist/client/index.js +4 -0
  24. package/dist/client/index.js.map +1 -0
  25. package/dist/client/result.d.ts +67 -0
  26. package/dist/client/result.js +125 -0
  27. package/dist/client/result.js.map +1 -0
  28. package/dist/client/version.gen.d.ts +1 -0
  29. package/dist/client/version.gen.js +4 -0
  30. package/dist/client/version.gen.js.map +1 -0
  31. package/dist/index.d.ts +292 -0
  32. package/dist/index.js +3 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/tsdoc-metadata.json +11 -0
  35. package/docs/index.md +12 -0
  36. package/docs/server-sdk.failed_due_to_client_error_code.md +15 -0
  37. package/docs/server-sdk.failed_to_decode_response_error_code.md +13 -0
  38. package/docs/server-sdk.failed_to_encode_error_code.md +13 -0
  39. package/docs/server-sdk.friendlycaptchaclient._constructor_.md +20 -0
  40. package/docs/server-sdk.friendlycaptchaclient.md +26 -0
  41. package/docs/server-sdk.friendlycaptchaclient.verifycaptcharesponse.md +30 -0
  42. package/docs/server-sdk.friendlycaptchaoptions.apikey.md +13 -0
  43. package/docs/server-sdk.friendlycaptchaoptions.fetch.md +13 -0
  44. package/docs/server-sdk.friendlycaptchaoptions.md +24 -0
  45. package/docs/server-sdk.friendlycaptchaoptions.sitekey.md +11 -0
  46. package/docs/server-sdk.friendlycaptchaoptions.siteverifyendpoint.md +15 -0
  47. package/docs/server-sdk.friendlycaptchaoptions.strict.md +15 -0
  48. package/docs/server-sdk.md +41 -0
  49. package/docs/server-sdk.request_failed_error_code.md +13 -0
  50. package/docs/server-sdk.request_failed_timeout_error_code.md +13 -0
  51. package/docs/server-sdk.siteverifyerrorresponse.error.md +11 -0
  52. package/docs/server-sdk.siteverifyerrorresponse.md +20 -0
  53. package/docs/server-sdk.siteverifyerrorresponse.success.md +11 -0
  54. package/docs/server-sdk.siteverifyerrorresponseerrordata.detail.md +11 -0
  55. package/docs/server-sdk.siteverifyerrorresponseerrordata.error_code.md +11 -0
  56. package/docs/server-sdk.siteverifyerrorresponseerrordata.md +20 -0
  57. package/docs/server-sdk.siteverifyresponse.md +14 -0
  58. package/docs/server-sdk.siteverifyresponsechallengedata.md +20 -0
  59. package/docs/server-sdk.siteverifyresponsechallengedata.origin.md +13 -0
  60. package/docs/server-sdk.siteverifyresponsechallengedata.timestamp.md +13 -0
  61. package/docs/server-sdk.siteverifyresponsedata.challenge.md +11 -0
  62. package/docs/server-sdk.siteverifyresponsedata.md +19 -0
  63. package/docs/server-sdk.siteverifysuccessresponse.data.md +11 -0
  64. package/docs/server-sdk.siteverifysuccessresponse.md +20 -0
  65. package/docs/server-sdk.siteverifysuccessresponse.success.md +11 -0
  66. package/docs/server-sdk.verifyclienterrorcode.md +14 -0
  67. package/docs/server-sdk.verifyresult._constructor_.md +20 -0
  68. package/docs/server-sdk.verifyresult.clienterrortype.md +11 -0
  69. package/docs/server-sdk.verifyresult.geterrorcode.md +15 -0
  70. package/docs/server-sdk.verifyresult.getresponse.md +17 -0
  71. package/docs/server-sdk.verifyresult.getresponseerror.md +17 -0
  72. package/docs/server-sdk.verifyresult.isclienterror.md +19 -0
  73. package/docs/server-sdk.verifyresult.isdecodeerror.md +17 -0
  74. package/docs/server-sdk.verifyresult.isencodeerror.md +17 -0
  75. package/docs/server-sdk.verifyresult.isrequestortimeouterror.md +17 -0
  76. package/docs/server-sdk.verifyresult.isstrict.md +17 -0
  77. package/docs/server-sdk.verifyresult.md +44 -0
  78. package/docs/server-sdk.verifyresult.response.md +13 -0
  79. package/docs/server-sdk.verifyresult.shouldaccept.md +17 -0
  80. package/docs/server-sdk.verifyresult.shouldreject.md +17 -0
  81. package/docs/server-sdk.verifyresult.status.md +13 -0
  82. package/docs/server-sdk.verifyresult.wasabletoverify.md +17 -0
  83. package/package.json +44 -0
  84. package/src/api/errors.ts +77 -0
  85. package/src/api/index.ts +1 -0
  86. package/src/api/types.ts +66 -0
  87. package/src/client/client.ts +154 -0
  88. package/src/client/errors.ts +42 -0
  89. package/src/client/index.ts +3 -0
  90. package/src/client/result.ts +151 -0
  91. package/src/client/version.gen.ts +3 -0
  92. package/src/index.ts +2 -0
  93. package/test/client/client.test.ts +48 -0
  94. package/test/client/mock.test.ts +44 -0
  95. package/tsconfig.json +20 -0
@@ -0,0 +1,17 @@
1
+ <!-- Do not edit this file. It is automatically generated by API Documenter. -->
2
+
3
+ [Home](./index.md) &gt; [@friendlycaptcha/server-sdk](./server-sdk.md) &gt; [VerifyResult](./server-sdk.verifyresult.md) &gt; [wasAbleToVerify](./server-sdk.verifyresult.wasabletoverify.md)
4
+
5
+ ## VerifyResult.wasAbleToVerify() method
6
+
7
+ Whether the request to verify the captcha was completed. In other words: the API responded with status 200.' If this is false, you should notify yourself and use `getErrorCode()` and `getResponseError()` to see what is wrong.
8
+
9
+ **Signature:**
10
+
11
+ ```typescript
12
+ wasAbleToVerify(): boolean;
13
+ ```
14
+ **Returns:**
15
+
16
+ boolean
17
+
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@friendlycaptcha/server-sdk",
3
+ "version": "0.1.0",
4
+ "description": "Serverside client SDK for the Friendly Captcha V2 API",
5
+ "main": "dist/index.js",
6
+ "type": "module",
7
+ "scripts": {
8
+ "build": "npm run build:tsc",
9
+ "build:dist": "rm -rf dist/ build/ && node scripts/updateVersion.mjs && npm run build:tsc && cp -r build/src/ dist/ && npm run build:apiextractor && npm run build:docs",
10
+ "build:tsc": "tsc",
11
+ "build:apiextractor": "api-extractor run --local --verbose",
12
+ "build:docs": "rm -rf build/docs && api-documenter markdown --output-folder docs --input temp/",
13
+ "test": "ava test/**/*.ts --timeout=1m",
14
+ "api-extractor": "api-extractor",
15
+ "fmt": "prettier src/**/*.ts test/**/*.ts package.json --write"
16
+ },
17
+ "author": "Friendly Captcha GmbH",
18
+ "license": "MIT",
19
+ "devDependencies": {
20
+ "@ava/typescript": "^3.0.1",
21
+ "@babel/cli": "^7.19.3",
22
+ "@babel/core": "^7.19.3",
23
+ "@babel/preset-env": "^7.19.3",
24
+ "@microsoft/api-documenter": "^7.22.27",
25
+ "@microsoft/api-extractor": "^7.36.2",
26
+ "ava": "^4.3.1",
27
+ "esbuild": "^0.16.9",
28
+ "prettier": "^3.0.0",
29
+ "sync-request": "^6.1.0",
30
+ "typescript": "^4.7.4"
31
+ },
32
+ "ava": {
33
+ "files": [
34
+ "test/**/*"
35
+ ],
36
+ "typescript": {
37
+ "rewritePaths": {
38
+ "src/": "build/src/",
39
+ "test/": "build/test/"
40
+ },
41
+ "compile": "tsc"
42
+ }
43
+ }
44
+ }
@@ -0,0 +1,77 @@
1
+ export type SiteverifyErrorCode =
2
+ | typeof SITEKEY_INVALID
3
+ | typeof AUTH_INVALID
4
+ | typeof AUTH_REQUIRED
5
+ | typeof BAD_REQUEST
6
+ | typeof RESPONSE_TIMEOUT
7
+ | typeof RESPONSE_DUPLICATE
8
+ | typeof RESPONSE_INVALID
9
+ | typeof RESPONSE_MISSING
10
+ | typeof INTERNAL_SERVER_ERROR;
11
+
12
+ /**
13
+ * The API key you provided was invalid.
14
+ *
15
+ * HTTP status 401.
16
+ */
17
+ export const AUTH_INVALID = "auth_invalid";
18
+ /**
19
+ * You forgot to set the `X-API-Key` header.
20
+ *
21
+ * HTTP status 401.
22
+ */
23
+ export const AUTH_REQUIRED = "auth_required";
24
+
25
+ /**
26
+ * The sitekey in your request is invalid.
27
+ *
28
+ * HTTP status 400.
29
+ */
30
+ export const SITEKEY_INVALID = "sitekey_invalid";
31
+
32
+ /**
33
+ * Something else is wrong with your request, e.g. your request body is empty.
34
+ */
35
+ export const BAD_REQUEST = "bad_request";
36
+ /**
37
+ * The response has expired.
38
+ *
39
+ * HTTP status 200.
40
+ */
41
+ export const RESPONSE_TIMEOUT = "response_timeout";
42
+ /**
43
+ * The response has already been used.
44
+ *
45
+ * HTTP status 200.
46
+ */
47
+ export const RESPONSE_DUPLICATE = "response_duplicate";
48
+ /**
49
+ * The response you provided was invalid (perhaps the user tried to work around the captcha).
50
+ *
51
+ * HTTP status 200.
52
+ */
53
+ export const RESPONSE_INVALID = "response_invalid";
54
+ /**
55
+ * You forgot to add the response parameter.
56
+ *
57
+ * HTTP status 400.
58
+ */
59
+ export const RESPONSE_MISSING = "response_missing";
60
+ /**
61
+ * Something went wrong within the server. (Should never happen).
62
+ *
63
+ * HTTP status 500.
64
+ */
65
+ export const INTERNAL_SERVER_ERROR = "internal_server_error";
66
+
67
+ export const ERROR_CODE_TO_STATUS: Record<SiteverifyErrorCode, number> = {
68
+ [SITEKEY_INVALID]: 400,
69
+ [AUTH_INVALID]: 401,
70
+ [AUTH_REQUIRED]: 401,
71
+ [BAD_REQUEST]: 400,
72
+ [RESPONSE_TIMEOUT]: 200,
73
+ [RESPONSE_DUPLICATE]: 200,
74
+ [RESPONSE_INVALID]: 200,
75
+ [RESPONSE_MISSING]: 400,
76
+ [INTERNAL_SERVER_ERROR]: 500,
77
+ };
@@ -0,0 +1 @@
1
+ export * from "./types";
@@ -0,0 +1,66 @@
1
+ import { SiteverifyErrorCode } from "./errors";
2
+
3
+ /**
4
+ * The request we make to the Frienldy Captcha API.
5
+ * @internal
6
+ */
7
+ export interface SiteverifyRequest {
8
+ /**
9
+ * The response value that the user submitted in the frc-captcha-response field
10
+ */
11
+ response: string;
12
+ /**
13
+ * Optional: the sitekey that you want to make sure the puzzle was generated from.
14
+ */
15
+ sitekey?: string;
16
+ }
17
+
18
+ /**
19
+ * @public
20
+ */
21
+ export interface SiteverifyResponseData {
22
+ challenge: SiteverifyResponseChallengeData;
23
+ }
24
+
25
+ /**
26
+ * @public
27
+ */
28
+ export interface SiteverifyResponseChallengeData {
29
+ /**
30
+ * Timestamp when the captcha challenge was completed (RFC3339).
31
+ */
32
+ timestamp: string;
33
+ /**
34
+ * The origin of the site where the captcha was solved (if known, can be empty string if not known).
35
+ */
36
+ origin: string;
37
+ }
38
+
39
+ /**
40
+ * @public
41
+ */
42
+ export interface SiteverifySuccessResponse {
43
+ success: true;
44
+ data: SiteverifyResponseData;
45
+ }
46
+
47
+ /**
48
+ * @public
49
+ */
50
+ export interface SiteverifyErrorResponseErrorData {
51
+ error_code: SiteverifyErrorCode;
52
+ detail: string;
53
+ }
54
+
55
+ /**
56
+ * @public
57
+ */
58
+ export interface SiteverifyErrorResponse {
59
+ success: false;
60
+ error: SiteverifyErrorResponseErrorData;
61
+ }
62
+
63
+ /**
64
+ * @public
65
+ */
66
+ export type SiteverifyResponse = SiteverifySuccessResponse | SiteverifyErrorResponse;
@@ -0,0 +1,154 @@
1
+ import type { SiteverifyRequest } from "../api/index.js";
2
+ import {
3
+ FAILED_DUE_TO_CLIENT_ERROR_CODE,
4
+ FAILED_TO_DECODE_RESPONSE_ERROR_CODE,
5
+ FAILED_TO_ENCODE_ERROR_CODE,
6
+ REQUEST_FAILED_ERROR_CODE,
7
+ REQUEST_FAILED_TIMEOUT_ERROR_CODE,
8
+ } from "./errors.js";
9
+ import { VerifyResult } from "./result.js";
10
+ import { SDK_VERSION } from "./version.gen.js";
11
+
12
+ /**
13
+ * Configuration options when creating a new `FriendlyCaptchaClient`.
14
+ * @public
15
+ */
16
+ export interface FriendlyCaptchaOptions {
17
+ sitekey?: string;
18
+ /**
19
+ * Friendly Captcha API Key.
20
+ */
21
+ apiKey: string;
22
+
23
+ /**
24
+ * The endpoint to use for the API. Can be "eu", "global", or a custom URL.
25
+ *
26
+ * Defaults to `"global"`.
27
+ */
28
+ siteverifyEndpoint?: string;
29
+
30
+ /**
31
+ * Whether to use strict mode, which rejects all captcha responses which were not strictly verified.
32
+ *
33
+ * This is not recommended. Defaults to `false`.
34
+ */
35
+ strict?: boolean;
36
+
37
+ /**
38
+ * The fetch implementation to use. Defaults to `globalThis.fetch`.
39
+ */
40
+ fetch?: typeof globalThis.fetch;
41
+ }
42
+
43
+ const GLOBAL_SITEVERIFY_ENDPOINT = "https://global.frcapi.com/api/v2/captcha/siteverify";
44
+ const EU_SITEVERIFY_ENDPOINT = "https://eu.frcapi.com/api/v2/captcha/siteverify";
45
+
46
+ /**
47
+ * A client for the Friendly Captcha API.
48
+ * @public
49
+ */
50
+ export class FriendlyCaptchaClient {
51
+ private sitekey?: string;
52
+ private apiKey: string;
53
+ private siteverifyEndpoint: string;
54
+ private strict: boolean;
55
+
56
+ private fetch: typeof globalThis.fetch;
57
+
58
+ constructor(opts: FriendlyCaptchaOptions) {
59
+ this.sitekey = opts.sitekey;
60
+
61
+ if (!opts.apiKey) {
62
+ throw new Error("api key is required");
63
+ }
64
+ this.apiKey = opts.apiKey;
65
+
66
+ let siteverifyEndpoint = opts.siteverifyEndpoint || "global";
67
+ if (siteverifyEndpoint === "global") {
68
+ siteverifyEndpoint = GLOBAL_SITEVERIFY_ENDPOINT;
69
+ } else if (siteverifyEndpoint === "eu") {
70
+ siteverifyEndpoint = EU_SITEVERIFY_ENDPOINT;
71
+ }
72
+ this.siteverifyEndpoint = siteverifyEndpoint;
73
+
74
+ this.strict = !!opts.strict;
75
+ this.fetch = opts.fetch || globalThis.fetch;
76
+ }
77
+
78
+ /**
79
+ * Verify a captcha response.
80
+ *
81
+ * @param response - The response token from the captcha.
82
+ * @param opts - Optional options object:
83
+ * * `timeout`: The timeout in milliseconds. Defaults to 20 seconds.
84
+ * * `sitekey`: The sitekey to use for this request. Defaults to the sitekey passed to the constructor (if any).
85
+ * @returns A promise that always resolves to a `VerifyResult` object, which contains the fields `shouldAccept()` and `wasAbleToVerify()`. This promise never rejects.
86
+ */
87
+ public verifyCaptchaResponse(response: string, opts: { timeout?: number, sitekey?: string } = {}): Promise<VerifyResult> {
88
+ const siteverifyRequest: SiteverifyRequest = {
89
+ response,
90
+ };
91
+ if (this.sitekey || opts.sitekey) {
92
+ siteverifyRequest.sitekey = this.sitekey || opts.sitekey;
93
+ }
94
+
95
+ const result = new VerifyResult(this.strict);
96
+ let body: string;
97
+
98
+ try {
99
+ body = JSON.stringify(siteverifyRequest);
100
+ } catch (e) {
101
+ result.clientErrorType = FAILED_TO_ENCODE_ERROR_CODE;
102
+ return Promise.resolve(result);
103
+ }
104
+
105
+ const headers = {
106
+ "Content-Type": "application/json",
107
+ Accept: "application/json",
108
+ "X-Frc-Sdk": "friendly-captcha-javascript-sdk@" + SDK_VERSION,
109
+ "X-Api-Key": this.apiKey,
110
+ };
111
+
112
+ const timeout = opts?.timeout || 20_000;
113
+
114
+ return new Promise((resolve) => {
115
+ const controller = new AbortController();
116
+ const signal = controller.signal;
117
+ setTimeout(() => {
118
+ controller.abort();
119
+ result.clientErrorType = REQUEST_FAILED_TIMEOUT_ERROR_CODE;
120
+ resolve(result);
121
+ }, timeout);
122
+
123
+ this.fetch(this.siteverifyEndpoint, {
124
+ method: "POST",
125
+ headers,
126
+ body,
127
+ signal,
128
+ })
129
+ .then((response) => {
130
+ result.status = response.status;
131
+ if (response.status >= 400 && response.status < 500) {
132
+ result.clientErrorType = FAILED_DUE_TO_CLIENT_ERROR_CODE;
133
+ }
134
+ return response.json().catch(() => {
135
+ result.clientErrorType = FAILED_TO_DECODE_RESPONSE_ERROR_CODE;
136
+ resolve(result);
137
+ });
138
+ })
139
+ .then((json) => {
140
+ if (typeof json !== "object" || json === null) {
141
+ result.clientErrorType = FAILED_TO_DECODE_RESPONSE_ERROR_CODE;
142
+ resolve(result);
143
+ return;
144
+ }
145
+ result.response = json;
146
+ resolve(result);
147
+ })
148
+ .catch((e) => {
149
+ result.clientErrorType = REQUEST_FAILED_ERROR_CODE;
150
+ resolve(result);
151
+ });
152
+ });
153
+ }
154
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Failed to encode the captcha response. This means the captcha response was invalid and should never be accepted.
3
+ *
4
+ * @public
5
+ */
6
+ export const FAILED_TO_ENCODE_ERROR_CODE = "failed_to_encode_request";
7
+ /**
8
+ *
9
+ * The request couldn't be made, perhaps there is a network outage, DNS issue, or the API is unreachable.
10
+ *
11
+ * @public
12
+ */
13
+ export const REQUEST_FAILED_ERROR_CODE = "request_failed";
14
+ /**
15
+ *
16
+ * The request couldn't be made, perhaps there is a network outage, DNS issue, or the API is unreachable.
17
+ *
18
+ * @public
19
+ */
20
+ export const REQUEST_FAILED_TIMEOUT_ERROR_CODE = "request_failed_due_to_timeout";
21
+ /**
22
+ * An error occured on the client side that could've been prevented. This generally means your configuration is wrong.
23
+ *
24
+ * Check your API key and sitekey.
25
+ * @public
26
+ */
27
+ export const FAILED_DUE_TO_CLIENT_ERROR_CODE = "request_failed_due_to_client_error";
28
+ /**
29
+ * The response from the Friendly Captcha API could not be decoded.
30
+ *
31
+ * @public
32
+ */
33
+ export const FAILED_TO_DECODE_RESPONSE_ERROR_CODE = "verification_response_could_not_be_decoded";
34
+ /**
35
+ * @public
36
+ */
37
+ export type VerifyClientErrorCode =
38
+ | typeof FAILED_TO_ENCODE_ERROR_CODE
39
+ | typeof REQUEST_FAILED_ERROR_CODE
40
+ | typeof REQUEST_FAILED_TIMEOUT_ERROR_CODE
41
+ | typeof FAILED_DUE_TO_CLIENT_ERROR_CODE
42
+ | typeof FAILED_TO_DECODE_RESPONSE_ERROR_CODE;
@@ -0,0 +1,3 @@
1
+ export * from "./client.js";
2
+ export * from "./result.js";
3
+ export * from "./errors.js";
@@ -0,0 +1,151 @@
1
+ import type { SiteverifyErrorResponseErrorData, SiteverifyResponse } from "../api";
2
+ import {
3
+ FAILED_DUE_TO_CLIENT_ERROR_CODE,
4
+ FAILED_TO_DECODE_RESPONSE_ERROR_CODE,
5
+ FAILED_TO_ENCODE_ERROR_CODE,
6
+ REQUEST_FAILED_ERROR_CODE,
7
+ REQUEST_FAILED_TIMEOUT_ERROR_CODE,
8
+ VerifyClientErrorCode,
9
+ } from "./errors.js";
10
+
11
+ /**
12
+ * The result of a captcha siteverify request.
13
+ *
14
+ * @public
15
+ */
16
+ export class VerifyResult {
17
+
18
+ private strict: boolean;
19
+ /**
20
+ * The HTTP status code of the response.
21
+ * `-1` if there was no response.
22
+ */
23
+ public status: number = -1;
24
+
25
+ /**
26
+ * The response from the Friendly Captcha API, or null if the request was not made at all.
27
+ */
28
+ public response: SiteverifyResponse | null = null;
29
+ public clientErrorType: VerifyClientErrorCode | null = null;
30
+
31
+ constructor(strict: boolean) {
32
+ this.strict = strict;
33
+ }
34
+
35
+ /**
36
+ * @returns Whether strict mode was enabled for this verification.
37
+ */
38
+ public isStrict(): boolean {
39
+ return this.strict;
40
+ }
41
+
42
+ /**
43
+ * @returns Whether the captcha should be accepted.
44
+ * Note that this does not necessarily mean it was verified.
45
+ */
46
+ public shouldAccept(): boolean {
47
+ if (this.wasAbleToVerify()) {
48
+ // We want to reject in case we were not able to encode the captcha response sent by the client.
49
+ // This is because an attacker could send malformed data that can not be encoded, and if we would accept that
50
+ // they could circumvent the captcha.
51
+ if (this.isEncodeError()) {
52
+ return false;
53
+ }
54
+
55
+ return this.response!.success === true;
56
+ }
57
+ if (this.clientErrorType !== null) {
58
+ if (this.strict) {
59
+ // In strict mode we reject on any error.
60
+ return false;
61
+ } else if (
62
+ this.clientErrorType === REQUEST_FAILED_ERROR_CODE ||
63
+ this.clientErrorType === REQUEST_FAILED_TIMEOUT_ERROR_CODE ||
64
+ this.clientErrorType === FAILED_DUE_TO_CLIENT_ERROR_CODE ||
65
+ this.clientErrorType === FAILED_TO_DECODE_RESPONSE_ERROR_CODE
66
+ ) {
67
+ // In case of failures that are not the captcha response being invalid or rejected, we accept.
68
+ // This is because we don't want to lock out all users in case of a temporary network outage or a misconfiguration.
69
+ return true;
70
+ } else {
71
+ return false;
72
+ }
73
+ }
74
+
75
+ throw new Error(
76
+ "Implementation error in @friendlycaptcha/server-sdk shouldAccept: errorCode should never be undefined if success is false. " +
77
+ JSON.stringify(this),
78
+ );
79
+ }
80
+
81
+ /**
82
+ * @returns The reverse of `shouldAccept()`.
83
+ */
84
+ public shouldReject(): boolean {
85
+ return !this.shouldAccept();
86
+ }
87
+
88
+ /**
89
+ * Was unable to encode the captcha response. This means the captcha response was invalid and should never be accepted.
90
+ */
91
+ public isEncodeError(): boolean {
92
+ return this.clientErrorType === FAILED_TO_ENCODE_ERROR_CODE;
93
+ }
94
+
95
+ /**
96
+ * Something went wrong making the request to the Friendly Captcha API, perhaps there is a network connection issue?
97
+ */
98
+ public isRequestOrTimeoutError(): boolean {
99
+ return this.clientErrorType === REQUEST_FAILED_ERROR_CODE || this.clientErrorType === REQUEST_FAILED_TIMEOUT_ERROR_CODE;
100
+ }
101
+
102
+ /**
103
+ * Something went wrong decoding the response from the Friendly Captcha API.
104
+ */
105
+ public isDecodeError(): boolean {
106
+ return this.clientErrorType === FAILED_TO_DECODE_RESPONSE_ERROR_CODE;
107
+ }
108
+
109
+ /**
110
+ * Something went wrong on the client side, this generally means your configuration is wrong.
111
+ * Check your secrets (API key) and sitekey.
112
+ *
113
+ * See `getResponseError()` for more information.
114
+ */
115
+ public isClientError(): boolean {
116
+ return this.clientErrorType === FAILED_DUE_TO_CLIENT_ERROR_CODE;
117
+ }
118
+ /**
119
+ * @returns The response from the Friendly Captcha API, or null if the request was not made at all.
120
+ */
121
+ public getResponse(): SiteverifyResponse | null {
122
+ return this.response;
123
+ }
124
+
125
+ /**
126
+ * @returns The `error` field form the response, or null if it is not present.
127
+ */
128
+ public getResponseError(): SiteverifyErrorResponseErrorData | null {
129
+ if (!this.response || this.response.success) return null;
130
+ return this.response.error;
131
+ }
132
+
133
+ public getErrorCode(): VerifyClientErrorCode | null {
134
+ return this.clientErrorType;
135
+ }
136
+
137
+ /**
138
+ * Whether the request to verify the captcha was completed. In other words: the API responded with status 200.'
139
+ * If this is false, you should notify yourself and use `getErrorCode()` and `getResponseError()` to see what is wrong.
140
+ */
141
+ public wasAbleToVerify(): boolean {
142
+ // If we failed to encode, we actually consider `wasAbleToVerify` to be true. This is because we don't want to
143
+ // alert on failed encoding: an attacker could send such malformed data that it fails to encode.
144
+ if (this.isEncodeError()) {
145
+ return true;
146
+ }
147
+
148
+ // We got a status 200, and we were able to actually make the request and decode its response.
149
+ return this.status === 200 && !this.isRequestOrTimeoutError() && !this.isDecodeError();
150
+ }
151
+ }
@@ -0,0 +1,3 @@
1
+ // This file is auto-generated by scripts/updateVersion.mjs
2
+ // Do not edit this file directly
3
+ export const SDK_VERSION = "0.1.0";
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from "./api/index.js";
2
+ export * from "./client/index.js";
@@ -0,0 +1,48 @@
1
+ import test from "ava";
2
+ import { FriendlyCaptchaClient } from "../../src/client/index.js";
3
+
4
+ // More of a test pre-assertion than anything else.
5
+ test("globalThis.fetch is present", (t) => {
6
+ t.assert(globalThis.fetch);
7
+ });
8
+
9
+ test("client without API key throws", (t) => {
10
+ t.throws(() => {
11
+ const client = new FriendlyCaptchaClient({} as any);
12
+ });
13
+ });
14
+
15
+ test("client with API key does not throw", (t) => {
16
+ const client = new FriendlyCaptchaClient({ apiKey: "test" });
17
+ t.assert(client);
18
+ });
19
+
20
+ test("unencodable response gets rejected", async (t) => {
21
+ const circularReference = { self: undefined as any };
22
+ circularReference.self = circularReference;
23
+
24
+ const client = new FriendlyCaptchaClient({ apiKey: "my-api-key" });
25
+ const result = await client.verifyCaptchaResponse(circularReference as any);
26
+
27
+ t.true(result.isEncodeError());
28
+ t.false(result.isClientError());
29
+ t.false(result.isRequestOrTimeoutError());
30
+ t.false(result.isDecodeError());
31
+
32
+ t.false(result.shouldAccept());
33
+ t.true(result.shouldReject());
34
+ });
35
+
36
+ test("request failure gets accepted", async (t) => {
37
+ const client = new FriendlyCaptchaClient({ apiKey: "my-api-key", siteverifyEndpoint: "http://localhost:9999" }); // Assumes nothing is listening on port 9999
38
+ const result = await client.verifyCaptchaResponse("something");
39
+
40
+ t.false(result.isEncodeError());
41
+ t.false(result.isClientError());
42
+ t.true(result.isRequestOrTimeoutError());
43
+ t.false(result.isDecodeError());
44
+
45
+ t.true(result.shouldAccept());
46
+ t.false(result.shouldReject());
47
+ });
48
+
@@ -0,0 +1,44 @@
1
+ import test from "ava";
2
+ import request from "sync-request";
3
+ import { FriendlyCaptchaClient } from "../../src/client/index.js";
4
+
5
+ // Tests served from the SDK test mock server
6
+ const mockServerUrl = "http://localhost:1090";
7
+
8
+ type TestCase = {
9
+ name: string;
10
+ response: string;
11
+ strict: boolean;
12
+
13
+ siteverify_response: any;
14
+ siteverify_status_code: number;
15
+
16
+ expectation: {
17
+ should_accept: boolean;
18
+ was_able_to_verify: boolean;
19
+ is_client_error: boolean;
20
+ };
21
+ };
22
+
23
+ type TestCasesFile = {
24
+ version: number;
25
+ tests: TestCase[];
26
+ };
27
+
28
+ const casesFile: TestCasesFile = JSON.parse(request("GET", `${mockServerUrl}/api/v1/tests`).getBody("utf8"));
29
+
30
+ for (const testCase of casesFile.tests) {
31
+ test(testCase.name, async (t) => {
32
+ const client = new FriendlyCaptchaClient({
33
+ apiKey: "some-api-key",
34
+ siteverifyEndpoint: `${mockServerUrl}/api/v2/captcha/siteverify`,
35
+ strict: testCase.strict,
36
+ });
37
+
38
+ const result = await client.verifyCaptchaResponse(testCase.response);
39
+
40
+ t.is(result.shouldAccept(), testCase.expectation.should_accept);
41
+ t.is(result.wasAbleToVerify(), testCase.expectation.was_able_to_verify);
42
+ t.is(result.isClientError(), testCase.expectation.is_client_error);
43
+ });
44
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "moduleResolution": "node",
4
+ "target": "es6",
5
+ "module":"es2015",
6
+ "lib": ["es2015", "es2016", "es2017", "dom"],
7
+ "strict": true,
8
+ "sourceMap": true,
9
+ "declaration": true,
10
+ "allowSyntheticDefaultImports": true,
11
+ "outDir": "build/",
12
+ "typeRoots": [
13
+ "node_modules/@types"
14
+ ]
15
+ },
16
+ "include": [
17
+ "src",
18
+ "test"
19
+ ]
20
+ }