@prosopo/better-auth-procaptcha-plugin 0.0.0-stage → 0.0.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 (54) hide show
  1. package/.turbo/turbo-build$colon$types.log +54 -0
  2. package/LICENSE +201 -0
  3. package/coverage/base.css +362 -0
  4. package/coverage/block-navigation.js +82 -0
  5. package/coverage/client.ts.html +316 -0
  6. package/coverage/clover.xml +151 -0
  7. package/coverage/constants.ts.html +190 -0
  8. package/coverage/coverage-final.json +6 -0
  9. package/coverage/favicon.png +0 -0
  10. package/coverage/index.html +176 -0
  11. package/coverage/index.ts.html +706 -0
  12. package/coverage/path.ts.html +355 -0
  13. package/coverage/prettify.css +101 -0
  14. package/coverage/prettify.js +937 -0
  15. package/coverage/siteverify.ts.html +559 -0
  16. package/coverage/sort-arrow-sprite.png +0 -0
  17. package/coverage/sorter.js +205 -0
  18. package/dist/cjs/client.cjs +32 -0
  19. package/dist/cjs/constants.cjs +22 -0
  20. package/dist/cjs/index.cjs +80 -0
  21. package/dist/cjs/path.cjs +41 -0
  22. package/dist/cjs/siteverify.cjs +68 -0
  23. package/dist/esm/_virtual/_rolldown/runtime.js +3 -0
  24. package/dist/esm/client.js +31 -0
  25. package/dist/esm/constants.js +18 -0
  26. package/dist/esm/index.js +75 -0
  27. package/dist/esm/path.js +40 -0
  28. package/dist/esm/siteverify.js +68 -0
  29. package/dist/types/client.d.ts +19 -0
  30. package/dist/types/client.d.ts.map +1 -0
  31. package/dist/types/constants.d.ts +12 -0
  32. package/dist/types/constants.d.ts.map +1 -0
  33. package/dist/types/index.d.ts +28 -0
  34. package/dist/types/index.d.ts.map +1 -0
  35. package/dist/types/path.d.ts +4 -0
  36. package/dist/types/path.d.ts.map +1 -0
  37. package/dist/types/siteverify.d.ts +40 -0
  38. package/dist/types/siteverify.d.ts.map +1 -0
  39. package/dist/types/tests/client.unit.test.d.ts +2 -0
  40. package/dist/types/tests/client.unit.test.d.ts.map +1 -0
  41. package/dist/types/tests/path.unit.test.d.ts +2 -0
  42. package/dist/types/tests/path.unit.test.d.ts.map +1 -0
  43. package/dist/types/tests/procaptcha.unit.test.d.ts +2 -0
  44. package/dist/types/tests/procaptcha.unit.test.d.ts.map +1 -0
  45. package/dist/types/tests/siteVerifyMock.d.ts +17 -0
  46. package/dist/types/tests/siteVerifyMock.d.ts.map +1 -0
  47. package/dist/types/tests/siteverify.unit.test.d.ts +2 -0
  48. package/dist/types/tests/siteverify.unit.test.d.ts.map +1 -0
  49. package/package.json +68 -3
  50. package/readme.md +145 -0
  51. package/vite.cjs.config.ts +24 -0
  52. package/vite.esm.config.ts +23 -0
  53. package/vite.test.config.ts +19 -0
  54. package/README.md +0 -3
