@internetarchive/recaptcha-manager 0.0.1-alpha.1 → 0.0.1-alpha.2

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.
@@ -0,0 +1,87 @@
1
+ /**
2
+ * This encompasses a widget for a given site key.
3
+ */
4
+ export class RecaptchaWidget {
5
+ constructor(config, recaptchaParams) {
6
+ this.widgetId = null;
7
+ this.siteKey = config.siteKey;
8
+ this.grecaptchaLibrary = config.grecaptchaLibrary;
9
+ const container = this.createContainer();
10
+ this.setup(container, recaptchaParams);
11
+ }
12
+ /** @inheritdoc */
13
+ async execute() {
14
+ const { widgetId } = this;
15
+ if (widgetId === null) {
16
+ throw new Error('Recaptcha is not setup');
17
+ }
18
+ // there is no way to know when users have dismissed a previous recaptcha
19
+ // so if we're still in the middle of execution when we call execute again,
20
+ // just reset it and execute it again
21
+ this.grecaptchaLibrary.reset(widgetId);
22
+ return new Promise((resolve, reject) => {
23
+ this.executionSuccessBlock = (token) => {
24
+ this.grecaptchaLibrary.reset(widgetId);
25
+ resolve(token);
26
+ };
27
+ this.executionExpiredBlock = () => {
28
+ this.grecaptchaLibrary.reset(widgetId);
29
+ reject(new Error('expired'));
30
+ };
31
+ this.executionErrorBlock = () => {
32
+ this.grecaptchaLibrary.reset(widgetId);
33
+ reject(new Error('error'));
34
+ };
35
+ this.grecaptchaLibrary.execute(widgetId);
36
+ });
37
+ }
38
+ /** @inheritdoc */
39
+ setup(container, recaptchaParams) {
40
+ var _a;
41
+ this.widgetId = this.grecaptchaLibrary.render(container, {
42
+ callback: this.responseHandler.bind(this),
43
+ 'expired-callback': this.expiredHandler.bind(this),
44
+ 'error-callback': this.errorHandler.bind(this),
45
+ sitekey: this.siteKey,
46
+ tabindex: recaptchaParams === null || recaptchaParams === void 0 ? void 0 : recaptchaParams.tabindex,
47
+ theme: recaptchaParams === null || recaptchaParams === void 0 ? void 0 : recaptchaParams.theme,
48
+ type: recaptchaParams === null || recaptchaParams === void 0 ? void 0 : recaptchaParams.type,
49
+ size: (_a = recaptchaParams === null || recaptchaParams === void 0 ? void 0 : recaptchaParams.size) !== null && _a !== void 0 ? _a : 'invisible',
50
+ });
51
+ }
52
+ createContainer() {
53
+ const elementId = `recaptchaManager-${this.siteKey}`;
54
+ let element = document.getElementById(elementId);
55
+ if (!element) {
56
+ element = document.createElement('div');
57
+ element.id = elementId;
58
+ element.style.position = 'fixed';
59
+ element.style.top = '50%';
60
+ element.style.left = '50%';
61
+ element.style.outline = '1px solid purple';
62
+ element.style.width = '10px';
63
+ element.style.height = '10px';
64
+ document.body.appendChild(element);
65
+ }
66
+ return element;
67
+ }
68
+ responseHandler(response) {
69
+ if (this.executionSuccessBlock) {
70
+ this.executionSuccessBlock(response);
71
+ this.executionSuccessBlock = undefined;
72
+ }
73
+ }
74
+ expiredHandler() {
75
+ if (this.executionExpiredBlock) {
76
+ this.executionExpiredBlock();
77
+ this.executionExpiredBlock = undefined;
78
+ }
79
+ }
80
+ errorHandler() {
81
+ if (this.executionErrorBlock) {
82
+ this.executionErrorBlock();
83
+ this.executionErrorBlock = undefined;
84
+ }
85
+ }
86
+ }
87
+ //# sourceMappingURL=recaptcha.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recaptcha.js","sourceRoot":"","sources":["../../src/recaptcha.ts"],"names":[],"mappings":"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","sourcesContent":["export interface RecaptchaWidgetInterface {\n /** execute the recaptcha request and returns a token */\n execute(): Promise<string>;\n}\n\n/**\n * This encompasses a widget for a given site key.\n */\nexport class RecaptchaWidget implements RecaptchaWidgetInterface {\n private executionSuccessBlock?: (token: string) => void;\n\n private executionExpiredBlock?: () => void;\n\n private executionErrorBlock?: () => void;\n\n private grecaptchaLibrary: ReCaptchaV2.ReCaptcha;\n\n private siteKey: string;\n\n private widgetId: number | null = null;\n\n constructor(\n config: {\n siteKey: string;\n grecaptchaLibrary: ReCaptchaV2.ReCaptcha;\n },\n recaptchaParams?: ReCaptchaV2.Parameters\n ) {\n this.siteKey = config.siteKey;\n this.grecaptchaLibrary = config.grecaptchaLibrary;\n\n const container = this.createContainer();\n this.setup(container, recaptchaParams);\n }\n\n /** @inheritdoc */\n async execute(): Promise<string> {\n const { widgetId } = this;\n\n if (widgetId === null) {\n throw new Error('Recaptcha is not setup');\n }\n\n // there is no way to know when users have dismissed a previous recaptcha\n // so if we're still in the middle of execution when we call execute again,\n // just reset it and execute it again\n this.grecaptchaLibrary.reset(widgetId);\n\n return new Promise((resolve, reject) => {\n this.executionSuccessBlock = (token: string): void => {\n this.grecaptchaLibrary.reset(widgetId);\n resolve(token);\n };\n\n this.executionExpiredBlock = (): void => {\n this.grecaptchaLibrary.reset(widgetId);\n reject(new Error('expired'));\n };\n\n this.executionErrorBlock = (): void => {\n this.grecaptchaLibrary.reset(widgetId);\n reject(new Error('error'));\n };\n\n this.grecaptchaLibrary.execute(widgetId);\n });\n }\n\n /** @inheritdoc */\n private setup(\n container: HTMLElement,\n recaptchaParams?: ReCaptchaV2.Parameters\n ): void {\n this.widgetId = this.grecaptchaLibrary.render(container, {\n callback: this.responseHandler.bind(this),\n 'expired-callback': this.expiredHandler.bind(this),\n 'error-callback': this.errorHandler.bind(this),\n sitekey: this.siteKey,\n tabindex: recaptchaParams?.tabindex,\n theme: recaptchaParams?.theme,\n type: recaptchaParams?.type,\n size: recaptchaParams?.size ?? 'invisible',\n });\n }\n\n private createContainer(): HTMLElement {\n const elementId = `recaptchaManager-${this.siteKey}`;\n let element: HTMLElement | null = document.getElementById(elementId);\n if (!element) {\n element = document.createElement('div');\n element.id = elementId;\n element.style.position = 'fixed';\n element.style.top = '50%';\n element.style.left = '50%';\n element.style.outline = '1px solid purple';\n element.style.width = '10px';\n element.style.height = '10px';\n document.body.appendChild(element);\n }\n return element;\n }\n\n private responseHandler(response: any): void {\n if (this.executionSuccessBlock) {\n this.executionSuccessBlock(response);\n this.executionSuccessBlock = undefined;\n }\n }\n\n private expiredHandler(): void {\n if (this.executionExpiredBlock) {\n this.executionExpiredBlock();\n this.executionExpiredBlock = undefined;\n }\n }\n\n private errorHandler(): void {\n if (this.executionErrorBlock) {\n this.executionErrorBlock();\n this.executionErrorBlock = undefined;\n }\n }\n}\n"]}
@@ -1,4 +1,3 @@
1
1
  "use strict";
