google-oauth2-env-vars 1.4.0 β 2.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/LICENCE.md +21 -0
- package/README.md +36 -12
- package/index.js +93 -43
- package/package.json +20 -17
- package/types/index.d.ts +56 -0
- package/types/utils/ask-client-ids.d.ts +10 -0
- package/types/utils/ask.d.ts +7 -0
- package/types/utils/constants.d.ts +8 -0
- package/types/utils/get-authorization-code.d.ts +8 -0
- package/types/utils/get-new-token.d.ts +12 -0
- package/types/utils/init-auth.d.ts +10 -0
- package/types/utils/open-url.d.ts +8 -0
- package/types/utils/write-to-env-files.d.ts +5 -0
- package/utils/ask-client-ids.js +11 -21
- package/utils/ask.js +29 -0
- package/utils/constants.js +0 -1
- package/utils/get-authorization-code.js +43 -21
- package/utils/get-new-token.js +54 -42
- package/utils/init-auth.js +7 -16
- package/utils/open-url.js +35 -33
- package/utils/write-to-env-files.js +33 -13
- package/utils/wait-input.js +0 -19
package/LICENCE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2020 CΓ©dric Delpoux
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<div align="center">
|
|
2
|
-
<h1>google-oauth2-env-vars
|
|
2
|
+
<h1>google-oauth2-env-vars</h1>
|
|
3
3
|
|
|
4
4
|
[![Npm version][badge-npm]][npm]
|
|
5
5
|
[![Npm downloads][badge-npm-dl]][npm]
|
|
@@ -15,15 +15,15 @@ Helper class to generate and cache `Google OAuth 2.0` tokens in `.env` files:
|
|
|
15
15
|
```dotenv
|
|
16
16
|
GOOGLE_OAUTH_CLIENT_ID=23...8o1jm.apps.googleusercontent.com
|
|
17
17
|
GOOGLE_OAUTH_CLIENT_SECRET=QL...naxL
|
|
18
|
-
|
|
18
|
+
GOOGLE_DOCS_TOKEN={"access_token":"ya..54c","refresh_token":"1..qq0","scope":"https://www.googleapis.com/auth/documents.readonly","token_type":"Bearer","expiry_date":1598277881619}
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
## Usage
|
|
22
22
|
|
|
23
|
-
Download package from the
|
|
23
|
+
It requires Node.js 22 or later. Download the package from the npm registry:
|
|
24
24
|
|
|
25
25
|
```shell
|
|
26
|
-
|
|
26
|
+
npm install google-oauth2-env-vars
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
### Generate .env vars
|
|
@@ -33,40 +33,64 @@ const GoogleOAuth2 = require("google-oauth2-env-vars")
|
|
|
33
33
|
|
|
34
34
|
const googleOAuth2 = new GoogleOAuth2({
|
|
35
35
|
// .env token var name
|
|
36
|
-
token: "
|
|
36
|
+
token: "GOOGLE_DOCS_TOKEN",
|
|
37
37
|
// Authorization scope
|
|
38
|
-
scope: ["https://www.googleapis.com/auth/
|
|
38
|
+
scope: ["https://www.googleapis.com/auth/documents.readonly"],
|
|
39
39
|
// APIs to enable in Google console
|
|
40
|
-
apis: ["
|
|
40
|
+
apis: ["docs.googleapis.com"],
|
|
41
41
|
//
|
|
42
42
|
// Optional
|
|
43
43
|
//
|
|
44
|
-
//
|
|
45
|
-
port
|
|
44
|
+
// Fixed port of the local server receiving the authorization code,
|
|
45
|
+
// for a "Web application" client. Any free port by default.
|
|
46
|
+
port: 5000,
|
|
46
47
|
})
|
|
47
48
|
|
|
48
49
|
const envVars = await googleOAuth2.generateEnvVars()
|
|
49
50
|
```
|
|
50
51
|
|
|
52
|
+
When the variables are missing from the environment, `generateEnvVars()` walks you through the setup in the terminal:
|
|
53
|
+
|
|
54
|
+
1. It asks you to enable each API in `apis` in the Google Cloud console.
|
|
55
|
+
2. It asks for the `Client ID` and `Client secret` of an OAuth 2.0 client of type "Desktop app". With a fixed `port`, it can also be a "Web application" client whose authorized redirect URI is `http://localhost:<port>/callback`.
|
|
56
|
+
3. It asks you to authorize the application, and receives the authorization code on a local server, which listens on any free port unless `port` is set. The authorization uses PKCE.
|
|
57
|
+
4. It writes `GOOGLE_OAUTH_CLIENT_ID`, `GOOGLE_OAUTH_CLIENT_SECRET` and the token to the `.env` and `.env.*` files of the current directory, or to a new `.env` file when there is none. It leaves out `.envrc` and the templates `.env.example`, `.env.sample` and `.env.template`, which are often committed. A variable that is already there is replaced.
|
|
58
|
+
|
|
59
|
+
Each URL is printed in the terminal and opened in your browser when possible.
|
|
60
|
+
|
|
51
61
|
### Get Auth
|
|
52
62
|
|
|
63
|
+
`getAuth()` returns an `OAuth2Client` from [google-auth-library](https://www.npmjs.com/package/google-auth-library), which refreshes the access token when it expires:
|
|
64
|
+
|
|
53
65
|
```js
|
|
54
66
|
const GoogleOAuth2 = require("google-oauth2-env-vars")
|
|
67
|
+
const {docs} = require("@googleapis/docs")
|
|
55
68
|
|
|
56
69
|
const googleOAuth2 = new GoogleOAuth2({
|
|
57
70
|
// .env token var name
|
|
58
|
-
token: "
|
|
71
|
+
token: "GOOGLE_DOCS_TOKEN",
|
|
59
72
|
})
|
|
60
73
|
const auth = await googleOAuth2.getAuth()
|
|
61
|
-
const
|
|
62
|
-
const googlePhotos = new GooglePhotos(token)
|
|
74
|
+
const googleDocs = docs({version: "v1", auth})
|
|
63
75
|
```
|
|
64
76
|
|
|
77
|
+
`getEnvVars()` returns the same variables without creating a client, and throws when one of them is missing or invalid.
|
|
78
|
+
|
|
65
79
|
## Contributing
|
|
66
80
|
|
|
67
81
|
- β Pull/Merge requests and β
Stars are always welcome.
|
|
68
82
|
- For bugs and feature requests, please [create an issue][github-issue].
|
|
69
83
|
|
|
84
|
+
```shell
|
|
85
|
+
npm install
|
|
86
|
+
npm run lint
|
|
87
|
+
npm test
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The code is plain JavaScript. The types of the public API are written as [JSDoc](https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html) comments: `npm run lint` checks them with TypeScript, and `npm pack` turns them into the `.d.ts` files of the package.
|
|
91
|
+
|
|
92
|
+
To release a version, bump it in `package.json` and `CHANGELOG.md`, then publish a GitHub release: the `Publish` workflow publishes it to npm, with its provenance.
|
|
93
|
+
|
|
70
94
|
[badge-npm]: https://img.shields.io/npm/v/google-oauth2-env-vars.svg?style=flat-square
|
|
71
95
|
[badge-npm-dl]: https://img.shields.io/npm/dt/google-oauth2-env-vars.svg?style=flat-square
|
|
72
96
|
[badge-licence]: https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square
|
package/index.js
CHANGED
|
@@ -1,34 +1,74 @@
|
|
|
1
|
-
require("dotenv").config()
|
|
2
|
-
|
|
3
1
|
const {askClientIds} = require("./utils/ask-client-ids")
|
|
4
2
|
const {getNewToken} = require("./utils/get-new-token")
|
|
5
3
|
const {initAuth} = require("./utils/init-auth")
|
|
6
4
|
|
|
7
|
-
const {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
5
|
+
const {CLIENT_ID, CLIENT_SECRET, TOKEN_FIELDS} = require("./utils/constants")
|
|
6
|
+
|
|
7
|
+
try {
|
|
8
|
+
process.loadEnvFile()
|
|
9
|
+
} catch (e) {
|
|
10
|
+
if (e.code !== "ENOENT") {
|
|
11
|
+
throw e
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {object} Options
|
|
17
|
+
* @property {string} [token] Name of the .env variable holding the token
|
|
18
|
+
* @property {string[]} [scope] Scopes the token is authorized for
|
|
19
|
+
* @property {string[]} [apis] APIs to enable in the Google Cloud console
|
|
20
|
+
* @property {number} [port] Fixed port for a "Web application" client registered
|
|
21
|
+
* with http://localhost:<port>/callback. A free port is picked by default,
|
|
22
|
+
* which needs a "Desktop app" client.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/** @typedef {import("google-auth-library").Credentials} Token */
|
|
26
|
+
|
|
27
|
+
/** @param {Error} e */
|
|
28
|
+
const printError = (e) => {
|
|
29
|
+
console.error("")
|
|
30
|
+
console.error(`β ${e.message}`)
|
|
31
|
+
process.exitCode = 1
|
|
32
|
+
}
|
|
13
33
|
|
|
14
34
|
class GoogleOAuth2 {
|
|
35
|
+
/** @param {Options} [options] */
|
|
15
36
|
constructor(options = {}) {
|
|
16
37
|
this.tokenName = options.token || null
|
|
17
38
|
this.scope = options.scope || null
|
|
18
39
|
this.apisToEnable = options.apis || []
|
|
19
|
-
this.port = options.port ||
|
|
40
|
+
this.port = options.port || 0
|
|
20
41
|
}
|
|
21
42
|
|
|
43
|
+
/** @param {Token} token */
|
|
22
44
|
isTokenValid(token) {
|
|
23
45
|
return token && TOKEN_FIELDS.every((field) => !!token[field])
|
|
24
46
|
}
|
|
25
47
|
|
|
48
|
+
/**
|
|
49
|
+
* @param {string} rawToken
|
|
50
|
+
* @returns {Token}
|
|
51
|
+
*/
|
|
52
|
+
parseToken(rawToken) {
|
|
53
|
+
try {
|
|
54
|
+
return JSON.parse(rawToken)
|
|
55
|
+
} catch (e) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`${this.tokenName} is not valid JSON (${e.message}). ` +
|
|
58
|
+
"It must be on a single line, with double quotes around every key and string value: " +
|
|
59
|
+
`${this.tokenName}={"access_token":"...","refresh_token":"...","scope":"...","token_type":"Bearer","expiry_date":0}. ` +
|
|
60
|
+
"Run the token generation again to get a new one.",
|
|
61
|
+
{cause: e}
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
26
66
|
async getNewEnvVars() {
|
|
27
67
|
if (!this.scope) {
|
|
28
68
|
throw new Error("You need to define `scope` in GoogleOAuth2 constructor")
|
|
29
69
|
}
|
|
30
70
|
|
|
31
|
-
const {clientId, clientSecret} = await askClientIds()
|
|
71
|
+
const {clientId, clientSecret} = await askClientIds({port: this.port})
|
|
32
72
|
|
|
33
73
|
const token = await getNewToken({
|
|
34
74
|
clientId,
|
|
@@ -39,58 +79,73 @@ class GoogleOAuth2 {
|
|
|
39
79
|
port: this.port,
|
|
40
80
|
})
|
|
41
81
|
|
|
42
|
-
return
|
|
82
|
+
return {
|
|
83
|
+
[CLIENT_ID]: clientId,
|
|
84
|
+
[CLIENT_SECRET]: clientSecret,
|
|
85
|
+
[this.tokenName]: token,
|
|
86
|
+
}
|
|
43
87
|
}
|
|
44
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Read the variables from the environment, and throw when one of them is
|
|
91
|
+
* missing or invalid.
|
|
92
|
+
*
|
|
93
|
+
* @returns {Record<string, string | Token>}
|
|
94
|
+
*/
|
|
45
95
|
getEnvVars() {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
96
|
+
const clientId = process.env[CLIENT_ID]
|
|
97
|
+
const clientSecret = process.env[CLIENT_SECRET]
|
|
98
|
+
const rawToken = this.tokenName && process.env[this.tokenName]
|
|
49
99
|
|
|
50
|
-
if (
|
|
51
|
-
envClientId = process.env[CLIENT_ID]
|
|
52
|
-
} else {
|
|
100
|
+
if (!clientId) {
|
|
53
101
|
throw new Error(`${CLIENT_ID} not found in .env`)
|
|
54
102
|
}
|
|
55
103
|
|
|
56
|
-
if (
|
|
57
|
-
envClientSecret = process.env[CLIENT_SECRET]
|
|
58
|
-
} else {
|
|
104
|
+
if (!clientSecret) {
|
|
59
105
|
throw new Error(`${CLIENT_SECRET} not found in .env`)
|
|
60
106
|
}
|
|
61
107
|
|
|
62
|
-
if (
|
|
63
|
-
envToken = JSON.parse(process.env[this.tokenName])
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (!envToken) {
|
|
108
|
+
if (!rawToken) {
|
|
67
109
|
throw new Error(`${this.tokenName} not found in .env`)
|
|
68
110
|
}
|
|
69
111
|
|
|
70
|
-
|
|
112
|
+
const token = this.parseToken(rawToken)
|
|
113
|
+
|
|
114
|
+
if (!this.isTokenValid(token)) {
|
|
71
115
|
throw new Error(
|
|
72
116
|
`${this.tokenName} not valid. Please generate a new token`
|
|
73
117
|
)
|
|
74
118
|
}
|
|
75
119
|
|
|
76
120
|
return {
|
|
77
|
-
[CLIENT_ID]:
|
|
78
|
-
[CLIENT_SECRET]:
|
|
79
|
-
[this.tokenName]:
|
|
121
|
+
[CLIENT_ID]: clientId,
|
|
122
|
+
[CLIENT_SECRET]: clientSecret,
|
|
123
|
+
[this.tokenName]: token,
|
|
80
124
|
}
|
|
81
125
|
}
|
|
82
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Read the variables, or walk through their generation when they are missing.
|
|
129
|
+
* A failure is printed in the console and sets the exit code, instead of
|
|
130
|
+
* throwing, and the result is then undefined.
|
|
131
|
+
*
|
|
132
|
+
* @returns {Promise<Record<string, string | Token> | undefined>}
|
|
133
|
+
*/
|
|
83
134
|
async generateEnvVars() {
|
|
84
135
|
let envVars
|
|
85
136
|
|
|
86
137
|
try {
|
|
87
138
|
envVars = this.getEnvVars()
|
|
88
|
-
console.log(
|
|
89
|
-
console.log(
|
|
90
|
-
console.log(
|
|
139
|
+
console.log(`β
${CLIENT_ID} found in .env`)
|
|
140
|
+
console.log(`β
${CLIENT_SECRET} found in .env`)
|
|
141
|
+
console.log(`β
${this.tokenName} found in .env`)
|
|
91
142
|
} catch (e) {
|
|
92
|
-
console.log(
|
|
93
|
-
envVars = await this.getNewEnvVars()
|
|
143
|
+
console.log(`β ${e.message}`)
|
|
144
|
+
envVars = await this.getNewEnvVars().catch(printError)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (!envVars) {
|
|
148
|
+
return undefined
|
|
94
149
|
}
|
|
95
150
|
|
|
96
151
|
console.log("")
|
|
@@ -104,17 +159,12 @@ class GoogleOAuth2 {
|
|
|
104
159
|
|
|
105
160
|
async getAuth() {
|
|
106
161
|
const envVars = this.getEnvVars()
|
|
107
|
-
const clientId = envVars[CLIENT_ID]
|
|
108
|
-
const clientSecret = envVars[CLIENT_SECRET]
|
|
109
|
-
const token = envVars[this.tokenName]
|
|
110
162
|
|
|
111
|
-
|
|
112
|
-
clientId,
|
|
113
|
-
clientSecret,
|
|
114
|
-
token,
|
|
163
|
+
return initAuth({
|
|
164
|
+
clientId: envVars[CLIENT_ID],
|
|
165
|
+
clientSecret: envVars[CLIENT_SECRET],
|
|
166
|
+
token: envVars[this.tokenName],
|
|
115
167
|
})
|
|
116
|
-
|
|
117
|
-
return auth
|
|
118
168
|
}
|
|
119
169
|
}
|
|
120
170
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "google-oauth2-env-vars",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Generate and cache Google OAuth 2.0 tokens in .env files",
|
|
5
5
|
"repository": "https://github.com/cedricdelpoux/google-oauth2-env-vars",
|
|
6
6
|
"author": {
|
|
@@ -17,29 +17,32 @@
|
|
|
17
17
|
"api"
|
|
18
18
|
],
|
|
19
19
|
"main": "index.js",
|
|
20
|
+
"types": "types/index.d.ts",
|
|
20
21
|
"files": [
|
|
21
22
|
"index.js",
|
|
22
|
-
"utils"
|
|
23
|
+
"utils",
|
|
24
|
+
"types"
|
|
23
25
|
],
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=22"
|
|
28
|
+
},
|
|
24
29
|
"dependencies": {
|
|
25
|
-
"
|
|
26
|
-
"dotenv": "^16.0.3",
|
|
27
|
-
"express": "^4.18.2",
|
|
28
|
-
"glob": "^9.3.2",
|
|
29
|
-
"googleapis": "^114.0.0",
|
|
30
|
-
"inquirer": "^8.0.0",
|
|
31
|
-
"open": "^8.0.0"
|
|
30
|
+
"google-auth-library": "^11.3.4"
|
|
32
31
|
},
|
|
33
32
|
"devDependencies": {
|
|
34
|
-
"eslint": "^
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"prettier": "
|
|
33
|
+
"@eslint/js": "^10.0.1",
|
|
34
|
+
"@types/node": "^22.20.5",
|
|
35
|
+
"eslint": "^10.14.0",
|
|
36
|
+
"globals": "^17.14.0",
|
|
37
|
+
"prettier": "^3.10.1",
|
|
38
|
+
"typescript": "^7.0.2"
|
|
39
39
|
},
|
|
40
40
|
"scripts": {
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
41
|
+
"build": "tsc",
|
|
42
|
+
"lint": "eslint . && prettier --check . && tsc --noEmit",
|
|
43
|
+
"format": "prettier --write .",
|
|
44
|
+
"test": "node --test",
|
|
45
|
+
"prepack": "npm run build",
|
|
46
|
+
"prepublishOnly": "npm run lint && npm test"
|
|
44
47
|
}
|
|
45
48
|
}
|
package/types/index.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export = GoogleOAuth2;
|
|
2
|
+
export type Options = {
|
|
3
|
+
/**
|
|
4
|
+
* Name of the .env variable holding the token
|
|
5
|
+
*/
|
|
6
|
+
token?: string;
|
|
7
|
+
/**
|
|
8
|
+
* Scopes the token is authorized for
|
|
9
|
+
*/
|
|
10
|
+
scope?: string[];
|
|
11
|
+
/**
|
|
12
|
+
* APIs to enable in the Google Cloud console
|
|
13
|
+
*/
|
|
14
|
+
apis?: string[];
|
|
15
|
+
/**
|
|
16
|
+
* Fixed port for a "Web application" client registered
|
|
17
|
+
* with http://localhost:<port>/callback. A free port is picked by default,
|
|
18
|
+
* which needs a "Desktop app" client.
|
|
19
|
+
*/
|
|
20
|
+
port?: number;
|
|
21
|
+
};
|
|
22
|
+
export type Token = import("google-auth-library").Credentials;
|
|
23
|
+
declare class GoogleOAuth2 {
|
|
24
|
+
tokenName: string;
|
|
25
|
+
scope: string[];
|
|
26
|
+
apisToEnable: string[];
|
|
27
|
+
port: number;
|
|
28
|
+
/** @param {Options} [options] */
|
|
29
|
+
constructor(options?: Options);
|
|
30
|
+
/** @param {Token} token */
|
|
31
|
+
isTokenValid(token: Token): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* @param {string} rawToken
|
|
34
|
+
* @returns {Token}
|
|
35
|
+
*/
|
|
36
|
+
parseToken(rawToken: string): Token;
|
|
37
|
+
getNewEnvVars(): Promise<{
|
|
38
|
+
[x: string]: string | import("google-auth-library").Credentials;
|
|
39
|
+
}>;
|
|
40
|
+
/**
|
|
41
|
+
* Read the variables from the environment, and throw when one of them is
|
|
42
|
+
* missing or invalid.
|
|
43
|
+
*
|
|
44
|
+
* @returns {Record<string, string | Token>}
|
|
45
|
+
*/
|
|
46
|
+
getEnvVars(): Record<string, string | Token>;
|
|
47
|
+
/**
|
|
48
|
+
* Read the variables, or walk through their generation when they are missing.
|
|
49
|
+
* A failure is printed in the console and sets the exit code, instead of
|
|
50
|
+
* throwing, and the result is then undefined.
|
|
51
|
+
*
|
|
52
|
+
* @returns {Promise<Record<string, string | Token> | undefined>}
|
|
53
|
+
*/
|
|
54
|
+
generateEnvVars(): Promise<Record<string, string | Token> | undefined>;
|
|
55
|
+
getAuth(): Promise<import("google-auth-library").OAuth2Client>;
|
|
56
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
declare const _exports: {
|
|
2
|
+
getNewToken: typeof getNewToken;
|
|
3
|
+
};
|
|
4
|
+
export = _exports;
|
|
5
|
+
declare const getNewToken: ({ clientId, clientSecret, scope, tokenName, apisToEnable, port, }: {
|
|
6
|
+
apisToEnable: any;
|
|
7
|
+
clientId: any;
|
|
8
|
+
clientSecret: any;
|
|
9
|
+
port: any;
|
|
10
|
+
scope: any;
|
|
11
|
+
tokenName: any;
|
|
12
|
+
}) => Promise<import("google-auth-library").Credentials>;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
declare const _exports: {
|
|
2
|
+
initAuth: typeof initAuth;
|
|
3
|
+
};
|
|
4
|
+
export = _exports;
|
|
5
|
+
import { OAuth2Client } from "google-auth-library";
|
|
6
|
+
declare const initAuth: ({ clientId, clientSecret, token }: {
|
|
7
|
+
clientId: any;
|
|
8
|
+
clientSecret: any;
|
|
9
|
+
token: any;
|
|
10
|
+
}) => OAuth2Client;
|
package/utils/ask-client-ids.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
const
|
|
2
|
-
|
|
1
|
+
const {askRequired} = require("./ask")
|
|
3
2
|
const {openUrl} = require("./open-url")
|
|
4
3
|
const {writeToEnvFiles} = require("./write-to-env-files")
|
|
5
4
|
|
|
6
5
|
const {CLIENT_ID, CLIENT_SECRET, OAUTH_URL} = require("./constants")
|
|
7
6
|
|
|
8
|
-
const askClientIds = async () => {
|
|
7
|
+
const askClientIds = async ({port}) => {
|
|
9
8
|
if (process.env[CLIENT_ID] && process.env[CLIENT_SECRET]) {
|
|
10
9
|
console.log(`β
${CLIENT_ID} found in .env`)
|
|
11
10
|
console.log(`β
${CLIENT_SECRET} found in .env`)
|
|
@@ -17,28 +16,19 @@ const askClientIds = async () => {
|
|
|
17
16
|
}
|
|
18
17
|
|
|
19
18
|
console.log("")
|
|
20
|
-
console.info('Create
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
console.info('Create a "Google OAuth 2.0 Client ID" of type "Desktop app"')
|
|
20
|
+
if (port) {
|
|
21
|
+
console.info(
|
|
22
|
+
`or of type "Web application" with the redirect URI "http://localhost:${port}/callback"`
|
|
23
|
+
)
|
|
24
|
+
}
|
|
23
25
|
|
|
24
|
-
|
|
26
|
+
openUrl({message: "Google OAuth 2.0 URL:", url: OAUTH_URL})
|
|
25
27
|
|
|
26
28
|
console.log("Copy your `Client ID` and `Client secret`")
|
|
27
29
|
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
type: "input",
|
|
31
|
-
name: "clientId",
|
|
32
|
-
message: "Client ID:",
|
|
33
|
-
validate: (input) => !!input,
|
|
34
|
-
},
|
|
35
|
-
{
|
|
36
|
-
type: "input",
|
|
37
|
-
name: "clientSecret",
|
|
38
|
-
message: "Client Secret:",
|
|
39
|
-
validate: (input) => !!input,
|
|
40
|
-
},
|
|
41
|
-
])
|
|
30
|
+
const clientId = await askRequired("Client ID: ")
|
|
31
|
+
const clientSecret = await askRequired("Client Secret: ")
|
|
42
32
|
|
|
43
33
|
writeToEnvFiles(CLIENT_ID, clientId)
|
|
44
34
|
writeToEnvFiles(CLIENT_SECRET, clientSecret)
|
package/utils/ask.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const readline = require("node:readline/promises")
|
|
2
|
+
|
|
3
|
+
const ask = async (question) => {
|
|
4
|
+
const rl = readline.createInterface({
|
|
5
|
+
input: process.stdin,
|
|
6
|
+
output: process.stdout,
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
return (await rl.question(question)).trim()
|
|
11
|
+
} finally {
|
|
12
|
+
rl.close()
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const askRequired = async (question) => {
|
|
17
|
+
let answer = ""
|
|
18
|
+
|
|
19
|
+
while (!answer) {
|
|
20
|
+
answer = await ask(question)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return answer
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = {
|
|
27
|
+
ask,
|
|
28
|
+
askRequired,
|
|
29
|
+
}
|
package/utils/constants.js
CHANGED
|
@@ -1,34 +1,56 @@
|
|
|
1
|
-
|
|
2
|
-
const express = require("express")
|
|
1
|
+
const http = require("node:http")
|
|
3
2
|
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const app = express()
|
|
3
|
+
const TEXT_HEADERS = {
|
|
4
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
5
|
+
Connection: "close",
|
|
6
|
+
}
|
|
9
7
|
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
const getAuthorizationCode = ({port, onListening}) =>
|
|
9
|
+
new Promise((resolve, reject) => {
|
|
10
|
+
const server = http.createServer((req, res) => {
|
|
11
|
+
const {pathname, searchParams} = new URL(req.url, "http://localhost")
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
if (pathname !== "/callback") {
|
|
14
|
+
res.writeHead(404, TEXT_HEADERS).end()
|
|
15
|
+
return
|
|
16
|
+
}
|
|
14
17
|
|
|
15
|
-
|
|
16
|
-
|
|
18
|
+
const code = searchParams.get("code")
|
|
19
|
+
server.close()
|
|
17
20
|
|
|
18
|
-
|
|
19
|
-
if (req.query.code) {
|
|
21
|
+
if (code) {
|
|
20
22
|
console.log("β
Application authorized")
|
|
21
|
-
res
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
resolve(req.query.code)
|
|
23
|
+
res
|
|
24
|
+
.writeHead(200, TEXT_HEADERS)
|
|
25
|
+
.end("Success! You can close this tab and go back to your terminal.")
|
|
26
|
+
resolve(code)
|
|
26
27
|
} else {
|
|
27
|
-
|
|
28
|
+
const message = `Authorization failed: ${searchParams.get("error") || "no code received"}`
|
|
29
|
+
res.writeHead(400, TEXT_HEADERS).end(message)
|
|
30
|
+
reject(new Error(message))
|
|
28
31
|
}
|
|
29
32
|
})
|
|
33
|
+
|
|
34
|
+
server.on("error", (/** @type {NodeJS.ErrnoException} */ e) => {
|
|
35
|
+
if (e.code !== "EADDRINUSE") {
|
|
36
|
+
reject(e)
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
reject(
|
|
40
|
+
new Error(
|
|
41
|
+
`Port ${port} is already in use. Free it, or leave out \`port\` ` +
|
|
42
|
+
'to listen on any free port, which needs a "Desktop app" OAuth client.',
|
|
43
|
+
{cause: e}
|
|
44
|
+
)
|
|
45
|
+
)
|
|
46
|
+
})
|
|
47
|
+
server.listen(port, () => {
|
|
48
|
+
const address = /** @type {import("node:net").AddressInfo} */ (
|
|
49
|
+
server.address()
|
|
50
|
+
)
|
|
51
|
+
onListening(address.port)
|
|
52
|
+
})
|
|
30
53
|
})
|
|
31
|
-
}
|
|
32
54
|
|
|
33
55
|
module.exports = {
|
|
34
56
|
getAuthorizationCode,
|
package/utils/get-new-token.js
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
|
-
const {
|
|
1
|
+
const {CodeChallengeMethod, OAuth2Client} = require("google-auth-library")
|
|
2
2
|
|
|
3
|
+
const {ask} = require("./ask")
|
|
3
4
|
const {getAuthorizationCode} = require("./get-authorization-code")
|
|
5
|
+
const {openUrl} = require("./open-url")
|
|
4
6
|
const {writeToEnvFiles} = require("./write-to-env-files")
|
|
5
7
|
const {ENABLE_API_URL} = require("./constants")
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
+
|
|
9
|
+
const enableApi = async (api) => {
|
|
10
|
+
console.log(`π€ You need to enable "${api}" API`)
|
|
11
|
+
openUrl({message: "API URL:", url: `${ENABLE_API_URL}${api}`})
|
|
12
|
+
console.log(`π Follow the URL and enable "${api}" API`)
|
|
13
|
+
console.log("")
|
|
14
|
+
await ask("π¦ API enabled? Press Enter to continue setup")
|
|
15
|
+
console.log("")
|
|
16
|
+
console.log("------------------------")
|
|
17
|
+
}
|
|
8
18
|
|
|
9
19
|
const getNewToken = async ({
|
|
10
20
|
clientId,
|
|
@@ -14,46 +24,48 @@ const getNewToken = async ({
|
|
|
14
24
|
apisToEnable,
|
|
15
25
|
port,
|
|
16
26
|
}) => {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
console.log(`π€ You need to enable "${api}" API`)
|
|
25
|
-
|
|
26
|
-
await openUrl({message: "API URL:", url: `${ENABLE_API_URL}${api}`})
|
|
27
|
-
|
|
28
|
-
console.log(`π Follow the URL and enable "${api}" API`)
|
|
29
|
-
|
|
30
|
-
await waitInput("API enabled?")
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const client = new google.auth.OAuth2(
|
|
35
|
-
clientId,
|
|
36
|
-
clientSecret,
|
|
37
|
-
`http://localhost:${port}/callback`
|
|
38
|
-
)
|
|
39
|
-
|
|
40
|
-
const authUrl = client.generateAuthUrl({
|
|
41
|
-
access_type: "offline",
|
|
42
|
-
scope,
|
|
43
|
-
prompt: "consent",
|
|
44
|
-
})
|
|
45
|
-
const code = await getAuthorizationCode({url: authUrl, port})
|
|
46
|
-
|
|
47
|
-
const {tokens} = await client.getToken(code)
|
|
48
|
-
|
|
49
|
-
if (tokenName) {
|
|
50
|
-
writeToEnvFiles(tokenName, JSON.stringify(tokens))
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return tokens
|
|
54
|
-
} catch (e) {
|
|
55
|
-
throw new Error(e)
|
|
27
|
+
console.log("------------------------")
|
|
28
|
+
console.log("| Generating new token |")
|
|
29
|
+
console.log("------------------------")
|
|
30
|
+
console.log("")
|
|
31
|
+
|
|
32
|
+
for (const api of apisToEnable) {
|
|
33
|
+
await enableApi(api)
|
|
56
34
|
}
|
|
35
|
+
|
|
36
|
+
const client = new OAuth2Client({clientId, clientSecret})
|
|
37
|
+
const {codeVerifier, codeChallenge} = await client.generateCodeVerifierAsync()
|
|
38
|
+
let redirectUri
|
|
39
|
+
|
|
40
|
+
const code = await getAuthorizationCode({
|
|
41
|
+
port,
|
|
42
|
+
onListening: (listeningPort) => {
|
|
43
|
+
redirectUri = `http://localhost:${listeningPort}/callback`
|
|
44
|
+
const url = client.generateAuthUrl({
|
|
45
|
+
access_type: "offline",
|
|
46
|
+
scope,
|
|
47
|
+
prompt: "consent",
|
|
48
|
+
redirect_uri: redirectUri,
|
|
49
|
+
code_challenge: codeChallenge,
|
|
50
|
+
code_challenge_method: CodeChallengeMethod.S256,
|
|
51
|
+
})
|
|
52
|
+
console.log("π€ You need to authorize your application")
|
|
53
|
+
openUrl({message: "Authorization URL:", url})
|
|
54
|
+
console.log("π Follow the URL to authorize your application")
|
|
55
|
+
},
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
const {tokens} = await client.getToken({
|
|
59
|
+
code,
|
|
60
|
+
codeVerifier,
|
|
61
|
+
redirect_uri: redirectUri,
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
if (tokenName) {
|
|
65
|
+
writeToEnvFiles(tokenName, JSON.stringify(tokens))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return tokens
|
|
57
69
|
}
|
|
58
70
|
|
|
59
71
|
module.exports = {
|
package/utils/init-auth.js
CHANGED
|
@@ -1,23 +1,14 @@
|
|
|
1
|
-
const {
|
|
1
|
+
const {OAuth2Client} = require("google-auth-library")
|
|
2
2
|
|
|
3
|
-
const initAuth =
|
|
4
|
-
const auth = new
|
|
3
|
+
const initAuth = ({clientId, clientSecret, token}) => {
|
|
4
|
+
const auth = new OAuth2Client({clientId, clientSecret})
|
|
5
5
|
|
|
6
6
|
auth.setCredentials(token)
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const expirationDate = new Date(token.expiry_date)
|
|
13
|
-
expired = expirationDate.getTime() < nowDate.getTime()
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
if (expired) {
|
|
17
|
-
auth.on("tokens", (refreshedToken) => {
|
|
18
|
-
auth.setCredentials(refreshedToken)
|
|
19
|
-
})
|
|
20
|
-
}
|
|
8
|
+
// A refreshed token comes without its refresh_token, which must be kept
|
|
9
|
+
auth.on("tokens", (refreshedToken) => {
|
|
10
|
+
auth.setCredentials({...token, ...refreshedToken})
|
|
11
|
+
})
|
|
21
12
|
|
|
22
13
|
return auth
|
|
23
14
|
}
|
package/utils/open-url.js
CHANGED
|
@@ -1,39 +1,41 @@
|
|
|
1
|
-
const
|
|
2
|
-
const clipboardy = require("clipboardy")
|
|
3
|
-
const open = require("open")
|
|
1
|
+
const {spawn} = require("node:child_process")
|
|
4
2
|
|
|
5
|
-
|
|
3
|
+
/**
|
|
4
|
+
* @param {string} url
|
|
5
|
+
* @returns {[string, string[]]}
|
|
6
|
+
*/
|
|
7
|
+
const getOpenCommand = (url) => {
|
|
8
|
+
switch (process.platform) {
|
|
9
|
+
case "darwin":
|
|
10
|
+
return ["open", [url]]
|
|
11
|
+
case "win32":
|
|
12
|
+
// Quoted so that cmd does not split the URL on its "&"
|
|
13
|
+
return ["cmd", ["/c", "start", '""', `"${url}"`]]
|
|
14
|
+
default:
|
|
15
|
+
return ["xdg-open", [url]]
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const openUrl = ({message, url}) => {
|
|
20
|
+
console.log("")
|
|
21
|
+
console.log(`π ${message} ${url}`)
|
|
6
22
|
console.log("")
|
|
7
|
-
const {choice} = await inquirer.prompt([
|
|
8
|
-
{
|
|
9
|
-
type: "list",
|
|
10
|
-
name: "choice",
|
|
11
|
-
message,
|
|
12
|
-
choices: [
|
|
13
|
-
{
|
|
14
|
-
name: "Open in my browser",
|
|
15
|
-
value: "browser",
|
|
16
|
-
short: "π Opened in your browser",
|
|
17
|
-
},
|
|
18
|
-
{
|
|
19
|
-
name: "Copy to clipboard",
|
|
20
|
-
value: "clipboard",
|
|
21
|
-
short: "π Added to your clipboard",
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
name: "Write to terminal",
|
|
25
|
-
value: "terminal",
|
|
26
|
-
short: `π ${url}`,
|
|
27
|
-
},
|
|
28
|
-
],
|
|
29
|
-
},
|
|
30
|
-
])
|
|
31
23
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
24
|
+
const [command, args] = getOpenCommand(url)
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const child = spawn(command, args, {
|
|
28
|
+
detached: true,
|
|
29
|
+
stdio: "ignore",
|
|
30
|
+
windowsVerbatimArguments: true,
|
|
31
|
+
})
|
|
32
|
+
child.on("error", () => {})
|
|
33
|
+
child.unref()
|
|
34
|
+
} catch {
|
|
35
|
+
// The URL is printed above, so it can still be opened by hand
|
|
36
36
|
}
|
|
37
|
+
}
|
|
37
38
|
|
|
38
|
-
|
|
39
|
+
module.exports = {
|
|
40
|
+
openUrl,
|
|
39
41
|
}
|
|
@@ -1,22 +1,42 @@
|
|
|
1
|
-
const
|
|
2
|
-
const fs = require("fs")
|
|
1
|
+
const fs = require("node:fs")
|
|
3
2
|
|
|
4
|
-
const
|
|
3
|
+
const TEMPLATE_FILES = new Set([".env.example", ".env.sample", ".env.template"])
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
const isEnvFile = (name) =>
|
|
6
|
+
name === ".env" || (name.startsWith(".env.") && !TEMPLATE_FILES.has(name))
|
|
7
|
+
|
|
8
|
+
const getEnvFiles = () => {
|
|
9
|
+
const files = fs
|
|
10
|
+
.readdirSync(".", {withFileTypes: true})
|
|
11
|
+
.filter((entry) => entry.isFile() && isEnvFile(entry.name))
|
|
12
|
+
.map((entry) => entry.name)
|
|
13
|
+
|
|
14
|
+
return files.length > 0 ? files : [".env"]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const setEnvLine = (content, name, line) => {
|
|
18
|
+
const lines = content.split("\n")
|
|
19
|
+
const isVarLine = (text) => text.startsWith(`${name}=`)
|
|
20
|
+
const index = lines.findIndex(isVarLine)
|
|
21
|
+
|
|
22
|
+
if (index === -1) {
|
|
23
|
+
const separator = content && !content.endsWith("\n") ? "\n" : ""
|
|
24
|
+
return `${content}${separator}${line}\n`
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return lines
|
|
28
|
+
.map((text, i) => (i === index ? line : text))
|
|
29
|
+
.filter((text, i) => i === index || !isVarLine(text))
|
|
30
|
+
.join("\n")
|
|
8
31
|
}
|
|
9
32
|
|
|
10
33
|
const writeToEnvFiles = (name, value) => {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
})
|
|
16
|
-
console.log("")
|
|
17
|
-
} catch (e) {
|
|
18
|
-
throw new Error(e)
|
|
34
|
+
for (const file of getEnvFiles()) {
|
|
35
|
+
const content = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : ""
|
|
36
|
+
fs.writeFileSync(file, setEnvLine(content, name, `${name}=${value}`))
|
|
37
|
+
console.log(`β
${name} written to ${file}`)
|
|
19
38
|
}
|
|
39
|
+
console.log("")
|
|
20
40
|
}
|
|
21
41
|
|
|
22
42
|
module.exports = {
|
package/utils/wait-input.js
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
/* global Promise */
|
|
2
|
-
const readline = require("readline")
|
|
3
|
-
|
|
4
|
-
exports.waitInput = async (message) => {
|
|
5
|
-
return new Promise((resolve) => {
|
|
6
|
-
const rl = readline.createInterface({
|
|
7
|
-
input: process.stdin,
|
|
8
|
-
output: process.stdout,
|
|
9
|
-
})
|
|
10
|
-
|
|
11
|
-
console.log("")
|
|
12
|
-
|
|
13
|
-
rl.question(`π¦ ${message} Press any key to continue setup`, () => {
|
|
14
|
-
console.log("")
|
|
15
|
-
console.log("------------------------")
|
|
16
|
-
resolve()
|
|
17
|
-
})
|
|
18
|
-
})
|
|
19
|
-
}
|