@@ -0,0 +1,68 @@
1
+ import { DEFAULT_TIMEOUT_MS, PROCAPTCHA_SITEVERIFY_URL } from "./constants.js";
2
+ //#region src/siteverify.ts
3
+ var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
4
+ var isSiteVerifyResponse = (value) => isRecord(value) && typeof value.status === "string" && typeof value.verified === "boolean" && (value.score === void 0 || typeof value.score === "number") && (value.reason === void 0 || typeof value.reason === "string");
5
+ var isSiteVerifyErrorResponse = (value) => isRecord(value) && isRecord(value.error) && typeof value.error.key === "string";
6
+ var readJson = async (response) => {
7
+ try {
8
+ return await response.json();
9
+ } catch {
10
+ return;
11
+ }
12
+ };
13
+ var isTransientStatus = (status) => status === 429 || status >= 500;
14
+ var verifyProcaptchaToken = async ({ secretKey, token, ip, email, siteVerifyURL = PROCAPTCHA_SITEVERIFY_URL, timeoutMs = DEFAULT_TIMEOUT_MS, fetch: fetchImpl = fetch }) => {
15
+ const body = {
16
+ secret: secretKey,
17
+ token,
18
+ ...ip ? { ip } : {},
19
+ ...email ? { email } : {}
20
+ };
21
+ let response;
22
+ try {
23
+ response = await fetchImpl(siteVerifyURL, {
24
+ method: "POST",
25
+ headers: {
26
+ "Content-Type": "application/json",
27
+ Accept: "application/json"
28
+ },
29
+ body: JSON.stringify(body),
30
+ signal: AbortSignal.timeout(timeoutMs)
31
+ });
32
+ } catch (error) {
33
+ return {
34
+ outcome: "error",
35
+ transient: true,
36
+ message: error instanceof Error ? error.message : String(error)
37
+ };
38
+ }
39
+ const payload = await readJson(response);
40
+ if (!response.ok) {
41
+ const errorBody = isSiteVerifyErrorResponse(payload) ? payload.error : void 0;
42
+ return {
43
+ outcome: "error",
44
+ transient: isTransientStatus(response.status),
45
+ status: response.status,
46
+ ...errorBody ? { key: errorBody.key } : {},
47
+ message: typeof errorBody?.message === "string" ? errorBody.message : `siteverify responded with HTTP ${response.status}`
48
+ };
49
+ }
50
+ if (!isSiteVerifyResponse(payload)) return {
51
+ outcome: "error",
52
+ transient: true,
53
+ status: response.status,
54
+ message: "siteverify returned an unexpected response body"
55
+ };
56
+ const score = payload.score === void 0 ? {} : { score: payload.score };
57
+ if (!payload.verified) return {
58
+ outcome: "rejected",
59
+ ...payload.reason ? { reason: payload.reason } : {},
60
+ ...score
61
+ };
62
+ return {
63
+ outcome: "verified",
64
+ ...score
65
+ };
66
+ };
67
+ //#endregion
68
+ export { verifyProcaptchaToken };
@@ -0,0 +1,19 @@
1
+ import type { procaptcha } from "./index.js";
2
+ export interface ProcaptchaClientOptions {
3
+ getToken?: () => string | undefined | Promise<string | undefined>;
4
+ endpoints?: readonly string[];
5
+ headerName?: string;
6
+ }
7
+ export declare const procaptchaHeaders: (token: string, headerName?: string) => Record<string, string>;
8
+ export declare const procaptchaClient: (options?: ProcaptchaClientOptions) => {
9
+ id: "procaptcha";
10
+ $InferServerPlugin: ReturnType<typeof procaptcha>;
11
+ fetchPlugins: {
12
+ id: string;
13
+ name: string;
14
+ hooks: {
15
+ onRequest: <T extends Record<string, any>>(context: import("better-auth/client").RequestContext<T>) => Promise<import("better-auth/client").RequestContext<T>>;
16
+ };
17
+ }[];
18
+ };
19
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/client.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAG7C,MAAM,WAAW,uBAAuB;IAMvC,QAAQ,CAAC,EAAE,MAAM,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IAElE,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAE9B,UAAU,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,eAAO,MAAM,iBAAiB,UACtB,MAAM,eACD,MAAM,KAChB,MAAM,CAAC,MAAM,EAAE,MAAM,CAA8B,CAAC;AAQvD,eAAO,MAAM,gBAAgB,aAAa,uBAAuB;;wBASrC,UAAU,CAAC,OAAO,UAAU,CAAC;;;;;;;;CAyBxD,CAAC"}
@@ -0,0 +1,12 @@
1
+ export declare const PROCAPTCHA_SITEVERIFY_URL = "https://api.prosopo.io/siteverify";
2
+ export declare const PROCAPTCHA_HEADER = "x-captcha-response";
3
+ export declare const DEFAULT_ENDPOINTS: readonly string[];
4
+ export declare const DEFAULT_TIMEOUT_MS = 10000;
5
+ export declare const PROCAPTCHA_ERROR_CODES: {
6
+ PROCAPTCHA_MISSING_RESPONSE: import("better-auth").RawError<"PROCAPTCHA_MISSING_RESPONSE">;
7
+ PROCAPTCHA_VERIFICATION_FAILED: import("better-auth").RawError<"PROCAPTCHA_VERIFICATION_FAILED">;
8
+ PROCAPTCHA_SERVICE_UNAVAILABLE: import("better-auth").RawError<"PROCAPTCHA_SERVICE_UNAVAILABLE">;
9
+ PROCAPTCHA_UNKNOWN_ERROR: import("better-auth").RawError<"PROCAPTCHA_UNKNOWN_ERROR">;
10
+ };
11
+ export type ProcaptchaErrorCode = keyof typeof PROCAPTCHA_ERROR_CODES;
12
+ //# sourceMappingURL=constants.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../src/constants.ts"],"names":[],"mappings":"AAeA,eAAO,MAAM,yBAAyB,sCAAsC,CAAC;AAE7E,eAAO,MAAM,iBAAiB,uBAAuB,CAAC;AAEtD,eAAO,MAAM,iBAAiB,EAAE,SAAS,MAAM,EAI9C,CAAC;AAEF,eAAO,MAAM,kBAAkB,QAAS,CAAC;AAEzC,eAAO,MAAM,sBAAsB;;;;;CAKjC,CAAC;AAEH,MAAM,MAAM,mBAAmB,GAAG,MAAM,OAAO,sBAAsB,CAAC"}
@@ -0,0 +1,28 @@
1
+ import type { AuthContext } from "better-auth";
2
+ export { DEFAULT_ENDPOINTS, PROCAPTCHA_ERROR_CODES, PROCAPTCHA_HEADER, PROCAPTCHA_SITEVERIFY_URL, type ProcaptchaErrorCode, } from "./constants.js";
3
+ export { verifyProcaptchaToken, type SiteVerifyErrorResponse, type SiteVerifyRequestBody, type SiteVerifyResponse, type VerifyResult, type VerifyTokenParams, } from "./siteverify.js";
4
+ export interface ProcaptchaOptions {
5
+ secretKey: string;
6
+ endpoints?: readonly string[];
7
+ siteVerifyURL?: string;
8
+ headerName?: string;
9
+ sendIP?: boolean;
10
+ sendEmailDomain?: boolean;
11
+ maxScore?: number;
12
+ timeoutMs?: number;
13
+ fetch?: typeof fetch;
14
+ }
15
+ export declare const procaptcha: (options: ProcaptchaOptions) => {
16
+ id: "procaptcha";
17
+ $ERROR_CODES: {
18
+ PROCAPTCHA_MISSING_RESPONSE: import("better-auth").RawError<"PROCAPTCHA_MISSING_RESPONSE">;
19
+ PROCAPTCHA_VERIFICATION_FAILED: import("better-auth").RawError<"PROCAPTCHA_VERIFICATION_FAILED">;
20
+ PROCAPTCHA_SERVICE_UNAVAILABLE: import("better-auth").RawError<"PROCAPTCHA_SERVICE_UNAVAILABLE">;
21
+ PROCAPTCHA_UNKNOWN_ERROR: import("better-auth").RawError<"PROCAPTCHA_UNKNOWN_ERROR">;
22
+ };
23
+ onRequest: (request: Request, ctx: AuthContext) => Promise<{
24
+ response: Response;
25
+ } | undefined>;
26
+ options: ProcaptchaOptions;
27
+ };
28
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EACX,WAAW,EAGX,MAAM,aAAa,CAAC;AAarB,OAAO,EACN,iBAAiB,EACjB,sBAAsB,EACtB,iBAAiB,EACjB,yBAAyB,EACzB,KAAK,mBAAmB,GACxB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACN,qBAAqB,EACrB,KAAK,uBAAuB,EAC5B,KAAK,qBAAqB,EAC1B,KAAK,kBAAkB,EACvB,KAAK,YAAY,EACjB,KAAK,iBAAiB,GACtB,MAAM,iBAAiB,CAAC;AAEzB,MAAM,WAAW,iBAAiB;IAEjC,SAAS,EAAE,MAAM,CAAC;IAOlB,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAE9B,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB,UAAU,CAAC,EAAE,MAAM,CAAC;IAOpB,MAAM,CAAC,EAAE,OAAO,CAAC;IAOjB,eAAe,CAAC,EAAE,OAAO,CAAC;IAM1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;CACrB;AA8CD,eAAO,MAAM,UAAU,YAAa,iBAAiB;;;;;;;;yBAUzC,OAAO,OACX,WAAW,KACd,OAAO,CAAC;QAAE,QAAQ,EAAE,QAAQ,CAAA;KAAE,GAAG,SAAS,CAAC;;CAgE/C,CAAC"}
@@ -0,0 +1,4 @@
1
+ export declare const normalizeAuthPath: (pathname: string, basePath: string) => string;
2
+ export type PathMatcher = (path: string) => boolean;
3
+ export declare const createPathMatcher: (endpoints: readonly string[]) => PathMatcher;
4
+ //# sourceMappingURL=path.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"path.d.ts","sourceRoot":"","sources":["../../src/path.ts"],"names":[],"mappings":"AAaA,eAAO,MAAM,iBAAiB,aACnB,MAAM,YACN,MAAM,KACd,MAOF,CAAC;AAoDF,MAAM,MAAM,WAAW,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;AAEpD,eAAO,MAAM,iBAAiB,cAClB,SAAS,MAAM,EAAE,KAC1B,WAUF,CAAC"}
@@ -0,0 +1,40 @@
1
+ import type { VerificationResponse } from "@prosopo/types";
2
+ export interface SiteVerifyRequestBody {
3
+ secret: string;
4
+ token: string;
5
+ ip?: string;
6
+ email?: string;
7
+ }
8
+ export type SiteVerifyResponse = Omit<VerificationResponse, "error" | "sessionId">;
9
+ export interface SiteVerifyErrorResponse {
10
+ error: {
11
+ code: number;
12
+ key: string;
13
+ message: string | unknown[];
14
+ };
15
+ }
16
+ export type VerifyResult = {
17
+ outcome: "verified";
18
+ score?: number;
19
+ } | {
20
+ outcome: "rejected";
21
+ reason?: string;
22
+ score?: number;
23
+ } | {
24
+ outcome: "error";
25
+ transient: boolean;
26
+ status?: number;
27
+ key?: string;
28
+ message: string;
29
+ };
30
+ export interface VerifyTokenParams {
31
+ secretKey: string;
32
+ token: string;
33
+ ip?: string;
34
+ email?: string;
35
+ siteVerifyURL?: string;
36
+ timeoutMs?: number;
37
+ fetch?: typeof fetch;
38
+ }
39
+ export declare const verifyProcaptchaToken: ({ secretKey, token, ip, email, siteVerifyURL, timeoutMs, fetch: fetchImpl, }: VerifyTokenParams) => Promise<VerifyResult>;
40
+ //# sourceMappingURL=siteverify.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"siteverify.d.ts","sourceRoot":"","sources":["../../src/siteverify.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAG3D,MAAM,WAAW,qBAAqB;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,MAAM,kBAAkB,GAAG,IAAI,CACpC,oBAAoB,EACpB,OAAO,GAAG,WAAW,CACrB,CAAC;AAEF,MAAM,WAAW,uBAAuB;IACvC,KAAK,EAAE;QACN,IAAI,EAAE,MAAM,CAAC;QACb,GAAG,EAAE,MAAM,CAAC;QACZ,OAAO,EAAE,MAAM,GAAG,OAAO,EAAE,CAAC;KAC5B,CAAC;CACF;AAED,MAAM,MAAM,YAAY,GACrB;IAAE,OAAO,EAAE,UAAU,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GACvC;IAAE,OAAO,EAAE,UAAU,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GACxD;IACA,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACf,CAAC;AAEL,MAAM,WAAW,iBAAiB;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;CACrB;AA+BD,eAAO,MAAM,qBAAqB,iFAQ/B,iBAAiB,KAAG,OAAO,CAAC,YAAY,CA+D1C,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=client.unit.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.unit.test.d.ts","sourceRoot":"","sources":["../../../src/tests/client.unit.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=path.unit.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"path.unit.test.d.ts","sourceRoot":"","sources":["../../../src/tests/path.unit.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=procaptcha.unit.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"procaptcha.unit.test.d.ts","sourceRoot":"","sources":["../../../src/tests/procaptcha.unit.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,17 @@
1
+ import { type Mock } from "vitest";
2
+ import type { SiteVerifyRequestBody, SiteVerifyResponse } from "../siteverify.js";
3
+ export interface SiteVerifyCall {
4
+ url: string;
5
+ headers: Headers;
6
+ body: SiteVerifyRequestBody;
7
+ }
8
+ export type SiteVerifyResponder = (body: SiteVerifyRequestBody) => Response | Promise<Response>;
9
+ export interface SiteVerifyMock {
10
+ fetch: Mock<typeof fetch>;
11
+ calls: SiteVerifyCall[];
12
+ }
13
+ export declare const verifiedResponse: (overrides?: Partial<SiteVerifyResponse>) => Response;
14
+ export declare const rejectedResponse: (reason?: string) => Response;
15
+ export declare const errorEnvelope: (status: number, key: string, message: string) => Response;
16
+ export declare const mockSiteVerify: (respond?: SiteVerifyResponder) => SiteVerifyMock;
17
+ //# sourceMappingURL=siteVerifyMock.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"siteVerifyMock.d.ts","sourceRoot":"","sources":["../../../src/tests/siteVerifyMock.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,KAAK,IAAI,EAAM,MAAM,QAAQ,CAAC;AACvC,OAAO,KAAK,EAEX,qBAAqB,EACrB,kBAAkB,EAClB,MAAM,kBAAkB,CAAC;AAE1B,MAAM,WAAW,cAAc;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,qBAAqB,CAAC;CAC5B;AAED,MAAM,MAAM,mBAAmB,GAAG,CACjC,IAAI,EAAE,qBAAqB,KACvB,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;AAElC,MAAM,WAAW,cAAc;IAC9B,KAAK,EAAE,IAAI,CAAC,OAAO,KAAK,CAAC,CAAC;IAC1B,KAAK,EAAE,cAAc,EAAE,CAAC;CACxB;AAUD,eAAO,MAAM,gBAAgB,eACjB,OAAO,CAAC,kBAAkB,CAAC,KACpC,QAOF,CAAC;AAEF,eAAO,MAAM,gBAAgB,YAAa,MAAM,KAAG,QAOlD,CAAC;AAEF,eAAO,MAAM,aAAa,WACjB,MAAM,OACT,MAAM,WACF,MAAM,KACb,QAKF,CAAC;AAEF,eAAO,MAAM,cAAc,aACjB,mBAAmB,KAC1B,cAeF,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=siteverify.unit.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"siteverify.unit.test.d.ts","sourceRoot":"","sources":["../../../src/tests/siteverify.unit.test.ts"],"names":[],"mappings":""}
package/package.json CHANGED
@@ -1,6 +1,71 @@
1
1
  {
2
2
  "name": "@prosopo/better-auth-procaptcha-plugin",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.0.0",
4
+ "description": "Prosopo Procaptcha bot protection for Better Auth. Verifies a Procaptcha token before sign-up, sign-in and other auth endpoints run.",
5
+ "type": "module",
6
+ "engines": {
7
+ "node": "^24"
8
+ },
9
+ "main": "dist/esm/index.js",
10
+ "types": "dist/types/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/types/index.d.ts",
14
+ "import": "./dist/esm/index.js",
15
+ "require": "./dist/cjs/index.cjs"
16
+ },
17
+ "./client": {
18
+ "types": "./dist/types/client.d.ts",
19
+ "import": "./dist/esm/client.js",
20
+ "require": "./dist/cjs/client.cjs"
21
+ }
22
+ },
23
+ "keywords": [
24
+ "better-auth",
25
+ "captcha",
26
+ "procaptcha",
27
+ "prosopo",
28
+ "bot-protection"
29
+ ],
30
+ "dependencies": {
31
+ "@prosopo/types": "5.30.0"
32
+ },
33
+ "peerDependencies": {
34
+ "better-auth": "^1.7.0"
35
+ },
36
+ "devDependencies": {
37
+ "@prosopo/config": "3.5.5",
38
+ "@types/node": "22.10.2",
39
+ "@vitest/coverage-v8": "5.0.4",
40
+ "better-auth": "1.7.15",
41
+ "del-cli": "6.0.0",
42
+ "tslib": "2.7.0",
43
+ "typescript": "5.6.2",
44
+ "vite": "8.1.6",
45
+ "vitest": "5.0.4"
46
+ },
47
+ "author": "PROSOPO LIMITED <info@prosopo.io>",
48
+ "license": "Apache-2.0",
49
+ "publishConfig": {
50
+ "access": "public",
51
+ "registry": "https://registry.npmjs.org"
52
+ },
53
+ "bugs": {
54
+ "url": "https://github.com/prosopo/captcha/issues"
55
+ },
56
+ "homepage": "https://github.com/prosopo/captcha/tree/main/integration/frameworks/better-auth/better-auth-procaptcha-plugin#readme",
57
+ "sideEffects": false,
58
+ "repository": {
59
+ "type": "git",
60
+ "url": "git+https://github.com/prosopo/captcha.git",
61
+ "directory": "integration/frameworks/better-auth/better-auth-procaptcha-plugin"
62
+ },
63
+ "scripts": {
64
+ "clean": "del-cli --verbose dist",
65
+ "build:esm": "vite build --config vite.esm.config.ts --mode ${NODE_ENV:-development}",
66
+ "build:tsc": "tsc --build --verbose",
67
+ "build:cjs": "NODE_ENV=${NODE_ENV:-development}; vite build --config vite.cjs.config.ts --mode $NODE_ENV",
68
+ "build:types": "tsc --project tsconfig.types.json",
69
+ "test": "NODE_ENV=${NODE_ENV:-test}; pnpm exec vitest run --config ./vite.test.config.ts"
70
+ }
6
71
  }
