@anmiles/google-api-wrapper 7.0.1 → 7.0.3

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/CHANGELOG.md CHANGED
@@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [7.0.3](../../tags/v7.0.3) - 2023-04-22
9
+ ### Changed
10
+ - Always require refresh token for permanent credentials
11
+
12
+ ## [7.0.2](../../tags/v7.0.2) - 2023-04-22
13
+ ### Changed
14
+ - Show instructions in the browser to prevent direct opening profile-oriented pages in wrong browsers
15
+
8
16
  ## [7.0.1](../../tags/v7.0.1) - 2023-04-22
9
17
  ### Changed
10
18
  - Compatibility for `open` package
@@ -1,4 +1,4 @@
1
- export { getJSON, getJSONAsync, writeJSON };
1
+ export { getJSON, getJSONAsync, writeJSON, readJSON };
2
2
  declare const _default: {
3
3
  getJSON: typeof getJSON;
4
4
  getJSONAsync: typeof getJSONAsync;
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.writeJSON = exports.getJSONAsync = exports.getJSON = void 0;
6
+ exports.readJSON = exports.writeJSON = exports.getJSONAsync = exports.getJSON = void 0;
7
7
  const fs_1 = __importDefault(require("fs"));
8
8
  const logger_1 = require("./logger");
9
9
  const paths_1 = require("./paths");
@@ -46,6 +46,7 @@ function readJSON(filename) {
46
46
  const jsonString = fs_1.default.readFileSync(filename).toString();
47
47
  return JSON.parse(jsonString);
48
48
  }
49
+ exports.readJSON = readJSON;
49
50
  function checkJSON(filename, json) {
50
51
  if (json) {
51
52
  return;
@@ -1 +1 @@
1
- {"version":3,"file":"jsonLib.js","sourceRoot":"","sources":["../../src/lib/jsonLib.ts"],"names":[],"mappings":";;;;;;AAAA,4CAAoB;AACpB,qCAAiC;AACjC,mCAAqC;AAErC,wDAAgC;AAGhC,kBAAe,EAAE,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC;AAEzE,SAAS,OAAO,CAAI,QAAgB,EAAE,cAA8C,EAAE,YAAmC;IACxH,IAAI,YAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE;QAC5B,MAAM,IAAI,GAAG,iBAAO,CAAC,QAAQ,CAAI,QAAQ,CAAC,CAAC;QAE3C,IAAI,CAAC,YAAY,IAAI,YAAY,CAAC,IAAI,CAAC,EAAE;YACxC,OAAO,IAAI,CAAC;SACZ;KACD;IAED,MAAM,IAAI,GAAG,cAAc,EAAE,CAAC;IAC9B,iBAAO,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IAClC,IAAA,kBAAU,EAAC,QAAQ,CAAC,CAAC;IACrB,iBAAO,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IAClC,OAAO,IAAI,CAAC;AACb,CAAC;AAjBQ,0BAAO;AAmBhB,KAAK,UAAU,YAAY,CAAI,QAAgB,EAAE,mBAAqC,EAAE,iBAAiD;IACxI,IAAI,YAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE;QAC5B,MAAM,IAAI,GAAG,iBAAO,CAAC,QAAQ,CAAI,QAAQ,CAAC,CAAC;QAE3C,IAAI,CAAC,iBAAiB,IAAI,MAAM,iBAAiB,CAAC,IAAI,CAAC,EAAE;YACxD,OAAO,IAAI,CAAC;SACZ;KACD;IAED,MAAM,IAAI,GAAG,MAAM,mBAAmB,EAAE,CAAC;IACzC,iBAAO,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IAClC,IAAA,kBAAU,EAAC,QAAQ,CAAC,CAAC;IACrB,iBAAO,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IAClC,OAAO,IAAI,CAAC;AACb,CAAC;AAjCiB,oCAAY;AAmC9B,SAAS,SAAS,CAAI,QAAgB,EAAE,IAAO;IAC9C,MAAM,UAAU,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;IACtD,YAAE,CAAC,aAAa,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;AACxC,CAAC;AAtC+B,8BAAS;AAwCzC,SAAS,QAAQ,CAAI,QAAgB;IACpC,MAAM,UAAU,GAAG,YAAE,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,QAAQ,EAAE,CAAC;IACxD,OAAO,IAAI,CAAC,KAAK,CAAC,UAAU,CAAM,CAAC;AACpC,CAAC;AAED,SAAS,SAAS,CAAI,QAAgB,EAAE,IAAO;IAC9C,IAAI,IAAI,EAAE;QACT,OAAO;KACP;IACD,IAAA,cAAK,EAAC,QAAQ,QAAQ,sGAAsG,CAAC,CAAC;AAC/H,CAAC"}
1
+ {"version":3,"file":"jsonLib.js","sourceRoot":"","sources":["../../src/lib/jsonLib.ts"],"names":[],"mappings":";;;;;;AAAA,4CAAoB;AACpB,qCAAiC;AACjC,mCAAqC;AAErC,wDAAgC;AAGhC,kBAAe,EAAE,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC;AAEzE,SAAS,OAAO,CAAI,QAAgB,EAAE,cAA8C,EAAE,YAAmC;IACxH,IAAI,YAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE;QAC5B,MAAM,IAAI,GAAG,iBAAO,CAAC,QAAQ,CAAI,QAAQ,CAAC,CAAC;QAE3C,IAAI,CAAC,YAAY,IAAI,YAAY,CAAC,IAAI,CAAC,EAAE;YACxC,OAAO,IAAI,CAAC;SACZ;KACD;IAED,MAAM,IAAI,GAAG,cAAc,EAAE,CAAC;IAC9B,iBAAO,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IAClC,IAAA,kBAAU,EAAC,QAAQ,CAAC,CAAC;IACrB,iBAAO,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IAClC,OAAO,IAAI,CAAC;AACb,CAAC;AAjBQ,0BAAO;AAmBhB,KAAK,UAAU,YAAY,CAAI,QAAgB,EAAE,mBAAqC,EAAE,iBAAiD;IACxI,IAAI,YAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE;QAC5B,MAAM,IAAI,GAAG,iBAAO,CAAC,QAAQ,CAAI,QAAQ,CAAC,CAAC;QAE3C,IAAI,CAAC,iBAAiB,IAAI,MAAM,iBAAiB,CAAC,IAAI,CAAC,EAAE;YACxD,OAAO,IAAI,CAAC;SACZ;KACD;IAED,MAAM,IAAI,GAAG,MAAM,mBAAmB,EAAE,CAAC;IACzC,iBAAO,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IAClC,IAAA,kBAAU,EAAC,QAAQ,CAAC,CAAC;IACrB,iBAAO,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IAClC,OAAO,IAAI,CAAC;AACb,CAAC;AAjCiB,oCAAY;AAmC9B,SAAS,SAAS,CAAI,QAAgB,EAAE,IAAO;IAC9C,MAAM,UAAU,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;IACtD,YAAE,CAAC,aAAa,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;AACxC,CAAC;AAtC+B,8BAAS;AAwCzC,SAAS,QAAQ,CAAI,QAAgB;IACpC,MAAM,UAAU,GAAG,YAAE,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,QAAQ,EAAE,CAAC;IACxD,OAAO,IAAI,CAAC,KAAK,CAAC,UAAU,CAAM,CAAC;AACpC,CAAC;AA3C0C,4BAAQ;AA6CnD,SAAS,SAAS,CAAI,QAAgB,EAAE,IAAO;IAC9C,IAAI,IAAI,EAAE;QACT,OAAO;KACP;IACD,IAAA,cAAK,EAAC,QAAQ,QAAQ,sGAAsG,CAAC,CAAC;AAC/H,CAAC"}
@@ -1,27 +1,4 @@
1
1
  "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || function (mod) {
19
- if (mod && mod.__esModule) return mod;
20
- var result = {};
21
- if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
- __setModuleDefault(result, mod);
23
- return result;
24
- };
25
2
  var __importDefault = (this && this.__importDefault) || function (mod) {
26
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
27
4
  };
@@ -29,7 +6,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
29
6
  exports.getCredentials = exports.getSecrets = void 0;
30
7
  const http_1 = __importDefault(require("http"));
31
8
  const server_destroy_1 = __importDefault(require("server-destroy"));
32
- const colorette = __importStar(require("colorette"));
33
9
  const open_1 = __importDefault(require("open"));
34
10
  const jsonLib_1 = require("./jsonLib");
35
11
  const logger_1 = require("./logger");
@@ -37,6 +13,7 @@ const paths_1 = require("./paths");
37
13
  const secrets_1 = __importDefault(require("./secrets"));
38
14
  exports.default = { getScopes, getSecrets, getCredentials, validateCredentials, createCredentials, checkSecrets, getSecretsError, getScopesError };
39
15
  const callbackPort = 6006;
16
+ const startURI = `http://localhost:${callbackPort}/`;
40
17
  const callbackURI = `http://localhost:${callbackPort}/oauthcallback`;
41
18
  const tokenExpiration = 7 * 24 * 60 * 60 * 1000;
42
19
  function getScopes() {
@@ -53,15 +30,25 @@ function getSecrets(profile) {
53
30
  exports.getSecrets = getSecrets;
54
31
  async function getCredentials(profile, auth, options) {
55
32
  const credentialsFile = (0, paths_1.getCredentialsFile)(profile);
56
- return (options === null || options === void 0 ? void 0 : options.temporary)
57
- ? secrets_1.default.createCredentials(profile, auth, options)
58
- : (0, jsonLib_1.getJSONAsync)(credentialsFile, () => secrets_1.default.createCredentials(profile, auth, options), secrets_1.default.validateCredentials);
33
+ if (options === null || options === void 0 ? void 0 : options.temporary) {
34
+ return secrets_1.default.createCredentials(profile, auth, options);
35
+ }
36
+ return (0, jsonLib_1.getJSONAsync)(credentialsFile, async () => {
37
+ // eslint-disable-next-line camelcase
38
+ const refresh_token = (0, paths_1.ensureFile)(credentialsFile) ? (0, jsonLib_1.readJSON)(credentialsFile).refresh_token : undefined;
39
+ const credentials = await secrets_1.default.createCredentials(profile, auth, options);
40
+ // eslint-disable-next-line camelcase
41
+ return { refresh_token, ...credentials };
42
+ }, secrets_1.default.validateCredentials);
59
43
  }
60
44
  exports.getCredentials = getCredentials;
61
45
  async function validateCredentials(credentials) {
62
46
  if (!credentials.access_token) {
63
47
  return false;
64
48
  }
49
+ if (!credentials.refresh_token) {
50
+ return false;
51
+ }
65
52
  if (!credentials.expiry_date) {
66
53
  return true;
67
54
  }
@@ -73,31 +60,38 @@ async function createCredentials(profile, auth, options) {
73
60
  const authUrl = auth.generateAuthUrl({
74
61
  // eslint-disable-next-line camelcase
75
62
  access_type: 'offline',
63
+ prompt: (options === null || options === void 0 ? void 0 : options.temporary) ? undefined : 'consent',
76
64
  scope,
77
65
  });
78
66
  const server = http_1.default.createServer(async (request, response) => {
79
- response.end('<h1>Please close this page and return to application. Wait for application to be finished automatically.</h1>');
80
- if (request.url) {
81
- const url = new URL(`http://${request.headers.host}${request.url}`);
82
- const code = url.searchParams.get('code');
83
- if (!code) {
84
- return;
85
- }
86
- server.destroy();
87
- const { tokens } = await auth.getToken(code);
88
- resolve(tokens);
67
+ if (!request.url) {
68
+ response.end('');
69
+ return;
89
70
  }
71
+ const url = new URL(`http://${request.headers.host}${request.url}`);
72
+ const code = url.searchParams.get('code');
73
+ if (!code) {
74
+ response.end(formatMessage(`Please open <a href="${authUrl}">this link</a> in a browser that belongs to <strong>${profile}</strong> google profile`));
75
+ return;
76
+ }
77
+ response.end(formatMessage('Please close this page and return to application'));
78
+ server.destroy();
79
+ const { tokens } = await auth.getToken(code);
80
+ resolve(tokens);
90
81
  });
91
82
  (0, server_destroy_1.default)(server);
92
83
  server.listen(callbackPort);
93
- if (options === null || options === void 0 ? void 0 : options.temporary) {
94
- (0, logger_1.info)(`Please open ${colorette.yellow(authUrl)} in your browser using google profile for ${colorette.yellow(profile)} and allow access to ${colorette.yellow(scope.join(','))}`);
95
- }
96
- else {
97
- (0, open_1.default)(authUrl);
98
- }
84
+ (0, logger_1.warn)('Please check your browser for further actions');
85
+ (0, open_1.default)(startURI);
99
86
  });
100
87
  }
88
+ function formatMessage(message) {
89
+ return [
90
+ '<div style="margin: 1em auto; padding: 0 1em; border: 1px solid black; max-width: 600px; text-align: center; font-family: Arial, sans-serif">',
91
+ `<p>${message}</p>`,
92
+ '</div>',
93
+ ].join('\n');
94
+ }
101
95
  function checkSecrets(profile, secretsObject, secretsFile) {
102
96
  if (secretsObject.web.redirect_uris[0] === callbackURI) {
103
97
  return true;
@@ -1 +1 @@
1
- {"version":3,"file":"secrets.js","sourceRoot":"","sources":["../../src/lib/secrets.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"secrets.js","sourceRoot":"","sources":["../../src/lib/secrets.ts"],"names":[],"mappings":";;;;;;AAAA,gDAAwB;AACxB,oEAA2C;AAC3C,gDAAwB;AAGxB,uCAA4D;AAC5D,qCAAuC;AACvC,mCAAwF;AAExF,wDAAgC;AAGhC,kBAAe,EAAE,SAAS,EAAE,UAAU,EAAE,cAAc,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,YAAY,EAAE,eAAe,EAAE,cAAc,EAAE,CAAC;AAEhJ,MAAM,YAAY,GAAM,IAAI,CAAC;AAC7B,MAAM,QAAQ,GAAU,oBAAoB,YAAY,GAAG,CAAC;AAC5D,MAAM,WAAW,GAAO,oBAAoB,YAAY,gBAAgB,CAAC;AACzE,MAAM,eAAe,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAEhD,SAAS,SAAS;IACjB,MAAM,UAAU,GAAG,IAAA,qBAAa,GAAE,CAAC;IACnC,MAAM,MAAM,GAAO,IAAA,iBAAO,EAAW,UAAU,EAAE,GAAG,EAAE,CAAC,IAAA,cAAK,EAAC,iBAAO,CAAC,cAAc,CAAC,UAAU,CAAC,CAAU,CAAC,CAAC;IAC3G,OAAO,MAAM,CAAC;AACf,CAAC;AAED,SAAS,UAAU,CAAC,OAAe;IAClC,MAAM,WAAW,GAAK,IAAA,sBAAc,EAAC,OAAO,CAAC,CAAC;IAC9C,MAAM,aAAa,GAAG,IAAA,iBAAO,EAAU,WAAW,EAAE,GAAG,EAAE,CAAC,IAAA,cAAK,EAAC,iBAAO,CAAC,eAAe,CAAC,OAAO,EAAE,WAAW,CAAC,CAAU,CAAC,CAAC;IACzH,iBAAO,CAAC,YAAY,CAAC,OAAO,EAAE,aAAa,EAAE,WAAW,CAAC,CAAC;IAC1D,OAAO,aAAa,CAAC;AACtB,CAAC;AAnBQ,gCAAU;AAqBnB,KAAK,UAAU,cAAc,CAAC,OAAe,EAAE,IAAoC,EAAE,OAAqB;IACzG,MAAM,eAAe,GAAG,IAAA,0BAAkB,EAAC,OAAO,CAAC,CAAC;IAEpD,IAAI,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAE,SAAS,EAAE;QACvB,OAAO,iBAAO,CAAC,iBAAiB,CAAC,OAAO,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;KACzD;IAED,OAAO,IAAA,sBAAY,EAAC,eAAe,EAAE,KAAK,IAAI,EAAE;QAC/C,qCAAqC;QACrC,MAAM,aAAa,GAAG,IAAA,kBAAU,EAAC,eAAe,CAAC,CAAC,CAAC,CAAC,IAAA,kBAAQ,EAA8B,eAAe,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,SAAS,CAAC;QACrI,MAAM,WAAW,GAAK,MAAM,iBAAO,CAAC,iBAAiB,CAAC,OAAO,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;QAC9E,qCAAqC;QACrC,OAAO,EAAE,aAAa,EAAE,GAAG,WAAW,EAAE,CAAC;IAC1C,CAAC,EAAE,iBAAO,CAAC,mBAAmB,CAAC,CAAC;AACjC,CAAC;AAnCoB,wCAAc;AAqCnC,KAAK,UAAU,mBAAmB,CAAC,WAAwC;IAC1E,IAAI,CAAC,WAAW,CAAC,YAAY,EAAE;QAC9B,OAAO,KAAK,CAAC;KACb;IAED,IAAI,CAAC,WAAW,CAAC,aAAa,EAAE;QAC/B,OAAO,KAAK,CAAC;KACb;IAED,IAAI,CAAC,WAAW,CAAC,WAAW,EAAE;QAC7B,OAAO,IAAI,CAAC;KACZ;IAED,OAAO,IAAI,IAAI,EAAE,CAAC,OAAO,EAAE,GAAG,WAAW,CAAC,WAAW,GAAG,eAAe,CAAC;AACzE,CAAC;AAED,KAAK,UAAU,iBAAiB,CAAC,OAAe,EAAE,IAAkC,EAAE,OAAqB;IAC1G,MAAM,KAAK,GAAG,CAAA,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAE,MAAM,KAAI,iBAAO,CAAC,SAAS,EAAE,CAAC;IAErD,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC9B,MAAM,OAAO,GAAG,IAAI,CAAC,eAAe,CAAC;YACpC,qCAAqC;YACrC,WAAW,EAAG,SAAS;YACvB,MAAM,EAAQ,CAAA,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAE,SAAS,EAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS;YACxD,KAAK;SACL,CAAC,CAAC;QAEH,MAAM,MAAM,GAAG,cAAI,CAAC,YAAY,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE;YAC5D,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE;gBACjB,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;gBACjB,OAAO;aACP;YAED,MAAM,GAAG,GAAI,IAAI,GAAG,CAAC,UAAU,OAAO,CAAC,OAAO,CAAC,IAAI,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;YACrE,MAAM,IAAI,GAAG,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;YAE1C,IAAI,CAAC,IAAI,EAAE;gBACV,QAAQ,CAAC,GAAG,CAAC,aAAa,CAAC,wBAAwB,OAAO,wDAAwD,OAAO,0BAA0B,CAAC,CAAC,CAAC;gBACtJ,OAAO;aACP;YAED,QAAQ,CAAC,GAAG,CAAC,aAAa,CAAC,kDAAkD,CAAC,CAAC,CAAC;YAChF,MAAM,CAAC,OAAO,EAAE,CAAC;YACjB,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;YAC7C,OAAO,CAAC,MAAM,CAAC,CAAC;QACjB,CAAC,CAAC,CAAC;QAEH,IAAA,wBAAa,EAAC,MAAM,CAAC,CAAC;QACtB,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;QAC5B,IAAA,aAAI,EAAC,+CAA+C,CAAC,CAAC;QACtD,IAAA,cAAI,EAAC,QAAQ,CAAC,CAAC;IAChB,CAAC,CAAC,CAAC;AACJ,CAAC;AAED,SAAS,aAAa,CAAC,OAAe;IACrC,OAAO;QACN,+IAA+I;QAC/I,MAAM,OAAO,MAAM;QACnB,QAAQ;KACR,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACd,CAAC;AAED,SAAS,YAAY,CAAC,OAAe,EAAE,aAAsB,EAAE,WAAmB;IACjF,IAAI,aAAa,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC,KAAK,WAAW,EAAE;QACvD,OAAO,IAAI,CAAC;KACZ;IACD,IAAA,cAAK,EAAC,qDAAqD,WAAW,MAAM,iBAAO,CAAC,eAAe,CAAC,OAAO,EAAE,WAAW,CAAC,EAAE,CAAC,CAAC;AAC9H,CAAC;AAED,SAAS,cAAc,CAAC,UAAkB;IACzC,OAAO;QACN,QAAQ,UAAU,aAAa;QAC/B,iDAAiD,UAAU,kCAAkC;KAC7F,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACd,CAAC;AAED,SAAS,eAAe,CAAC,OAAe,EAAE,WAAmB;IAC5D,OAAO;QACN,QAAQ,WAAW,aAAa;QAChC,2BAA2B;QAC3B,wDAAwD;QACxD,yBAAyB;QACzB,2DAA2D;QAC3D,yDAAyD;QACzD,+DAA+D;QAC/D,sCAAsC;QACtC,0BAA0B;QAC1B,yDAAyD;QACzD,iDAAiD;QACjD,qDAAqD;QACrD,2BAA2B;QAC3B,wBAAwB;QACxB,wCAAwC;QACxC,0GAA0G;QAC1G,mCAAmC;QACnC,oCAAoC;QACpC,uBAAuB,iBAAO,CAAC,SAAS,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE;QACtD,mCAAmC;QACnC,yBAAyB;QACzB,wBAAwB;QACxB,mCAAmC;QACnC,oDAAoD;QACpD,6CAA6C;QAC7C,+DAA+D;QAC/D,mDAAmD;QACnD,yCAAyC;QACzC,wCAAwC,WAAW,EAAE;QACrD,wBAAwB;QACxB,uEAAuE,OAAO,OAAO;QACrF,8BAA8B;KAC9B,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACd,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anmiles/google-api-wrapper",
3
- "version": "7.0.1",
3
+ "version": "7.0.3",
4
4
  "description": "Provides quick interface for getting google API data",
5
5
  "keywords": [
6
6
  "google",
@@ -5,6 +5,7 @@ import open from 'open';
5
5
  import type GoogleApis from 'googleapis';
6
6
  import jsonLib from '../jsonLib';
7
7
  import logger from '../logger';
8
+ import paths from '../paths';
8
9
  import type { Secrets } from '../../types';
9
10
 
10
11
  import secrets from '../secrets';
@@ -14,7 +15,7 @@ jest.mock<typeof secrets>('../secrets', () => ({
14
15
  getSecrets : jest.fn().mockImplementation(() => secretsJSON),
15
16
  getCredentials : jest.fn(),
16
17
  validateCredentials : jest.fn(),
17
- createCredentials : jest.fn(),
18
+ createCredentials : jest.fn().mockImplementation(() => credentialsJSON),
18
19
  checkSecrets : jest.fn(),
19
20
  getScopesError : jest.fn().mockImplementation(() => scopesError),
20
21
  getSecretsError : jest.fn().mockImplementation(() => secretsError),
@@ -41,20 +42,30 @@ jest.mock<Partial<typeof colorette>>('colorette', () => ({
41
42
  yellow : jest.fn().mockImplementation((text) => `yellow:${text}`),
42
43
  }));
43
44
 
44
- jest.mock<Partial<typeof open>>('open', () => jest.fn());
45
+ jest.mock('open', () => jest.fn().mockImplementation((url: string) => {
46
+ willOpen(url.replace('http://localhost:6006', ''));
47
+ }));
45
48
 
46
49
  jest.mock<Partial<typeof jsonLib>>('../jsonLib', () => ({
47
- getJSON : jest.fn().mockImplementation(() => json),
50
+ getJSON : jest.fn().mockImplementation(() => json),
48
51
  getJSONAsync : jest.fn().mockImplementation(async () => json),
52
+ readJSON : jest.fn().mockImplementation(async () => json),
49
53
  }));
50
54
 
51
55
  jest.mock<Partial<typeof logger>>('../logger', () => ({
52
- info : jest.fn(),
56
+ warn : jest.fn(),
53
57
  error : jest.fn().mockImplementation((error) => {
54
58
  throw error;
55
59
  }) as jest.Mock<never, any>,
56
60
  }));
57
61
 
62
+ jest.mock<Partial<typeof paths>>('../paths', () => ({
63
+ getScopesFile : jest.fn().mockImplementation(() => scopesFile),
64
+ getSecretsFile : jest.fn().mockImplementation(() => secretsFile),
65
+ getCredentialsFile : jest.fn().mockImplementation(() => credentialsFile),
66
+ ensureFile : jest.fn().mockImplementation(() => fileExists),
67
+ }));
68
+
58
69
  const profile = 'username1';
59
70
  const scopesFile = 'scopes.json';
60
71
  const secretsFile = 'secrets/username1.json';
@@ -83,11 +94,12 @@ const secretsJSON: Secrets = {
83
94
  },
84
95
  };
85
96
 
86
- const credentialsJSON = {
87
- token : {},
97
+ const credentialsJSON: GoogleApis.Auth.Credentials = {
98
+ // eslint-disable-next-line camelcase
99
+ access_token : 'access_token222',
88
100
  };
89
101
 
90
- let json: object;
102
+ let json: any;
91
103
 
92
104
  const code = 'code';
93
105
  const authUrl = 'https://authUrl';
@@ -96,8 +108,6 @@ const auth = {
96
108
  getToken : jest.fn().mockResolvedValue({ tokens : credentialsJSON }),
97
109
  } as unknown as GoogleApis.Common.OAuth2Client;
98
110
 
99
- let request: http.IncomingMessage;
100
-
101
111
  const response = {
102
112
  end : jest.fn(),
103
113
  } as unknown as http.ServerResponse;
@@ -107,6 +117,16 @@ let serverCallback: (
107
117
  response: http.ServerResponse
108
118
  ) => Promise<typeof credentialsJSON>;
109
119
 
120
+ function willOpen(url: string | undefined, timeout?: number) {
121
+ setTimeout(async () => {
122
+ await serverCallback({
123
+ url,
124
+ headers : {
125
+ host : 'localhost:6006',
126
+ },
127
+ } as http.IncomingMessage, response);
128
+ }, timeout || 0);
129
+ }
110
130
  let closedTime: number;
111
131
 
112
132
  const on = jest.fn().mockImplementation((event: string, listener: (...args: any[]) => void) => {
@@ -128,6 +148,8 @@ const connections = [
128
148
  { remoteAddress : 'server', remotePort : '1003', on : jest.fn(), destroy : jest.fn() },
129
149
  ];
130
150
 
151
+ let fileExists: boolean;
152
+
131
153
  describe('src/lib/secrets', () => {
132
154
  describe('getScopes', () => {
133
155
  const getJSONSpy = jest.spyOn(jsonLib, 'getJSON');
@@ -193,7 +215,8 @@ describe('src/lib/secrets', () => {
193
215
  const getJSONAsyncSpy = jest.spyOn(jsonLib, 'getJSONAsync');
194
216
 
195
217
  beforeEach(() => {
196
- json = credentialsJSON;
218
+ json = credentialsJSON;
219
+ fileExists = false;
197
220
  });
198
221
 
199
222
  it('should get json from credentials file by default', async () => {
@@ -203,7 +226,7 @@ describe('src/lib/secrets', () => {
203
226
  expect(getJSONAsyncSpy.mock.calls[0][0]).toEqual(credentialsFile);
204
227
  });
205
228
 
206
- it('should get json from credentials file if temporariness explicitly unset', async () => {
229
+ it('should get json from credentials file if temporariness not set', async () => {
207
230
  await original.getCredentials(profile, auth, { temporary : false });
208
231
 
209
232
  expect(getJSONAsyncSpy).toBeCalled();
@@ -216,18 +239,22 @@ describe('src/lib/secrets', () => {
216
239
  expect(getJSONAsyncSpy).not.toBeCalled();
217
240
  });
218
241
 
219
- it('should fallback to createCredentials by default', async () => {
242
+ it('should call createCredentials in fallback', async () => {
220
243
  await original.getCredentials(profile, auth);
221
244
 
245
+ expect(secrets.createCredentials).not.toBeCalled();
246
+
222
247
  const fallback = getJSONAsyncSpy.mock.calls[0][1];
223
248
  await fallback();
224
249
 
225
250
  expect(secrets.createCredentials).toBeCalledWith(profile, auth, undefined);
226
251
  });
227
252
 
228
- it('should call createCredentials directly if temporariness explicitly unset', async () => {
253
+ it('should call createCredentials in fallback if temporariness not set', async () => {
229
254
  await original.getCredentials(profile, auth, { temporary : false });
230
255
 
256
+ expect(secrets.createCredentials).not.toBeCalled();
257
+
231
258
  const fallback = getJSONAsyncSpy.mock.calls[0][1];
232
259
  await fallback();
233
260
 
@@ -240,22 +267,60 @@ describe('src/lib/secrets', () => {
240
267
  expect(secrets.createCredentials).toBeCalledWith(profile, auth, { temporary : true });
241
268
  });
242
269
 
243
- it('should return credentials by default', async () => {
244
- const result = await original.getCredentials(profile, auth);
270
+ it('should return credentials in fallback', async () => {
271
+ await original.getCredentials(profile, auth);
272
+
273
+ const fallback = getJSONAsyncSpy.mock.calls[0][1];
274
+ const result = await fallback();
245
275
 
246
276
  expect(result).toEqual(credentialsJSON);
247
277
  });
248
278
 
249
- it('should return credentials if temporariness explicitly unset', async () => {
250
- const result = await original.getCredentials(profile, auth, { temporary : false });
279
+ it('should return credentials in fallback and copy existing refresh token from existing file if created credentials do not have refresh token', async () => {
280
+ fileExists = true;
281
+ // eslint-disable-next-line camelcase
282
+ jest.spyOn(jsonLib, 'readJSON').mockReturnValueOnce({ ...credentialsJSON, refresh_token : 'refresh_token' });
251
283
 
284
+ await original.getCredentials(profile, auth);
285
+
286
+ const fallback = getJSONAsyncSpy.mock.calls[0][1];
287
+ const result = await fallback();
288
+
289
+ expect(jsonLib.readJSON).toBeCalledWith(credentialsFile);
290
+ // eslint-disable-next-line camelcase
291
+ expect(result).toEqual({ ...credentialsJSON, refresh_token : 'refresh_token' });
292
+ });
293
+
294
+ it('should return credentials in fallback and leave refresh_token undefined if there is no existing file', async () => {
295
+ fileExists = false;
296
+ // eslint-disable-next-line camelcase
297
+ jest.spyOn(jsonLib, 'readJSON').mockReturnValueOnce({ ...credentialsJSON, refresh_token : 'refresh_token' });
298
+
299
+ await original.getCredentials(profile, auth);
300
+
301
+ const fallback = getJSONAsyncSpy.mock.calls[0][1];
302
+ const result = await fallback();
303
+
304
+ expect(jsonLib.readJSON).not.toBeCalled();
305
+ // eslint-disable-next-line camelcase
252
306
  expect(result).toEqual(credentialsJSON);
253
307
  });
254
308
 
255
- it('should return nothing if temporariness set', async () => {
256
- const result = await original.getCredentials(profile, auth, { temporary : true });
309
+ it('should return credentials in fallback and leave refresh_token as is if it is set from createCredentials', async () => {
310
+ fileExists = true;
311
+ // eslint-disable-next-line camelcase
312
+ jest.spyOn(secrets, 'createCredentials').mockResolvedValueOnce({ ...credentialsJSON, refresh_token : 'refresh_token_1' });
313
+ // eslint-disable-next-line camelcase
314
+ jest.spyOn(jsonLib, 'readJSON').mockReturnValueOnce({ ...credentialsJSON, refresh_token : 'refresh_token_2' });
315
+
316
+ await original.getCredentials(profile, auth);
317
+
318
+ const fallback = getJSONAsyncSpy.mock.calls[0][1];
319
+ const result = await fallback();
257
320
 
258
- expect(result).toBeUndefined();
321
+ expect(jsonLib.readJSON).toBeCalledWith(credentialsFile);
322
+ // eslint-disable-next-line camelcase
323
+ expect(result).toEqual({ ...credentialsJSON, refresh_token : 'refresh_token_1' });
259
324
  });
260
325
  });
261
326
 
@@ -264,47 +329,55 @@ describe('src/lib/secrets', () => {
264
329
  expect(await original.validateCredentials({})).toEqual(false);
265
330
  });
266
331
 
332
+ it('should return true if no refresh token', async () => {
333
+ // eslint-disable-next-line camelcase
334
+ expect(await original.validateCredentials({ access_token : 'token' })).toEqual(false);
335
+ });
336
+
267
337
  it('should return true if no expiration', async () => {
268
338
  // eslint-disable-next-line camelcase
269
- expect(await original.validateCredentials({ access_token : 'token' })).toEqual(true);
339
+ expect(await original.validateCredentials({ access_token : 'token', refresh_token : 'token' })).toEqual(true);
270
340
  });
271
341
 
272
342
  it('should return true if credentials are not more than 1 week ago', async () => {
273
343
  const expiryDate = new Date();
274
344
  expiryDate.setDate(expiryDate.getDate() - 6);
275
345
  // eslint-disable-next-line camelcase
276
- expect(await original.validateCredentials({ access_token : 'token', expiry_date : expiryDate.getTime() })).toEqual(true);
346
+ expect(await original.validateCredentials({ access_token : 'token', refresh_token : 'token', expiry_date : expiryDate.getTime() })).toEqual(true);
277
347
  });
278
348
 
279
349
  it('should return true if credentials are more than 1 week ago', async () => {
280
350
  const expiryDate = new Date();
281
351
  expiryDate.setDate(expiryDate.getDate() - 8);
282
352
  // eslint-disable-next-line camelcase
283
- expect(await original.validateCredentials({ access_token : 'token', expiry_date : expiryDate.getTime() })).toEqual(false);
353
+ expect(await original.validateCredentials({ access_token : 'token', refresh_token : 'token', expiry_date : expiryDate.getTime() })).toEqual(false);
284
354
  });
285
355
  });
286
356
 
287
357
  describe('createCredentials', () => {
288
- function willOpen(request: http.IncomingMessage, timeout: number) {
289
- setTimeout(async () => {
290
- await serverCallback(request, response);
291
- }, timeout);
292
- }
293
-
294
- beforeEach(() => {
295
- request = {
296
- url : `/request.url?code=${code}`,
297
- headers : {
298
- host : 'localhost:6006',
299
- },
300
- } as http.IncomingMessage;
301
- });
358
+ const tokenUrl = `/request.url?code=${code}`;
302
359
 
303
360
  it('should generate authUrl', async () => {
304
- willOpen(request, 100);
361
+ willOpen(tokenUrl, 100);
305
362
 
306
363
  await original.createCredentials(profile, auth);
307
364
 
365
+ expect(auth.generateAuthUrl).toBeCalledWith({
366
+ // eslint-disable-next-line camelcase
367
+ access_type : 'offline',
368
+ prompt : 'consent',
369
+ scope : [
370
+ 'https://www.googleapis.com/auth/calendar.calendars.readonly',
371
+ 'https://www.googleapis.com/auth/calendar.events.readonly',
372
+ ],
373
+ });
374
+ });
375
+
376
+ it('should generate authUrl and do not require consent if credentials are temporary because refresh token is not required in credentials JSON', async () => {
377
+ willOpen(tokenUrl, 100);
378
+
379
+ await original.createCredentials(profile, auth, { temporary : true });
380
+
308
381
  expect(auth.generateAuthUrl).toBeCalledWith({
309
382
  // eslint-disable-next-line camelcase
310
383
  access_type : 'offline',
@@ -316,19 +389,20 @@ describe('src/lib/secrets', () => {
316
389
  });
317
390
 
318
391
  it('should generate authUrl with custom scopes', async () => {
319
- willOpen(request, 100);
392
+ willOpen(tokenUrl, 100);
320
393
 
321
394
  await original.createCredentials(profile, auth, { scopes : [ 'scope1', 'scope2' ] });
322
395
 
323
396
  expect(auth.generateAuthUrl).toBeCalledWith({
324
397
  // eslint-disable-next-line camelcase
325
398
  access_type : 'offline',
399
+ prompt : 'consent',
326
400
  scope : [ 'scope1', 'scope2' ],
327
401
  });
328
402
  });
329
403
 
330
404
  it('should create server on 6006 port', async () => {
331
- willOpen(request, 100);
405
+ willOpen(tokenUrl, 100);
332
406
 
333
407
  await original.createCredentials(profile, auth);
334
408
 
@@ -336,43 +410,43 @@ describe('src/lib/secrets', () => {
336
410
  expect(listen).toBeCalledWith(6006);
337
411
  });
338
412
 
339
- it('should open browser page without prompt if this is permanent request that tends to save credentials in the file', async () => {
340
- willOpen(request, 100);
413
+ it('should open browser page and warn about it', async () => {
414
+ willOpen(tokenUrl, 100);
341
415
 
342
416
  await original.createCredentials(profile, auth);
343
417
 
344
- expect(open).toBeCalledWith('https://authUrl');
345
- expect(logger.info).not.toBeCalled();
418
+ expect(open).toBeCalledWith('http://localhost:6006/');
419
+ expect(logger.warn).toBeCalledWith('Please check your browser for further actions');
346
420
  });
347
421
 
348
- it('should ask to open browser page', async () => {
349
- willOpen(request, 100);
422
+ it('should show nothing on the browser page if request.url is empty', async () => {
423
+ willOpen('', 100);
424
+ willOpen(tokenUrl, 200);
350
425
 
351
- await original.createCredentials(profile, auth, { temporary : true });
426
+ await original.createCredentials(profile, auth);
352
427
 
353
- expect(open).not.toBeCalled();
354
- expect(logger.info).toBeCalledWith(`Please open yellow:https://authUrl in your browser using google profile for yellow:${profile} and allow access to yellow:https://www.googleapis.com/auth/calendar.calendars.readonly,https://www.googleapis.com/auth/calendar.events.readonly`);
428
+ expect(response.end).toBeCalledWith('');
355
429
  });
356
430
 
357
- it('should ask to open browser page with custom scopes', async () => {
358
- willOpen(request, 100);
431
+ it('should show opening instructions if opened the home page', async () => {
432
+ willOpen('/', 100);
433
+ willOpen(tokenUrl, 200);
359
434
 
360
- await original.createCredentials(profile, auth, { temporary : true, scopes : [ 'scope1', 'scope2' ] });
435
+ await original.createCredentials(profile, auth);
361
436
 
362
- expect(open).not.toBeCalled();
363
- expect(logger.info).toBeCalledWith(`Please open yellow:https://authUrl in your browser using google profile for yellow:${profile} and allow access to yellow:scope1,scope2`);
437
+ expect(response.end).toBeCalledWith(`<div style="margin: 1em auto; padding: 0 1em; border: 1px solid black; max-width: 600px; text-align: center; font-family: Arial, sans-serif">\n<p>Please open <a href="${authUrl}">this link</a> in a browser that belongs to <strong>${profile}</strong> google profile</p>\n</div>`);
364
438
  });
365
439
 
366
440
  it('should ask to close webpage', async () => {
367
- willOpen(request, 100);
441
+ willOpen(tokenUrl, 100);
368
442
 
369
443
  await original.createCredentials(profile, auth);
370
444
 
371
- expect(response.end).toBeCalledWith('<h1>Please close this page and return to application. Wait for application to be finished automatically.</h1>');
445
+ expect(response.end).toBeCalledWith('<div style="margin: 1em auto; padding: 0 1em; border: 1px solid black; max-width: 600px; text-align: center; font-family: Arial, sans-serif">\n<p>Please close this page and return to application</p>\n</div>');
372
446
  });
373
447
 
374
448
  it('should close server and destroy all connections if request.url is truthy', async () => {
375
- willOpen(request, 100);
449
+ willOpen(tokenUrl, 100);
376
450
 
377
451
  await original.createCredentials(profile, auth);
378
452
 
@@ -384,12 +458,10 @@ describe('src/lib/secrets', () => {
384
458
  it('should only resolve when request.url is truthy', async () => {
385
459
  const emptyRequestTime = 100;
386
460
  const requestTime = 200;
387
- const emptyRequest = { ...request } as http.IncomingMessage;
388
- emptyRequest.url = undefined;
389
461
 
390
462
  const before = new Date().getTime();
391
- willOpen(emptyRequest, emptyRequestTime);
392
- willOpen(request, requestTime);
463
+ willOpen(undefined, emptyRequestTime);
464
+ willOpen(tokenUrl, requestTime);
393
465
 
394
466
  const result = await original.createCredentials(profile, auth);
395
467
  const after = new Date().getTime();
@@ -403,12 +475,10 @@ describe('src/lib/secrets', () => {
403
475
  it('should only resolve when request.url contains no code', async () => {
404
476
  const noCodeRequestTime = 100;
405
477
  const requestTime = 200;
406
- const noCodeRequest = { ...request } as http.IncomingMessage;
407
- noCodeRequest.url = '/request.url?param=value';
408
478
 
409
479
  const before = new Date().getTime();
410
- willOpen(noCodeRequest, noCodeRequestTime);
411
- willOpen(request, requestTime);
480
+ willOpen('/request.url?param=value', noCodeRequestTime);
481
+ willOpen(tokenUrl, requestTime);
412
482
 
413
483
  const result = await original.createCredentials(profile, auth);
414
484
  const after = new Date().getTime();
@@ -420,7 +490,7 @@ describe('src/lib/secrets', () => {
420
490
  });
421
491
 
422
492
  it('should return credentials JSON', async () => {
423
- willOpen(request, 100);
493
+ willOpen(tokenUrl, 100);
424
494
 
425
495
  const result = await original.createCredentials(profile, auth);
426
496
 
@@ -4,7 +4,7 @@ import { ensureFile } from './paths';
4
4
 
5
5
  import jsonLib from './jsonLib';
6
6
 
7
- export { getJSON, getJSONAsync, writeJSON };
7
+ export { getJSON, getJSONAsync, writeJSON, readJSON };
8
8
  export default { getJSON, getJSONAsync, writeJSON, readJSON, checkJSON };
9
9
 
10
10
  function getJSON<T>(filename: string, createCallback: () => Exclude<T, Promise<any>>, validateJSON?: (json: T) => boolean): T {
@@ -1,12 +1,11 @@
1
1
  import http from 'http';
2
2
  import enableDestroy from 'server-destroy';
3
- import * as colorette from 'colorette';
4
3
  import open from 'open';
5
4
  import type GoogleApis from 'googleapis';
6
5
  import type { Secrets, AuthOptions } from '../types';
7
- import { getJSON, getJSONAsync } from './jsonLib';
8
- import { info, error } from './logger';
9
- import { getScopesFile, getSecretsFile, getCredentialsFile } from './paths';
6
+ import { getJSON, getJSONAsync, readJSON } from './jsonLib';
7
+ import { warn, error } from './logger';
8
+ import { getScopesFile, getSecretsFile, getCredentialsFile, ensureFile } from './paths';
10
9
 
11
10
  import secrets from './secrets';
12
11
 
@@ -14,6 +13,7 @@ export { getSecrets, getCredentials };
14
13
  export default { getScopes, getSecrets, getCredentials, validateCredentials, createCredentials, checkSecrets, getSecretsError, getScopesError };
15
14
 
16
15
  const callbackPort = 6006;
16
+ const startURI = `http://localhost:${callbackPort}/`;
17
17
  const callbackURI = `http://localhost:${callbackPort}/oauthcallback`;
18
18
  const tokenExpiration = 7 * 24 * 60 * 60 * 1000;
19
19
 
@@ -33,9 +33,17 @@ function getSecrets(profile: string): Secrets {
33
33
  async function getCredentials(profile: string, auth: GoogleApis.Common.OAuth2Client, options?: AuthOptions): Promise<GoogleApis.Auth.Credentials> {
34
34
  const credentialsFile = getCredentialsFile(profile);
35
35
 
36
- return options?.temporary
37
- ? secrets.createCredentials(profile, auth, options)
38
- : getJSONAsync(credentialsFile, () => secrets.createCredentials(profile, auth, options), secrets.validateCredentials);
36
+ if (options?.temporary) {
37
+ return secrets.createCredentials(profile, auth, options);
38
+ }
39
+
40
+ return getJSONAsync(credentialsFile, async () => {
41
+ // eslint-disable-next-line camelcase
42
+ const refresh_token = ensureFile(credentialsFile) ? readJSON<GoogleApis.Auth.Credentials>(credentialsFile).refresh_token : undefined;
43
+ const credentials = await secrets.createCredentials(profile, auth, options);
44
+ // eslint-disable-next-line camelcase
45
+ return { refresh_token, ...credentials };
46
+ }, secrets.validateCredentials);
39
47
  }
40
48
 
41
49
  async function validateCredentials(credentials: GoogleApis.Auth.Credentials): Promise<boolean> {
@@ -43,6 +51,10 @@ async function validateCredentials(credentials: GoogleApis.Auth.Credentials): Pr
43
51
  return false;
44
52
  }
45
53
 
54
+ if (!credentials.refresh_token) {
55
+ return false;
56
+ }
57
+
46
58
  if (!credentials.expiry_date) {
47
59
  return true;
48
60
  }
@@ -57,37 +69,45 @@ async function createCredentials(profile: string, auth: GoogleApis.Auth.OAuth2Cl
57
69
  const authUrl = auth.generateAuthUrl({
58
70
  // eslint-disable-next-line camelcase
59
71
  access_type : 'offline',
72
+ prompt : options?.temporary ? undefined : 'consent',
60
73
  scope,
61
74
  });
62
75
 
63
76
  const server = http.createServer(async (request, response) => {
64
- response.end('<h1>Please close this page and return to application. Wait for application to be finished automatically.</h1>');
65
-
66
- if (request.url) {
67
- const url = new URL(`http://${request.headers.host}${request.url}`);
68
- const code = url.searchParams.get('code');
77
+ if (!request.url) {
78
+ response.end('');
79
+ return;
80
+ }
69
81
 
70
- if (!code) {
71
- return;
72
- }
82
+ const url = new URL(`http://${request.headers.host}${request.url}`);
83
+ const code = url.searchParams.get('code');
73
84
 
74
- server.destroy();
75
- const { tokens } = await auth.getToken(code);
76
- resolve(tokens);
85
+ if (!code) {
86
+ response.end(formatMessage(`Please open <a href="${authUrl}">this link</a> in a browser that belongs to <strong>${profile}</strong> google profile`));
87
+ return;
77
88
  }
89
+
90
+ response.end(formatMessage('Please close this page and return to application'));
91
+ server.destroy();
92
+ const { tokens } = await auth.getToken(code);
93
+ resolve(tokens);
78
94
  });
79
95
 
80
96
  enableDestroy(server);
81
97
  server.listen(callbackPort);
82
-
83
- if (options?.temporary) {
84
- info(`Please open ${colorette.yellow(authUrl)} in your browser using google profile for ${colorette.yellow(profile)} and allow access to ${colorette.yellow(scope.join(','))}`);
85
- } else {
86
- open(authUrl);
87
- }
98
+ warn('Please check your browser for further actions');
99
+ open(startURI);
88
100
  });
89
101
  }
90
102
 
103
+ function formatMessage(message: string): string {
104
+ return [
105
+ '<div style="margin: 1em auto; padding: 0 1em; border: 1px solid black; max-width: 600px; text-align: center; font-family: Arial, sans-serif">',
106
+ `<p>${message}</p>`,
107
+ '</div>',
108
+ ].join('\n');
109
+ }
110
+
91
111
  function checkSecrets(profile: string, secretsObject: Secrets, secretsFile: string): true | void {
92
112
  if (secretsObject.web.redirect_uris[0] === callbackURI) {
93
113
  return true;