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 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<h1>
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
- GOOGLE_PHOTOS_TOKEN={"access_token":"ya..54c","refresh_token":"1..qq0","scope":"https://www.googleapis.com/auth/photoslibrary.readonly","token_type":"Bearer","expiry_date":1598277881619}
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 NPM registry:
23
+ It requires Node.js 22 or later. Download the package from the npm registry:
24
24
 
25
25
  ```shell
26
- yarn add google-oauth2-env-vars
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: "GOOGLE_PHOTOS_TOKEN",
36
+ token: "GOOGLE_DOCS_TOKEN",
37
37
  // Authorization scope
38
- scope: ["https://www.googleapis.com/auth/photoslibrary.readonly"],
38
+ scope: ["https://www.googleapis.com/auth/documents.readonly"],
39
39
  // APIs to enable in Google console
40
- apis: ["photoslibrary.googleapis.com"],
40
+ apis: ["docs.googleapis.com"],
41
41
  //
42
42
  // Optional
43
43
  //
44
- // Port for node server
45
- port: 5000, // DEFAULT
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: "GOOGLE_PHOTOS_TOKEN",
71
+ token: "GOOGLE_DOCS_TOKEN",
59
72
  })
60
73
  const auth = await googleOAuth2.getAuth()
61
- const {token} = await auth.getAccessToken()
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
- CLIENT_ID,
9
- CLIENT_SECRET,
10
- DEFAULT_PORT,
11
- TOKEN_FIELDS,
12
- } = require("./utils/constants")
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 || DEFAULT_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 token
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
- let envClientId
47
- let envClientSecret
48
- let envToken
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 (process.env[CLIENT_ID]) {
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 (process.env[CLIENT_SECRET]) {
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 (this.tokenName && process.env[this.tokenName]) {
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
- if (!this.isTokenValid(envToken)) {
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]: envClientId,
78
- [CLIENT_SECRET]: envClientSecret,
79
- [this.tokenName]: envToken,
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(`βœ…${CLIENT_ID} found in .env`)
89
- console.log(`βœ…${CLIENT_SECRET} found in .env`)
90
- console.log(`βœ…${this.tokenName} found in .env`)
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(`❌${e.message}`)
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
- const auth = await initAuth({
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": "1.4.0",
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
- "clipboardy": "^2.3.0",
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": "^8.36.0",
35
- "eslint-config-prettier": "^8.8.0",
36
- "husky": "^8.0.3",
37
- "lint-staged": "^13.2.0",
38
- "prettier": "2.8.7"
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
- "lint": "eslint index.js utils/*.js",
42
- "prepare": "husky install",
43
- "prepublishOnly": "yarn lint"
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
  }
@@ -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,10 @@
1
+ declare const _exports: {
2
+ askClientIds: typeof askClientIds;
3
+ };
4
+ export = _exports;
5
+ declare const askClientIds: ({ port }: {
6
+ port: any;
7
+ }) => Promise<{
8
+ clientId: string;
9
+ clientSecret: string;
10
+ }>;
@@ -0,0 +1,7 @@
1
+ declare const _exports: {
2
+ ask: typeof ask;
3
+ askRequired: typeof askRequired;
4
+ };
5
+ export = _exports;
6
+ declare const ask: (question: any) => Promise<string>;
7
+ declare const askRequired: (question: any) => Promise<string>;
@@ -0,0 +1,8 @@
1
+ declare const _exports: {
2
+ CLIENT_ID: string;
3
+ CLIENT_SECRET: string;
4
+ OAUTH_URL: string;
5
+ ENABLE_API_URL: string;
6
+ TOKEN_FIELDS: string[];
7
+ };
8
+ export = _exports;
@@ -0,0 +1,8 @@
1
+ declare const _exports: {
2
+ getAuthorizationCode: typeof getAuthorizationCode;
3
+ };
4
+ export = _exports;
5
+ declare const getAuthorizationCode: ({ port, onListening }: {
6
+ onListening: any;
7
+ port: any;
8
+ }) => Promise<any>;
@@ -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;
@@ -0,0 +1,8 @@
1
+ declare const _exports: {
2
+ openUrl: typeof openUrl;
3
+ };
4
+ export = _exports;
5
+ declare const openUrl: ({ message, url }: {
6
+ message: any;
7
+ url: any;
8
+ }) => void;
@@ -0,0 +1,5 @@
1
+ declare const _exports: {
2
+ writeToEnvFiles: typeof writeToEnvFiles;
3
+ };
4
+ export = _exports;
5
+ declare const writeToEnvFiles: (name: any, value: any) => void;
@@ -1,11 +1,10 @@
1
- const inquirer = require("inquirer")
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 an "Google OAuth 2.0 Client ID" with the config:')
21
- console.info('Type: "Web application"')
22
- console.info('Redirect uri: "http://localhost:5000/callback"')
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
- await openUrl({message: "Google OAuth 2.0 URL:", url: OAUTH_URL})
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 {clientId, clientSecret} = await inquirer.prompt([
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
+ }
@@ -1,7 +1,6 @@
1
1
  module.exports = {
2
2
  CLIENT_ID: "GOOGLE_OAUTH_CLIENT_ID",
3
3
  CLIENT_SECRET: "GOOGLE_OAUTH_CLIENT_SECRET",
4
- DEFAULT_PORT: 5000,
5
4
  OAUTH_URL:
6
5
  "https://console.developers.google.com/apis/credentials/oauthclient",
7
6
  ENABLE_API_URL:
@@ -1,34 +1,56 @@
1
- /* global Promise */
2
- const express = require("express")
1
+ const http = require("node:http")
3
2
 
4
- const {openUrl} = require("./open-url")
5
-
6
- const getAuthorizationCode = async ({url, port}) => {
7
- return new Promise((resolve, reject) => {
8
- const app = express()
3
+ const TEXT_HEADERS = {
4
+ "Content-Type": "text/plain; charset=utf-8",
5
+ Connection: "close",
6
+ }
9
7
 
10
- app.listen(port, async () => {
11
- console.log("🀚 You need to authorize your application")
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
- await openUrl({message: "Authorization URL:", url})
13
+ if (pathname !== "/callback") {
14
+ res.writeHead(404, TEXT_HEADERS).end()
15
+ return
16
+ }
14
17
 
15
- console.log("πŸ›‘ Follow the URL to authorize your application")
16
- })
18
+ const code = searchParams.get("code")
19
+ server.close()
17
20
 
18
- app.get("/callback", (req, res) => {
19
- if (req.query.code) {
21
+ if (code) {
20
22
  console.log("βœ… Application authorized")
21
- res.send(
22
- "Succeed! You can close this tab and go back to your terminal."
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
- reject("no code")
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,
@@ -1,10 +1,20 @@
1
- const {google} = require("googleapis")
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
- const {openUrl} = require("./open-url")
7
- const {waitInput} = require("./wait-input")
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
- try {
18
- console.log("------------------------")
19
- console.log("| Generating new token |")
20
- console.log("------------------------")
21
- console.log("")
22
- if (apisToEnable) {
23
- for (const api of apisToEnable) {
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 = {
@@ -1,23 +1,14 @@
1
- const {google} = require("googleapis")
1
+ const {OAuth2Client} = require("google-auth-library")
2
2
 
3
- const initAuth = async ({clientId, clientSecret, token}) => {
4
- const auth = new google.auth.OAuth2(clientId, clientSecret)
3
+ const initAuth = ({clientId, clientSecret, token}) => {
4
+ const auth = new OAuth2Client({clientId, clientSecret})
5
5
 
6
6
  auth.setCredentials(token)
7
7
 
8
- let expired = true
9
-
10
- if (token.expiry_date) {
11
- const nowDate = new Date()
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 inquirer = require("inquirer")
2
- const clipboardy = require("clipboardy")
3
- const open = require("open")
1
+ const {spawn} = require("node:child_process")
4
2
 
5
- exports.openUrl = async ({message, url}) => {
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
- if (choice === "browser") {
33
- open(url)
34
- } else if (choice === "clipboard") {
35
- clipboardy.writeSync(url)
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
- console.log("")
39
+ module.exports = {
40
+ openUrl,
39
41
  }
@@ -1,22 +1,42 @@
1
- const glob = require("glob")
2
- const fs = require("fs")
1
+ const fs = require("node:fs")
3
2
 
4
- const envFiles = glob.sync(".env*")
3
+ const TEMPLATE_FILES = new Set([".env.example", ".env.sample", ".env.template"])
5
4
 
6
- if (envFiles.length === 0) {
7
- envFiles.push(".env")
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
- try {
12
- envFiles.forEach((file) => {
13
- fs.appendFileSync(file, `${name}=${value}\n`)
14
- console.log(`βœ… ${name} added to ${file}`)
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 = {
@@ -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
- }