@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.
- package/.github/workflows/build.yml +35 -0
- package/.prettierignore +1 -0
- package/.prettierrc.json +10 -0
- package/README.md +28 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +62 -0
- package/package.json +31 -0
- package/src/index.ts +94 -0
- package/tsconfig.json +13 -0
|
@@ -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
|
package/.prettierignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.github
|
package/.prettierrc.json
ADDED
|
@@ -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
|
+
```
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|