package/readme.md ADDED
@@ -0,0 +1,145 @@
1
+ # @prosopo/better-auth-procaptcha-plugin
2
+
3
+ [Prosopo Procaptcha](https://prosopo.io) bot protection for [Better Auth](https://www.better-auth.com).
4
+
5
+ The server plugin checks a Procaptcha token before Better Auth runs a protected endpoint, such as sign-up, sign-in or
6
+ password reset. Requests without a valid token are rejected before they reach your database.
7
+
8
+ - Works with Better Auth `^1.7.0`. Tested against 1.7.0 and 1.7.15.
9
+ - Uses only Better Auth's public plugin API (`onRequest`, `getIP` from `better-auth/api`, client `fetchPlugins`).
10
+ - Verifies tokens with Prosopo's [siteverify API](https://docs.prosopo.io/en/basics/server-side-verification/).
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ npm install @prosopo/better-auth-procaptcha-plugin
16
+ ```
17
+
18
+ You need a site key and a secret key from the [Prosopo portal](https://portal.prosopo.io).
19
+
20
+ ## Server
21
+
22
+ ```ts
23
+ import { betterAuth } from "better-auth";
24
+ import { procaptcha } from "@prosopo/better-auth-procaptcha-plugin";
25
+
26
+ export const auth = betterAuth({
27
+ plugins: [
28
+ procaptcha({
29
+ secretKey: process.env.PROSOPO_SECRET_KEY as string,
30
+ }),
31
+ ],
32
+ });
33
+ ```
34
+
35
+ By default the plugin protects `/sign-up/email`, `/sign-in/email` and `/request-password-reset`, the same endpoints as
36
+ Better Auth's built-in captcha plugin.
37
+
38
+ For each protected request the plugin:
39
+
40
+ 1. reads the token from the `x-captcha-response` header,
41
+ 2. sends `{ secret, token, ip }` to `https://api.prosopo.io/siteverify`,
42
+ 3. lets the request through only if the answer is `verified: true`.
43
+
44
+ ### Options
45
+
46
+ | Option | Default | What it does |
47
+ | ----------------- | ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
48
+ | `secretKey` | (required) | Your site's secret key. |
49
+ | `endpoints` | `["/sign-up/email", "/sign-in/email", "/request-password-reset"]` | Paths to protect, without the Better Auth base path. `*` matches one path segment, `**` matches any depth, for example `/sign-in/*`. |
50
+ | `siteVerifyURL` | `https://api.prosopo.io/siteverify` | Use `https://eu-api.prosopo.io/siteverify` or `https://us-api.prosopo.io/siteverify` to keep verification in one region. |
51
+ | `headerName` | `x-captcha-response` | The request header that carries the token. |
52
+ | `sendIP` | `true` | Send the visitor's IP so Prosopo can compare it with the IP that solved the captcha. The IP comes from Better Auth's `advanced.ipAddress` settings. |
53
+ | `sendEmailDomain` | `false` | Send the domain of the `email` field in the request body (not the full address) for Prosopo's disposable and spam email filter. |
54
+ | `maxScore` | none | Reject verified tokens whose risk score is above this number. Scores run from 0 (human) to 1 (bot) and are only returned on plans that include them. |
55
+ | `timeoutMs` | `10000` | How long to wait for siteverify before failing the request. |
56
+ | `fetch` | global `fetch` | A custom fetch, for example one that goes through a proxy. |
57
+
58
+ ### Errors
59
+
60
+ The plugin answers with a JSON body `{ "code": "...", "message": "..." }`.
61
+
62
+ | Status | `code` | When |
63
+ | ------ | -------------------------------- | ---------------------------------------------------------------------------------------------- |
64
+ | 400 | `PROCAPTCHA_MISSING_RESPONSE` | The request has no token. |
65
+ | 403 | `PROCAPTCHA_VERIFICATION_FAILED` | Prosopo did not verify the token, or its score is above `maxScore`. |
66
+ | 503 | `PROCAPTCHA_SERVICE_UNAVAILABLE` | siteverify could not be reached, timed out, rate limited the request or returned a 5xx. |
67
+ | 500 | `PROCAPTCHA_UNKNOWN_ERROR` | The plugin is misconfigured: no `secretKey`, or siteverify rejected the secret or the request. |
68
+
69
+ The plugin fails closed: if Prosopo cannot be reached, protected requests are rejected. The reason for a 500 or 503 is
70
+ written to the Better Auth logger.
71
+
72
+ ### Verifying a token yourself
73
+
74
+ `verifyProcaptchaToken` is the same check the plugin runs, for use in your own endpoints:
75
+
76
+ ```ts
77
+ import { verifyProcaptchaToken } from "@prosopo/better-auth-procaptcha-plugin";
78
+
79
+ const result = await verifyProcaptchaToken({ secretKey, token, ip });
80
+ if (result.outcome !== "verified") {
81
+ // reject
82
+ }
83
+ ```
84
+
85
+ ## Client
86
+
87
+ Render the Procaptcha widget as described in the [client-side rendering guide](https://docs.prosopo.io/en/basics/client-side-rendering/),
88
+ or with one of the framework wrappers, and keep the token it gives you.
89
+
90
+ Then pass the token to Better Auth, either per request:
91
+
92
+ ```ts
93
+ import { createAuthClient } from "better-auth/client";
94
+ import {
95
+ procaptchaClient,
96
+ procaptchaHeaders,
97
+ } from "@prosopo/better-auth-procaptcha-plugin/client";
98
+
99
+ const authClient = createAuthClient({ plugins: [procaptchaClient()] });
100
+
101
+ await authClient.signIn.email({
102
+ email,
103
+ password,
104
+ fetchOptions: { headers: procaptchaHeaders(token) },
105
+ });
106
+ ```
107
+
108
+ or automatically, by giving the client plugin a function that returns the current token:
109
+
110
+ ```ts
111
+ let token: string | undefined;
112
+
113
+ window.procaptcha.render(document.getElementById("procaptcha-container"), {
114
+ siteKey: "YOUR_SITE_KEY",
115
+ callback: (value: string) => {
116
+ token = value;
117
+ },
118
+ "expired-callback": () => {
119
+ token = undefined;
120
+ },
121
+ });
122
+
123
+ const authClient = createAuthClient({
124
+ plugins: [procaptchaClient({ getToken: () => token })],
125
+ });
126
+ ```
127
+
128
+ The client plugin only adds the header to requests for the protected endpoints. If you change `endpoints` or
129
+ `headerName` on the server, pass the same values to `procaptchaClient`.
130
+
131
+ A token can be verified only once and expires a few minutes after it is issued, so the visitor needs to solve the
132
+ captcha again after each attempt.
133
+
134
+ ## Testing
135
+
136
+ Prosopo's [test site keys](https://docs.prosopo.io/en/basics/test-keys/) work with this plugin. With the always-pass
137
+ site key, siteverify returns `verified: true` for any secret, so you can run your sign-up and sign-in flows end to end
138
+ in CI without solving a captcha.
139
+
140
+ When Better Auth runs in development or test mode and no IP header is present, it reports the visitor's IP as
141
+ `127.0.0.1`. Set `sendIP: false` locally if you use IP validation rules on your site.
142
+
143
+ ## License
144
+
145
+ Apache-2.0
@@ -0,0 +1,24 @@
1
+ // Copyright 2021-2026 Prosopo (UK) Ltd.
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+
15
+ import path from "node:path";
16
+ import { ViteCommonJSConfig } from "@prosopo/config";
17
+
18
+ export default function () {
19
+ return ViteCommonJSConfig(
20
+ path.basename("."),
21
+ path.resolve("./tsconfig.json"),
22
+ ["src/index.ts", "src/client.ts"],
23
+ );
24
+ }
@@ -0,0 +1,23 @@
1
+ // Copyright 2021-2026 Prosopo (UK) Ltd.
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+
15
+ import path from "node:path";
16
+ import { ViteEsmConfig } from "@prosopo/config";
17
+
18
+ export default function () {
19
+ return ViteEsmConfig(path.basename("."), path.resolve("./tsconfig.json"), [
20
+ "src/index.ts",
21
+ "src/client.ts",
22
+ ]);
23
+ }
@@ -0,0 +1,19 @@
1
+ // Copyright 2021-2026 Prosopo (UK) Ltd.
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+
15
+ import { ViteTestConfig } from "@prosopo/config";
16
+
17
+ process.env.NODE_ENV = "test";
18
+
19
+ export default ViteTestConfig();
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.