2
- describe('YourWebComponent', () => {
3
- });
2
+ describe('YourWebComponent', () => { });
4
3
  //# sourceMappingURL=recaptcha-manager.test.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"recaptcha-manager.test.js","sourceRoot":"","sources":["../../test/recaptcha-manager.test.ts"],"names":[],"mappings":";AAAA,QAAQ,CAAC,kBAAkB,EAAE,GAAG,EAAE;AAElC,CAAC,CAAC,CAAC","sourcesContent":["describe('YourWebComponent', () => {\n\n});\n"]}
1
+ {"version":3,"file":"recaptcha-manager.test.js","sourceRoot":"","sources":["../../test/recaptcha-manager.test.ts"],"names":[],"mappings":";AAAA,QAAQ,CAAC,kBAAkB,EAAE,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC","sourcesContent":["describe('YourWebComponent', () => {});\n"]}
package/index.ts CHANGED
@@ -1 +1,9 @@
1
- export { RecaptchaManager, RecaptchaManagerInterface } from './src/recaptcha-manager';
1
+ export {
2
+ RecaptchaManager,
3
+ RecaptchaManagerInterface,
4
+ } from './src/recaptcha-manager';
5
+
6
+ export {
7
+ RecaptchaWidget,
8
+ RecaptchaWidgetInterface,
9
+ } from './src/recaptcha-widget';
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  },
8
8
  "license": "AGPL-3.0-only",
