@thetypefounders/continue-with-google 1.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.
@@ -0,0 +1,35 @@
1
+ name: Build
2
+
3
+ on:
4
+ pull_request:
5
+ branches:
6
+ - main
7
+ workflow_dispatch:
8
+
9
+ concurrency:
10
+ group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
11
+ cancel-in-progress: true
12
+
13
+ jobs:
14
+ check:
15
+ name: Run the checks
16
+ runs-on: ubuntu-latest
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+ - uses: actions/setup-node@v4
20
+ with:
21
+ node-version: 20.x
22
+ - run: npm clean-install
23
+ - run: npm run check
24
+
25
+ test:
26
+ name: Run the tests
27
+ runs-on: ubuntu-latest
28
+ environment: staging
29
+ steps:
30
+ - uses: actions/checkout@v4
31
+ - uses: actions/setup-node@v4
32
+ with:
33
+ node-version: 20.x
34
+ - run: npm install
35
+ - run: npm run test
@@ -0,0 +1 @@
1
+ .github
@@ -0,0 +1,10 @@
1
+ {
2
+ "trailingComma": "es5",
3
+ "singleQuote": true,
4
+ "tabWidth": 2,
5
+ "useTabs": false,
6
+ "plugins": ["@trivago/prettier-plugin-sort-imports"],
7
+ "importOrder": ["<THIRD_PARTY_MODULES>", "^[./]"],
8
+ "importOrderSeparation": true,
9
+ "importOrderSortSpecifiers": true
10
+ }
package/README.md ADDED
@@ -0,0 +1,28 @@
1
+ # Continue with Google
2
+
3
+ The package provides a two-factor authentication with Google via Puppeteer.
4
+
5
+ ## Installation
6
+
7
+ ```shell
8
+ npm install @thetypefounders/continue-with-google --save
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```javascript
14
+ import authenticate from '@thetypefounders/continue-with-google';
15
+
16
+ const browser = await Puppeteer.launch();
17
+ const page = await browser.newPage();
18
+
19
+ // Go to a page that supports Google.
20
+ await page.goto('...');
21
+ await page.waitForSelector('...');
22
+
23
+ // Click on the continue-with-Google button.
24
+ await page.click('...');
25
+
26
+ // Finalize the authorization flow and wait for a selector after redirection.
27
+ const element = await authenticate(page, email, password, secret, selector);
28
+ ```
@@ -0,0 +1,5 @@
1
+ import { ElementHandle, Page } from 'puppeteer';
2
+ export interface Logger {
3
+ info(message: string): void;
4
+ }
5
+ export declare function authenticate(page: Page, email: string, password: string, secret: string, target: string, logger?: Logger, attemptCount?: number, attemptSeconds?: number): Promise<ElementHandle | null>;
package/dist/index.js ADDED
@@ -0,0 +1,62 @@
1
+ import { generateToken } from 'authenticator';
2
+ import { setTimeout } from 'node:timers/promises';
3
+ export async function authenticate(page, email, password, secret, target, logger = console, attemptCount = 3, attemptSeconds = 30) {
4
+ logger.info('Waiting to enter the email...');
5
+ await page.waitForSelector('input[type=email]', { visible: true });
6
+ logger.info('Entering the email...');
7
+ await page.type('input[type=email]', email);
8
+ await page.keyboard.press('Enter');
9
+ logger.info('Waiting to enter the password...');
10
+ await page.waitForSelector('input[type=password]', { visible: true });
11
+ logger.info('Entering the password...');
12
+ await page.type('input[type=password]', password);
13
+ await page.keyboard.press('Enter');
14
+ for (let attempt = 0, found = false; attempt < attemptCount && !found; attempt++) {
15
+ if (attempt > 0) {
16
+ logger.info(`Challenged on attempt ${attempt}. Entering the code...`);
17
+ if (attempt > 1) {
18
+ await setTimeout(1000 * attemptSeconds);
19
+ }
20
+ const code = generateToken(secret);
21
+ await page.evaluate(() => {
22
+ const field = document.querySelector('input[type=tel]');
23
+ field?.setAttribute('value', '');
24
+ });
25
+ await page.type('input[type=tel]', code);
26
+ await page.keyboard.press('Enter');
27
+ await waitForPeace(page, logger);
28
+ }
29
+ found = await Promise.any([
30
+ page.waitForSelector(target).then(() => true),
31
+ page
32
+ .waitForSelector('input[type=tel]', { visible: true })
33
+ .then(() => false),
34
+ ]);
35
+ }
36
+ return await page.$(target);
37
+ }
38
+ async function waitForPeace(page, logger, attemptCount = 10, attemptSeconds = 2) {
39
+ for (let attempt = -1, previous = undefined, current = undefined; attempt < attemptCount && (current === undefined || previous !== current); attempt++) {
40
+ if (attempt > 0) {
41
+ logger.info(`Changed on attempt ${attempt}. Taking a screenshot...`);
42
+ }
43
+ if (attempt > -1) {
44
+ await setTimeout(1000 * attemptSeconds);
45
+ }
46
+ const future = await screenshot(page);
47
+ if (future) {
48
+ previous = current;
49
+ current = future;
50
+ }
51
+ }
52
+ }
53
+ async function screenshot(page) {
54
+ try {
55
+ const content = '* { caret-color: transparent !important; }';
56
+ await page.addStyleTag({ content });
57
+ return await page.screenshot({ encoding: 'base64' });
58
+ }
59
+ catch {
60
+ return undefined;
61
+ }
62
+ }
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@thetypefounders/continue-with-google",
3
+ "version": "1.0.0",
4
+ "license": "Apache-2.0",
5
+ "author": "Ivan Ukhov <ivan.ukhov@gmail.com>",
6
+ "description": "Two-factor authentication with Google via Puppeteer",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/the-type-founders/continue-with-google.git"
10
+ },
11
+ "type": "module",
12
+ "main": "dist/index.js",
13
+ "types": "dist/index.d.ts",
14
+ "dependencies": {
15
+ "authenticator": "^1.1.5",
16
+ "puppeteer": "^24.1.1"
17
+ },
18
+ "devDependencies": {
19
+ "@trivago/prettier-plugin-sort-imports": "^5.2.1",
20
+ "@types/authenticator": "^1.1.4",
21
+ "prettier": "^3.4.2",
22
+ "typescript": "^5.7.3"
23
+ },
24
+ "scripts": {
25
+ "build": "tsc",
26
+ "check": "prettier . --check",
27
+ "format": "prettier . --check --write",
28
+ "prepublish": "tsc",
29
+ "test": "echo"
30
+ }
31
+ }
package/src/index.ts ADDED
@@ -0,0 +1,94 @@
1
+ import { generateToken } from 'authenticator';
2
+ import { setTimeout } from 'node:timers/promises';
3
+ import { ElementHandle, Page } from 'puppeteer';
4
+
5
+ export interface Logger {
6
+ info(message: string): void;
7
+ }
8
+
9
+ export async function authenticate(
10
+ page: Page,
11
+ email: string,
12
+ password: string,
13
+ secret: string,
14
+ target: string,
15
+ logger: Logger = console,
16
+ attemptCount: number = 3,
17
+ attemptSeconds: number = 30
18
+ ): Promise<ElementHandle | null> {
19
+ logger.info('Waiting to enter the email...');
20
+ await page.waitForSelector('input[type=email]', { visible: true });
21
+ logger.info('Entering the email...');
22
+ await page.type('input[type=email]', email);
23
+ await page.keyboard.press('Enter');
24
+
25
+ logger.info('Waiting to enter the password...');
26
+ await page.waitForSelector('input[type=password]', { visible: true });
27
+ logger.info('Entering the password...');
28
+ await page.type('input[type=password]', password);
29
+ await page.keyboard.press('Enter');
30
+
31
+ for (
32
+ let attempt = 0, found = false;
33
+ attempt < attemptCount && !found;
34
+ attempt++
35
+ ) {
36
+ if (attempt > 0) {
37
+ logger.info(`Challenged on attempt ${attempt}. Entering the code...`);
38
+ if (attempt > 1) {
39
+ await setTimeout(1000 * attemptSeconds);
40
+ }
41
+ const code = generateToken(secret);
42
+ await page.evaluate(() => {
43
+ const field = document.querySelector('input[type=tel]');
44
+ (field as HTMLInputElement)?.setAttribute('value', '');
45
+ });
46
+ await page.type('input[type=tel]', code);
47
+ await page.keyboard.press('Enter');
48
+ await waitForPeace(page, logger);
49
+ }
50
+ found = await Promise.any([
51
+ page.waitForSelector(target).then(() => true),
52
+ page
53
+ .waitForSelector('input[type=tel]', { visible: true })
54
+ .then(() => false),
55
+ ]);
56
+ }
57
+
58
+ return await page.$(target);
59
+ }
60
+
61
+ async function waitForPeace(
62
+ page: Page,
63
+ logger: Logger,
64
+ attemptCount: number = 10,
65
+ attemptSeconds: number = 2
66
+ ): Promise<void> {
67
+ for (
68
+ let attempt = -1, previous = undefined, current = undefined;
69
+ attempt < attemptCount && (current === undefined || previous !== current);
70
+ attempt++
71
+ ) {
72
+ if (attempt > 0) {
73
+ logger.info(`Changed on attempt ${attempt}. Taking a screenshot...`);
74
+ }
75
+ if (attempt > -1) {
76
+ await setTimeout(1000 * attemptSeconds);
77
+ }
78
+ const future = await screenshot(page);
79
+ if (future) {
80
+ previous = current;
81
+ current = future;
82
+ }
83
+ }
84
+ }
85
+
86
+ async function screenshot(page: Page): Promise<string | undefined> {
87
+ try {
88
+ const content = '* { caret-color: transparent !important; }';
89
+ await page.addStyleTag({ content });
90
+ return await page.screenshot({ encoding: 'base64' });
91
+ } catch {
92
+ return undefined;
93
+ }
94
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "esnext",
4
+ "module": "esnext",
5
+ "declaration": true,
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "moduleResolution": "node",
9
+ "outDir": "dist"
10
+ },
11
+ "include": ["src/**/*"],
12
+ "exclude": ["node_modules"]
13
+ }