9
9
  "author": "Internet Archive",
10
- "version": "0.0.1-alpha.1",
10
+ "version": "0.0.1-alpha.2",
11
11
  "main": "dist/index.js",
12
12
  "module": "dist/index.js",
13
13
  "scripts": {
@@ -2,173 +2,86 @@ import {
2
2
  LazyLoaderService,
3
3
  LazyLoaderServiceInterface,
4
4
  } from '@internetarchive/lazy-loader-service';
5
+ import { RecaptchaWidget } from './recaptcha-widget';
5
6
 
6
7
  export interface RecaptchaManagerInterface {
7
- execute(): Promise<string>;
8
- setup(
9
- container: HTMLElement,
10
- tabIndex: number,
11
- theme: ReCaptchaV2.Theme,
12
- type: ReCaptchaV2.Type
13
- ): void;
14
- }
15
-
16
- export class RecaptchaManager implements RecaptchaManagerInterface {
17
- /**
18
- * This is a convenience initializer that also lazily loads
19
- * the recaptcha library from Google.
20
- *
21
- * @param options
22
- * @returns
23
- */
24
- static async getRecaptchaManager(options: {
25
- siteKey: string;
26
- }): Promise<RecaptchaManagerInterface> {
27
- const grecaptchaLibrary = await RecaptchaManager.loadRecaptchaLibrary();
28
- return new RecaptchaManager({
29
- grecaptchaLibrary,
30
- siteKey: options.siteKey,
31
- });
32
- }
33
-
34
8
  /**
35
- * Load the Recaptch library from Google.
36
- *
37
- * @param options
38
- * @returns
9
+ * Load a recaptcha widget for a given site key or the default site key.
39
10
  */
40
- static async loadRecaptchaLibrary(options?: {
41
- lazyLoader?: LazyLoaderServiceInterface;
42
- }): Promise<ReCaptchaV2.ReCaptcha> {
43
- const service = options?.lazyLoader ?? new LazyLoaderService();
44
-
45
- return new Promise(resolve => {
46
- (window as any).grecaptchaLoadedCallback = (): void => {
47
- setTimeout(() => {
48
- // remove the callback when we're done with it
49
- delete (window as any).grecaptchaLoadedCallback;
50
- }, 10);
51
- resolve(window.grecaptcha);
52
- };
11
+ getRecaptchaWidget(options?: {
12
+ siteKey?: string;
13
+ recaptchaParams?: ReCaptchaV2.Parameters;
14
+ }): Promise<RecaptchaWidget>;
15
+ }
53
16
 
54
- service.loadScript({
55
- src: 'https://www.google.com/recaptcha/api.js?onload=grecaptchaLoadedCallback&render=explicit',
56
- });
57
- });
58
- }
17
+ export class RecaptchaManager implements RecaptchaManagerInterface {
18
+ private lazyLoader: LazyLoaderServiceInterface;
59
19
 
60
- private grecaptchaLibrary: ReCaptchaV2.ReCaptcha;
20
+ private defaultSiteKey?: string;
61
21
 
62
- private siteKey: string;
22
+ private recaptchaCache: Record<string, RecaptchaWidget> = {};
63
23
 
64
24
  constructor(options: {
65
- grecaptchaLibrary: ReCaptchaV2.ReCaptcha;
66
- siteKey: string;
25
+ defaultSiteKey: string;
26
+ lazyLoader?: LazyLoaderServiceInterface;
27
+ grecaptchaLibrary?: ReCaptchaV2.ReCaptcha; // allows dependency injection or will be lazy loaded
67
28
  }) {
68
- this.grecaptchaLibrary = options.grecaptchaLibrary;
69
- this.siteKey = options.siteKey;
29
+ this.defaultSiteKey = options.defaultSiteKey;
30
+ this.lazyLoader = options.lazyLoader ?? new LazyLoaderService();
31
+ this.grecaptchaLibraryCache = options.grecaptchaLibrary;
70
32
  }
71
33
 
72
- private executionSuccessBlock?: (token: string) => void;
34
+ /** @inheritdoc */
35
+ async getRecaptchaWidget(options?: {
36
+ siteKey?: string;
37
+ recaptchaParams?: ReCaptchaV2.Parameters;
38
+ }): Promise<RecaptchaWidget> {
39
+ const key = options?.siteKey ?? this.defaultSiteKey;
40
+ if (!key) {
41
+ throw new Error('RecaptchaManager requires a site key');
42
+ }
73
43
 
74
- private executionExpiredBlock?: () => void;
44
+ const cached = this.recaptchaCache[key];
45
+ if (cached) return cached;
75
46
 
76
- private executionErrorBlock?: () => void;
47
+ const grecaptchaLibrary = await this.getRecaptchaLibrary();
48
+ const recaptcha = new RecaptchaWidget(
49
+ {
50
+ siteKey: key,
51
+ grecaptchaLibrary,
52
+ },
53
+ options?.recaptchaParams
54
+ );
77
55
 
78
- private isExecuting = false;
56
+ this.recaptchaCache[key] = recaptcha;
57
+ return recaptcha;
58
+ }
79
59
 
80
60
  /**
81
- * Execute Recaptcha and return a Promise containing the response token.
82
- *
83
- * This is an interesting flow.. we call `execute()` here, but have to wait for the
84
- * response and expiration handlers that we bind during the inital `setup` call.
85
- * For consumers, we want to be able to just call `execute()` and wait for a response.
86
- * To allow this, we assign two callbacks:
87
- * - `executionSuccessBlock`
88
- * - `executionExpiredBlock`
89
- *
90
- * We then call those callbacks from inside `responseHandler` and `expiredHandler` to
91
- * either resolve or reject the Promise.
92
- *
93
- * ie:
94
- *
95
- * try {
96
- * const recaptchaResult = await recaptchaManager.execute();
97
- * console.log('recaptcha token:', recaptchaResult);
98
- * } catch {
99
- * console.error('something happened')
100
- * }
61
+ * Load the Recaptch library from Google.
101
62
  *
102
- * @returns {Promise<string>}
103
- * @memberof RecaptchaManager
63
+ * @returns Promise<ReCaptchaV2.ReCaptcha>
104
64
  */
105
- execute(): Promise<string> {
106
- if (this.isExecuting) {
107
- this.finishExecution();
65
+ private async getRecaptchaLibrary(): Promise<ReCaptchaV2.ReCaptcha> {
66
+ if (this.grecaptchaLibraryCache) {
67
+ return this.grecaptchaLibraryCache;
108
68
  }
109
- this.isExecuting = true;
110
- return new Promise((resolve, reject) => {
111
- this.executionSuccessBlock = (token: string): void => {
112
- this.finishExecution();
113
- resolve(token);
114
- };
115
-
116
- this.executionExpiredBlock = (): void => {
117
- this.finishExecution();
118
- reject(new Error('expired'));
119
- };
120
-
121
- this.executionErrorBlock = (): void => {
122
- this.finishExecution();
123
- reject(new Error('error'));
69
+ return new Promise(resolve => {
70
+ (window as any).grecaptchaLoadedCallback = (): void => {
71
+ setTimeout(() => {
72
+ // remove the callback when we're done with it
73
+ delete (window as any).grecaptchaLoadedCallback;
74
+ }, 10);
75
+ this.grecaptchaLibraryCache = window.grecaptcha;
76
+ resolve(window.grecaptcha);
124
77
  };
125
78
 
126
- this.grecaptchaLibrary.execute();
127
- });
128
- }
129
-
130
- private finishExecution(): void {
131
- this.isExecuting = false;
132
- this.grecaptchaLibrary.reset();
133
- }
134
-
135
- setup(
136
- container: HTMLElement,
137
- tabIndex: number,
138
- theme: ReCaptchaV2.Theme,
139
- type: ReCaptchaV2.Type
140
- ): void {
141
- this.grecaptchaLibrary.render(container, {
142
- callback: this.responseHandler.bind(this),
143
- 'expired-callback': this.expiredHandler.bind(this),
144
- 'error-callback': this.errorHandler.bind(this),
145
- sitekey: this.siteKey,
146
- tabindex: tabIndex,
147
- theme,
148
- type,
149
- size: 'invisible',
79
+ this.lazyLoader.loadScript({
80
+ src: 'https://www.google.com/recaptcha/api.js?onload=grecaptchaLoadedCallback&render=explicit',
81
+ });
150
82
  });
151
83
  }
152
84
 
153
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
154
- private responseHandler(response: any): void {
155
- if (this.executionSuccessBlock) {
156
- this.executionSuccessBlock(response);
157
- this.executionSuccessBlock = undefined;
158
- }
159
- }
160
-
161
- private expiredHandler(): void {
162
- if (this.executionExpiredBlock) {
163
- this.executionExpiredBlock();
164
- this.executionExpiredBlock = undefined;
165
- }
166
- }
167
-
168
- private errorHandler(): void {
169
- if (this.executionErrorBlock) {
170
- this.executionErrorBlock();
171
- this.executionErrorBlock = undefined;
172
- }
173
- }
85
+ /** don't use directly, use `getRecaptchaLibrary()` */
86
+ private grecaptchaLibraryCache?: ReCaptchaV2.ReCaptcha;
174
87
  }
@@ -0,0 +1,134 @@
1
+ /**
2
+ * This encompasses a widget for a given site key.
3
+ */
4
+ export interface RecaptchaWidgetInterface {
5
+ /** execute the recaptcha request and returns a token */
6
+ execute(): Promise<string>;
7
+ }
8
+
9
+ /**
10
+ * This encompasses a widget for a given site key.
11
+ */
12
+ export class RecaptchaWidget implements RecaptchaWidgetInterface {
13
+ private executionSuccessBlock?: (token: string) => void;
14
+
15
+ private executionExpiredBlock?: () => void;
16
+
17
+ private executionErrorBlock?: () => void;
18
+
19
+ private grecaptchaLibrary: ReCaptchaV2.ReCaptcha;
20
+
21
+ private siteKey: string;
22
+
23
+ private widgetId: number | null = null;
24
+
25
+ private isExecuting = false;
26
+
27
+ constructor(
28
+ config: {
29
+ siteKey: string;
30
+ grecaptchaLibrary: ReCaptchaV2.ReCaptcha;
31
+ },
32
+ recaptchaParams?: ReCaptchaV2.Parameters
33
+ ) {
34
+ this.siteKey = config.siteKey;
35
+ this.grecaptchaLibrary = config.grecaptchaLibrary;
36
+
37
+ const container = this.createContainer();
38
+ this.setup(container, recaptchaParams);
39
+ }
40
+
41
+ /** @inheritdoc */
42
+ async execute(): Promise<string> {
43
+ const { widgetId } = this;
44
+
45
+ if (widgetId === null) {
46
+ throw new Error('Recaptcha is not setup');
47
+ }
48
+
49
+ if (this.isExecuting) {
50
+ // there is no way to know when users have dismissed a previous recaptcha
51
+ // so if we're still in the middle of execution when we call execute again,
52
+ // just reset it and execute it again
53
+ this.finishExecution();
54
+ }
55
+
56
+ return new Promise((resolve, reject) => {
57
+ this.executionSuccessBlock = (token: string): void => {
58
+ this.finishExecution();
59
+ resolve(token);
60
+ };
61
+
62
+ this.executionExpiredBlock = (): void => {
63
+ this.finishExecution();
64
+ reject(new Error('expired'));
65
+ };
66
+
67
+ this.executionErrorBlock = (): void => {
68
+ this.finishExecution();
69
+ reject(new Error('error'));
70
+ };
71
+
72
+ this.grecaptchaLibrary.execute(widgetId);
73
+ });
74
+ }
75
+
76
+ private finishExecution() {
77
+ this.isExecuting = false;
78
+ const { widgetId } = this;
79
+ if (widgetId === null) return;
80
+ this.grecaptchaLibrary.reset(widgetId);
81
+ }
82
+
83
+ /** @inheritdoc */
84
+ private setup(
85
+ container: HTMLElement,
86
+ recaptchaParams?: ReCaptchaV2.Parameters
87
+ ): void {
88
+ this.widgetId = this.grecaptchaLibrary.render(container, {
89
+ callback: this.responseHandler.bind(this),
90
+ 'expired-callback': this.expiredHandler.bind(this),
91
+ 'error-callback': this.errorHandler.bind(this),
92
+ sitekey: this.siteKey,
93
+ tabindex: recaptchaParams?.tabindex,
94
+ theme: recaptchaParams?.theme,
95
+ type: recaptchaParams?.type,
96
+ size: recaptchaParams?.size ?? 'invisible',
97
+ });
98
+ }
99
+
100
+ private createContainer(): HTMLElement {
101
+ const elementId = `recaptchaManager-${this.siteKey}`;
102
+ let element: HTMLElement | null = document.getElementById(elementId);
103
+ if (!element) {
104
+ element = document.createElement('div');
105
+ element.id = elementId;
106
+ element.style.position = 'fixed';
107
+ element.style.top = '50%';
108
+ element.style.left = '50%';
109
+ document.body.appendChild(element);
110
+ }
111
+ return element;
112
+ }
113
+
114
+ private responseHandler(response: any): void {
115
+ if (this.executionSuccessBlock) {
116
+ this.executionSuccessBlock(response);
117
+ this.executionSuccessBlock = undefined;
118
+ }
119
+ }
120
+
121
+ private expiredHandler(): void {
122
+ if (this.executionExpiredBlock) {
123
+ this.executionExpiredBlock();
124
+ this.executionExpiredBlock = undefined;
125
+ }
126
+ }
127
+
128
+ private errorHandler(): void {
129
+ if (this.executionErrorBlock) {
130
+ this.executionErrorBlock();
131
+ this.executionErrorBlock = undefined;
132
+ }
133
+ }
134
+ }
@@ -1,3 +1 @@
1
- describe('YourWebComponent', () => {
2
-
3
- });
1
+ describe('YourWebComponent', () => {});